diff --git a/src/ManiaTemplates/Components/MtComponent.cs b/src/ManiaTemplates/Components/MtComponent.cs index b167182..64512a4 100644 --- a/src/ManiaTemplates/Components/MtComponent.cs +++ b/src/ManiaTemplates/Components/MtComponent.cs @@ -14,6 +14,7 @@ public class MtComponent public required Dictionary Properties { get; init; } public required List Namespaces { get; init; } public required List Scripts { get; init; } + public HashSet Slots { get; init; } = new(); /// /// Creates a MtComponent instance from template contents. @@ -24,6 +25,7 @@ public static MtComponent FromTemplate(ManiaTemplateEngine engine, string templa var namespaces = new List(); var foundProperties = new Dictionary(); var maniaScripts = new List(); + var slots = new HashSet(); var componentTemplate = ""; var hasSlot = false; string? layer = null; @@ -57,6 +59,7 @@ public static MtComponent FromTemplate(ManiaTemplateEngine engine, string templa componentTemplate = node.InnerXml; layer = ParseDisplayLayer(node); hasSlot = NodeHasSlot(node); + slots = GetSlotNamesInTemplate(node); break; case "script": @@ -70,6 +73,7 @@ public static MtComponent FromTemplate(ManiaTemplateEngine engine, string templa { TemplateContent = componentTemplate, HasSlot = hasSlot, + Slots = slots, ImportedComponents = foundComponents, Properties = foundProperties, Namespaces = namespaces, @@ -222,6 +226,36 @@ private static bool NodeHasSlot(XmlNode node) .Any(NodeHasSlot); } + /// + /// Gets all slot names recursively. + /// + private static HashSet GetSlotNamesInTemplate(XmlNode node) + { + var slotNames = new HashSet(); + + if (node.Name == "slot") + { + slotNames.Add(node.Attributes?["name"]?.Value.ToLower() ?? "default"); + } + else if (node.HasChildNodes) + { + foreach (XmlNode childNode in node.ChildNodes) + { + foreach (var slotName in GetSlotNamesInTemplate(childNode)) + { + if (slotNames.Contains(slotName)) + { + throw new DuplicateSlotException($"""A slot with the name "{slotName}" already exists."""); + } + + slotNames.Add(slotName); + } + } + } + + return slotNames; + } + public string Id() { return "MtContext" + GetHashCode().ToString().Replace("-", "N"); diff --git a/src/ManiaTemplates/Components/MtComponentSlot.cs b/src/ManiaTemplates/Components/MtComponentSlot.cs index 2f545ea..0a38fa8 100644 --- a/src/ManiaTemplates/Components/MtComponentSlot.cs +++ b/src/ManiaTemplates/Components/MtComponentSlot.cs @@ -1,10 +1,12 @@ using ManiaTemplates.ControlElements; +using ManiaTemplates.Lib; namespace ManiaTemplates.Components; public class MtComponentSlot { public required int Scope { get; init; } - public required string RenderMethod { get; init; } + public required string RenderMethodT4 { get; init; } public required MtDataContext Context { get; init; } + public string Name { get; init; } = "default"; } \ No newline at end of file diff --git a/src/ManiaTemplates/Exceptions/DuplicateSlotException.cs b/src/ManiaTemplates/Exceptions/DuplicateSlotException.cs new file mode 100644 index 0000000..03cdc09 --- /dev/null +++ b/src/ManiaTemplates/Exceptions/DuplicateSlotException.cs @@ -0,0 +1,18 @@ +namespace ManiaTemplates.Exceptions; + +public class DuplicateSlotException : Exception +{ + public DuplicateSlotException() + { + } + + public DuplicateSlotException(string message) + : base(message) + { + } + + public DuplicateSlotException(string message, Exception inner) + : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ManiaTemplates/Lib/MtTransformer.cs b/src/ManiaTemplates/Lib/MtTransformer.cs index 2032826..1268d4b 100644 --- a/src/ManiaTemplates/Lib/MtTransformer.cs +++ b/src/ManiaTemplates/Lib/MtTransformer.cs @@ -1,4 +1,5 @@ using System.CodeDom; +using System.Diagnostics; using System.Dynamic; using System.Globalization; using System.Text; @@ -105,7 +106,7 @@ private string CreateInsertedManiaScriptsList() /// private string CreateBodyRenderMethod(string body, MtDataContext context, MtComponent rootComponent) { - var bodyRenderMethod = new StringBuilder("void RenderBody(Action __slotRenderer) {\n"); + var bodyRenderMethod = new StringBuilder("void RenderBody(Action __slotRenderer_default) {\n"); //Arguments for root data context var renderBodyArguments = @@ -149,8 +150,8 @@ private string CreateImportStatements() foreach (var propertyValue in _engine.GlobalVariables.Values) { var nameSpace = propertyValue.GetType().Namespace; - - if(nameSpace != "System") + + if (nameSpace != "System") { imports.AppendLine(_maniaTemplateLanguage.Context($@"import namespace=""{nameSpace}""")); } @@ -171,12 +172,12 @@ private string CreateTemplatePropertiesBlock(MtComponent mtComponent) { var properties = new StringBuilder(); - foreach (var (propertyName, propertyValue) in _engine.GlobalVariables) + foreach (var (propertyName, propertyValue) in _engine.GlobalVariables) { var type = propertyValue.GetType(); properties.AppendLine(_maniaTemplateLanguage - .FeatureBlock($"public {GetFormattedName(type)} ?{propertyName} {{ get; init; }}").ToString()); + .FeatureBlock($"public {GetFormattedName(type)} ?{propertyName} {{ get; init; }}").ToString()); } foreach (var property in mtComponent.Properties.Values) @@ -198,17 +199,17 @@ private string CreateRenderMethodsBlock() return new StringBuilder() .AppendJoin("\n", _renderMethods.Values) .AppendJoin("\n", _maniaScriptRenderMethods.Values) - .AppendJoin("\n", _slots.Select(slot => slot.RenderMethod)) + .AppendJoin("\n", _slots.Select(slot => slot.RenderMethodT4)) .ToString(); } /// /// Creates the slot-render method for a given data context. /// - private string CreateSlotRenderMethod(int scope, MtDataContext context, string? slotContent = null) + private string CreateSlotRenderMethod(int scope, MtDataContext context, string slotName, string? slotContent = null) { var variablesInherited = new List(); - var methodName = "Render_Slot_" + scope; + var methodName = GetSlotRenderMethodName(scope, slotName); var output = new StringBuilder(_maniaTemplateLanguage.FeatureBlockStart()) .AppendLine("void " + CreateMethodCall(methodName, $"{context} __data", "") + " {"); @@ -250,16 +251,16 @@ private static string CreateLocalVariablesFromContext(MtDataContext context, /// /// Process a ManiaTemplate node. /// - private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, MtDataContext context, int depth = 1) + private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, MtDataContext context) { Snippet snippet = new(); var nodeId = 1; - foreach (XmlNode child in node.ChildNodes) + foreach (XmlNode childNode in node.ChildNodes) { var subSnippet = new Snippet(); - var tag = child.Name; - var attributeList = GetXmlNodeAttributes(child); + var tag = childNode.Name; + var attributeList = GetXmlNodeAttributes(childNode); var currentContext = context; var forEachCondition = GetForeachConditionFromNodeAttributes(attributeList, context, nodeId); @@ -276,32 +277,21 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M { //Node is a component var component = _engine.GetComponent(availableMtComponents[tag].TemplateKey); - - string? slotContent = null; - if (component.HasSlot) - { - slotContent = ProcessNode( - child, - availableMtComponents, - currentContext, - depth - ); - } + var slotContents = + GetSlotContentsBySlotName(childNode, component, availableMtComponents, currentContext); var componentRenderMethodCall = ProcessComponentNode( context != currentContext, - child.GetHashCode(), + childNode.GetHashCode(), component, currentContext, attributeList, ProcessNode( XmlStringToNode(component.TemplateContent), availableMtComponents.Overload(component.ImportedComponents), - currentContext, - depth + 1 + currentContext ), - slotContent, - depth + slotContents ); subSnippet.AppendLine(_maniaTemplateLanguage.FeatureBlockStart()) @@ -314,26 +304,27 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M switch (tag) { case "#text": - subSnippet.AppendLine(child.InnerText); + subSnippet.AppendLine(childNode.InnerText); break; case "#comment": - subSnippet.AppendLine($""); + subSnippet.AppendLine($""); break; case "slot": + var slotName = GetNameFromNodeAttributes(attributeList); subSnippet.AppendLine(_maniaTemplateLanguage.FeatureBlockStart()) - .AppendLine(CreateMethodCall("__slotRenderer", "")) + .AppendLine(CreateMethodCall("__slotRenderer_" + slotName, "")) .AppendLine(_maniaTemplateLanguage.FeatureBlockEnd()); break; default: { - var hasChildren = child.HasChildNodes; + var hasChildren = childNode.HasChildNodes; subSnippet.AppendLine(CreateXmlOpeningTag(tag, attributeList, hasChildren)); if (hasChildren) { subSnippet.AppendLine(1, - ProcessNode(child, availableMtComponents, currentContext)); + ProcessNode(childNode, availableMtComponents, currentContext)); subSnippet.AppendLine(CreateXmlClosingTag(tag)); } @@ -360,6 +351,44 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M return snippet.ToString(); } + private Dictionary GetSlotContentsBySlotName(XmlNode componentNode, + MtComponent component, + MtComponentMap availableMtComponents, MtDataContext context) + { + var contentsByName = new Dictionary(); + + foreach (XmlNode childNode in componentNode.ChildNodes) + { + if (childNode.Name != "template") + { + continue; + } + + var slotName = childNode.Attributes?["slot"]?.Value.ToLower() ?? "default"; + + if (slotName == "default") + { + //Do not strip contents for default slot from node + continue; + } + + if (component.Slots.Contains(slotName)) + { + //Only add templates, if the component does have a fitting slot + contentsByName[slotName] = childNode.Clone(); + } + + componentNode.RemoveChild(childNode); + } + + contentsByName["default"] = componentNode; + + return contentsByName.ToDictionary( + kvp => kvp.Key, + kvp => ProcessNode(kvp.Value, availableMtComponents, context) + ); + } + /// /// Process a node that has been identified as an component. /// @@ -370,21 +399,24 @@ private string ProcessComponentNode( MtDataContext currentContext, MtComponentAttributes attributeList, string componentBody, - string? slotContent = null, - int depth = 0 + IReadOnlyDictionary slotContents ) { - MtComponentSlot? slot = null; - if (component.HasSlot) + foreach (var slotName in component.Slots) { - slot = new MtComponentSlot + var slotContent = ""; + if (slotContents.ContainsKey(slotName)) + { + slotContent = slotContents[slotName]; + } + + _slots.Add(new MtComponentSlot { Scope = scope, Context = currentContext, - RenderMethod = CreateSlotRenderMethod(scope, currentContext, slotContent) - }; - - _slots.Add(slot); + Name = slotName, + RenderMethodT4 = CreateSlotRenderMethod(scope, currentContext, slotName, slotContent) + }); } var renderMethodName = GetComponentRenderMethodName(component); @@ -418,38 +450,44 @@ private string ProcessComponentNode( renderComponentCall.Append(string.Join(", ", renderArguments)); - if (slot != null) + if (component.Slots.Count > 0) { - if (renderArguments.Count > 0) + var i = 0; + foreach (var slotName in component.Slots) { - renderComponentCall.Append(", "); - } + if (renderArguments.Count > 0 || i > 0) + { + renderComponentCall.Append(", "); + } - renderComponentCall.Append("__slotRenderer: ") - .Append("() => ") - .Append(GetSlotRenderMethodName(slot)); + renderComponentCall.Append($"__slotRenderer_{slotName}: ") + .Append("() => ") + .Append(GetSlotRenderMethodName(scope, slotName)); - if (newScopeCreated) - { - var dataVariableSuffix = "(__data)"; - if (currentContext.ParentContext is { Count: 0 }) + if (newScopeCreated) { - dataVariableSuffix = ""; - } + var dataVariableSuffix = "(__data)"; + if (currentContext.ParentContext is { Count: 0 }) + { + dataVariableSuffix = ""; + } - renderComponentCall.Append($"(new {currentContext}{dataVariableSuffix}{{"); + renderComponentCall.Append($"(new {currentContext}{dataVariableSuffix}{{"); + + var variables = new List(); + foreach (var variableName in currentContext.Keys) + { + variables.Add($"{variableName} = {variableName}"); + } - var variables = new List(); - foreach (var variableName in currentContext.Keys) + renderComponentCall.Append(string.Join(", ", variables)).Append("})"); + } + else { - variables.Add($"{variableName} = {variableName}"); + renderComponentCall.Append("(__data)"); } - renderComponentCall.Append(string.Join(", ", variables)).Append("})"); - } - else - { - renderComponentCall.Append("(__data)"); + i++; } } @@ -474,9 +512,9 @@ private string CreateComponentRenderMethod(MtComponent component, string renderM var arguments = new List(); //add slot render method - if (component.HasSlot) + foreach (var slotName in component.Slots) { - arguments.Add("Action __slotRenderer"); + arguments.Add($"Action __slotRenderer_{slotName}"); } //add method arguments with defaults @@ -673,6 +711,14 @@ private string CreateManiaScriptDirectivesBlock() return attributeList.ContainsKey("if") ? attributeList.Pull("if") : null; } + /// + /// Checks the attribute list for name and returns it, else "default". + /// + private string GetNameFromNodeAttributes(MtComponentAttributes attributeList) + { + return attributeList.ContainsKey("name") ? attributeList.Pull("name") : "default"; + } + /// /// Checks the attribute list for loop-condition and returns it, else null. /// @@ -883,7 +929,7 @@ private static MtDataContext GetContextFromComponent(MtComponent component, stri return context; } - + /// /// Returns C# code representation of the type. /// @@ -894,7 +940,7 @@ private static string GetFormattedName(Type type) { return "dynamic"; } - + using var codeProvider = new CSharpCodeProvider(); var typeReference = new CodeTypeReference(type); return codeProvider.GetTypeOutput(typeReference); @@ -928,9 +974,9 @@ private string GetComponentScriptsRenderMethodName(MtComponent component) /// /// Returns the name of the method that renders the slot contents. /// - private string GetSlotRenderMethodName(MtComponentSlot slot) + private static string GetSlotRenderMethodName(int scope, string name) { - return "Render_Slot_" + slot.Scope.GetHashCode(); + return $"Render_Slot_{scope.GetHashCode()}_{name}"; } /// diff --git a/tests/ManiaTemplates.Tests/Components/MtComponentTest.cs b/tests/ManiaTemplates.Tests/Components/MtComponentTest.cs index 0673c1e..a9067c2 100644 --- a/tests/ManiaTemplates.Tests/Components/MtComponentTest.cs +++ b/tests/ManiaTemplates.Tests/Components/MtComponentTest.cs @@ -20,6 +20,7 @@ public void Should_Read_Empty_Template() Properties = new(), Scripts = new(), HasSlot = false, + Slots = new HashSet(), ImportedComponents = new(), TemplateContent = "" }; @@ -74,6 +75,7 @@ public void Should_Populate_Template() new() { Content = "scriptText3", HasMainMethod = false, Once = false } }, HasSlot = true, + Slots = new HashSet(){"default"}, ImportedComponents = new() { { "name1", new() { TemplateKey = "name1.mt", Tag = "name1" } }, diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs b/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs index d6697c5..7b37b00 100644 --- a/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs +++ b/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs @@ -1,4 +1,6 @@ -namespace ManiaTemplates.Tests.IntegrationTests; +using ManiaTemplates.Exceptions; + +namespace ManiaTemplates.Tests.IntegrationTests; public class ManialinkEngineTest { @@ -22,9 +24,9 @@ public void Should_Convert_Templates_To_Result(string template, dynamic data, st [Fact] public void Should_Pass_Global_Variables() { - var componentTemplate = File.ReadAllText($"IntegrationTests/templates/global-variables.mt"); - var componentWithGlobalVariable = File.ReadAllText($"IntegrationTests/templates/component-using-gvar.mt"); - var expectedOutput = File.ReadAllText($"IntegrationTests/expected/global-variables.xml"); + var componentTemplate = File.ReadAllText("IntegrationTests/templates/global-variables.mt"); + var componentWithGlobalVariable = File.ReadAllText("IntegrationTests/templates/component-using-gvar.mt"); + var expectedOutput = File.ReadAllText("IntegrationTests/expected/global-variables.xml"); var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; _maniaTemplateEngine.AddTemplateFromString("ComponentGlobalVariable", componentWithGlobalVariable); @@ -49,4 +51,37 @@ private void AddGlobalVariable(string name, object value) { _maniaTemplateEngine.GlobalVariables.AddOrUpdate(name, value, (s, o) => value); } + + [Fact] + public void Should_Fill_Named_Slots() + { + var namedSlotsTemplate = File.ReadAllText("IntegrationTests/templates/named-slots.mt"); + var componentTemplate = File.ReadAllText("IntegrationTests/templates/component-multi-slot.mt"); + var expected = File.ReadAllText($"IntegrationTests/expected/named-slots.xml"); + var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; + + _maniaTemplateEngine.AddTemplateFromString("NamedSlots", namedSlotsTemplate); + _maniaTemplateEngine.AddTemplateFromString("SlotsComponent", componentTemplate); + + var result = _maniaTemplateEngine.RenderAsync("NamedSlots", new + { + testVariable = "UnitTest" + }, assemblies).Result; + + Assert.Equal(expected, result, ignoreLineEndingDifferences: true); + } + + [Fact] + public async void Should_Throw_Exception_For_Duplicate_Slot_In_Source_Template() + { + var namedSlotsTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/named-slots.mt"); + var componentTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/component-duplicate-slot.mt"); + var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; + + _maniaTemplateEngine.AddTemplateFromString("NamedSlots", namedSlotsTemplate); + _maniaTemplateEngine.AddTemplateFromString("SlotsComponent", componentTemplate); + + await Assert.ThrowsAsync(() => + _maniaTemplateEngine.RenderAsync("NamedSlots", new { testVariable = "UnitTest" }, assemblies)); + } } \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/expected/named-slots.xml b/tests/ManiaTemplates.Tests/IntegrationTests/expected/named-slots.xml new file mode 100644 index 0000000..7ee93f9 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/expected/named-slots.xml @@ -0,0 +1,8 @@ + + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-duplicate-slot.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-duplicate-slot.mt new file mode 100644 index 0000000..9d13eb8 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-duplicate-slot.mt @@ -0,0 +1,12 @@ + + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-multi-slot.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-multi-slot.mt new file mode 100644 index 0000000..a7dcb9b --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/component-multi-slot.mt @@ -0,0 +1,11 @@ + + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/named-slots.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/named-slots.mt new file mode 100644 index 0000000..01c2148 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/named-slots.mt @@ -0,0 +1,15 @@ + + + + + + + diff --git a/tests/ManiaTemplates.Tests/Lib/expected.tt b/tests/ManiaTemplates.Tests/Lib/expected.tt index e3098d1..f129e22 100644 --- a/tests/ManiaTemplates.Tests/Lib/expected.tt +++ b/tests/ManiaTemplates.Tests/Lib/expected.tt @@ -33,17 +33,17 @@ i = data.i; List __insertedOneTimeManiaScripts = new List(); List __maniaScriptRenderMethods = new List(); string DoNothing(){return "";} -void RenderBody(Action __slotRenderer) { +void RenderBody(Action __slotRenderer_default) { var __data = new CRoot { numbers = numbers,enabled = enabled }; var __outerIndex1 = 0; foreach (int i in numbers) { var __index = __outerIndex1; if (enabled) { -Render_Component_MtContext2(x: (20 * __index), __slotRenderer: () => Render_Slot_3(new CRoot_ForEachLoop1(__data){__index = __index, i = i})); +Render_Component_MtContext2(x: (20 * __index), __slotRenderer_default: () => Render_Slot_3_default(new CRoot_ForEachLoop1(__data){__index = __index, i = i})); } __outerIndex1++; } -Render_Component_MtContext2(__slotRenderer: () => Render_Slot_4(__data)); +Render_Component_MtContext2(__slotRenderer_default: () => Render_Slot_4_default(__data)); foreach(var maniaScriptRenderMethod in __maniaScriptRenderMethods){ maniaScriptRenderMethod(); } #> <#+ } -void Render_Slot_3(CRoot_ForEachLoop1 __data) { +void Render_Slot_3_default(CRoot_ForEachLoop1 __data) { var numbers = __data.numbers; var enabled = __data.enabled; var __index = __data.__index; @@ -104,7 +104,7 @@ Render_Component_MtContext5(text: $"{(i)}, {(j)} at index {(__index)}, {(__index __outerIndex7++; } } -void Render_Slot_8(CRoot __data) { +void Render_Slot_8_default(CRoot __data) { var numbers = __data.numbers; var enabled = __data.enabled; #> @@ -115,9 +115,9 @@ Render_Component_MtContext6(arg3: (new test())); <#+ } -void Render_Slot_4(CRoot __data) { +void Render_Slot_4_default(CRoot __data) { var numbers = __data.numbers; var enabled = __data.enabled; -Render_Component_MtContext2(__slotRenderer: () => Render_Slot_8(__data)); +Render_Component_MtContext2(__slotRenderer_default: () => Render_Slot_8_default(__data)); } #> \ No newline at end of file