diff --git a/src/ManiaTemplates/Components/MtComponent.cs b/src/ManiaTemplates/Components/MtComponent.cs index 64512a4..7c05cc6 100644 --- a/src/ManiaTemplates/Components/MtComponent.cs +++ b/src/ManiaTemplates/Components/MtComponent.cs @@ -258,6 +258,6 @@ private static HashSet GetSlotNamesInTemplate(XmlNode node) public string Id() { - return "MtContext" + GetHashCode().ToString().Replace("-", "N"); + return GetHashCode().ToString().Replace("-", "N"); } } \ No newline at end of file diff --git a/src/ManiaTemplates/Exceptions/CurlyBraceCountMismatchException.cs b/src/ManiaTemplates/Exceptions/CurlyBraceCountMismatchException.cs new file mode 100644 index 0000000..74998af --- /dev/null +++ b/src/ManiaTemplates/Exceptions/CurlyBraceCountMismatchException.cs @@ -0,0 +1,16 @@ +namespace ManiaTemplates.Exceptions; + +public class CurlyBraceCountMismatchException : Exception +{ + public CurlyBraceCountMismatchException() + { + } + + public CurlyBraceCountMismatchException(string message) : base(message) + { + } + + public CurlyBraceCountMismatchException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ManiaTemplates/Exceptions/EmptyNodeAttributeException.cs b/src/ManiaTemplates/Exceptions/EmptyNodeAttributeException.cs new file mode 100644 index 0000000..e5fccfa --- /dev/null +++ b/src/ManiaTemplates/Exceptions/EmptyNodeAttributeException.cs @@ -0,0 +1,16 @@ +namespace ManiaTemplates.Exceptions; + +public class EmptyNodeAttributeException : Exception +{ + public EmptyNodeAttributeException() + { + } + + public EmptyNodeAttributeException(string message) : base(message) + { + } + + public EmptyNodeAttributeException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/ManiaTemplates/Exceptions/InterpolationRecursionException.cs b/src/ManiaTemplates/Exceptions/InterpolationRecursionException.cs new file mode 100644 index 0000000..9ef3bb0 --- /dev/null +++ b/src/ManiaTemplates/Exceptions/InterpolationRecursionException.cs @@ -0,0 +1,16 @@ +namespace ManiaTemplates.Exceptions; + +public class InterpolationRecursionException : Exception +{ + public InterpolationRecursionException() + { + } + + public InterpolationRecursionException(string message) : base(message) + { + } + + public InterpolationRecursionException(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 26e326d..477b18b 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; @@ -86,7 +87,7 @@ public string BuildManialink(MtComponent rootComponent, string className, int ve /// private string CreateDoNothingMethod() { - return _maniaTemplateLanguage.FeatureBlock(@"string DoNothing(){return """";}").ToString(); + return _maniaTemplateLanguage.FeatureBlock("""string DoNothing(){ return ""; }""").ToString(); } /// @@ -119,7 +120,7 @@ private string CreateBodyRenderMethod(string body, MtDataContext context, MtComp bodyRenderMethod.AppendLine($"var __data = {renderBodyArguments};"); //Root mania script block - string rootScriptBlock = ""; + var rootScriptBlock = ""; if (rootComponent.Scripts.Count > 0) { rootScriptBlock = CreateManiaScriptBlock(rootComponent); @@ -209,11 +210,11 @@ private string CreateRenderMethodsBlock() /// /// Creates the slot-render method for a given data context. /// - private string CreateSlotRenderMethod(MtComponent component, int scope, MtDataContext context, string slotName, + private string CreateSlotRenderMethod(string nodeName, MtComponent component, int scope, MtDataContext context, string slotName, string? slotContent = null, MtComponent? parentComponent = null) { var variablesInherited = new List(); - var methodName = GetSlotRenderMethodName(scope, slotName); + var methodName = GetSlotRenderMethodName(nodeName, scope, slotName, context); var methodArguments = new List { @@ -270,7 +271,7 @@ private static string CreateLocalVariablesFromContext(MtDataContext context, /// Process a ManiaTemplate node. /// private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, MtDataContext context, - MtComponent? parentComponent = null) + MtComponent? parentComponent = null, MtComponentAttributes? parentAttributes = null) { Snippet snippet = new(); @@ -296,10 +297,9 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M { //Node is a component var component = _engine.GetComponent(availableMtComponents[tag].TemplateKey); - var slotContents = - GetSlotContentsBySlotName(childNode, component, availableMtComponents, currentContext); - + var slotContents = GetSlotContentsBySlotName(childNode, component, availableMtComponents, currentContext); var componentRenderMethodCall = ProcessComponentNode( + childNode.Name, context != currentContext, childNode.GetHashCode(), component, @@ -309,7 +309,8 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M XmlStringToNode(component.TemplateContent), availableMtComponents.Overload(component.ImportedComponents), currentContext, - component + component, + attributeList ), slotContents, parentComponent @@ -340,12 +341,22 @@ private string ProcessNode(XmlNode node, MtComponentMap availableMtComponents, M default: { var hasChildren = childNode.HasChildNodes; + if (node.ChildNodes.Count == 1 && parentAttributes != null && parentComponent != null) + { + var attributesThatArentProperties = parentAttributes.Where(attribute => + !parentComponent.Properties.ContainsKey(attribute.Key)); + + foreach (var attribute in attributesThatArentProperties) + { + attributeList.TryAdd(attribute.Key, attribute.Value); + } + } + subSnippet.AppendLine(CreateXmlOpeningTag(tag, attributeList, hasChildren)); if (hasChildren) { - subSnippet.AppendLine(1, - ProcessNode(childNode, availableMtComponents, currentContext)); + subSnippet.AppendLine(1, ProcessNode(childNode, availableMtComponents, currentContext)); subSnippet.AppendLine(CreateXmlClosingTag(tag)); } @@ -384,8 +395,15 @@ private Dictionary GetSlotContentsBySlotName(XmlNode componentNo continue; } + //Get the name from the slot attribute, or default if not found. var slotName = childNode.Attributes?["slot"]?.Value.ToLower() ?? "default"; + if (slotName.Trim().Length == 0) + { + throw new EmptyNodeAttributeException( + $"There's a template tag with empty slot name in <{componentNode.Name}>."); + } + if (slotName == "default") { //Do not strip contents for default slot from node @@ -413,6 +431,7 @@ private Dictionary GetSlotContentsBySlotName(XmlNode componentNo /// Process a node that has been identified as an component. /// private string ProcessComponentNode( + string nodeName, bool newScopeCreated, int scope, MtComponent component, @@ -437,6 +456,7 @@ private string ProcessComponentNode( Context = currentContext, Name = slotName, RenderMethodT4 = CreateSlotRenderMethod( + nodeName, component, scope, currentContext, @@ -447,12 +467,12 @@ private string ProcessComponentNode( }); } - var renderMethodName = GetComponentRenderMethodName(component); + var renderMethodName = GetComponentRenderMethodName(nodeName, component, currentContext); if (!_renderMethods.ContainsKey(renderMethodName)) { _renderMethods.Add( renderMethodName, - CreateComponentRenderMethod(component, renderMethodName, componentBody) + CreateComponentRenderMethod(component, renderMethodName, componentBody, currentContext) ); } @@ -461,23 +481,19 @@ private string ProcessComponentNode( //Create available arguments var renderArguments = new List { "" }; + + //Attach variables from current context, that are not mapped to properties of component + renderArguments.AddRange(from variableName in currentContext.Keys where !attributeList.ContainsKey(variableName) select $"{variableName}: {variableName}"); //Attach attributes to render method call foreach (var (attributeName, attributeValue) in attributeList) { - if (component.Properties.TryGetValue(attributeName, out var value)) - { - if (IsStringType(value)) - { - renderArguments.Add( - $"{attributeName}: {WrapIfString(value, ReplaceCurlyBraces(attributeValue, s => $@"{{({s})}}"))}"); - } - else - { - renderArguments.Add( - $"{attributeName}: {ReplaceCurlyBraces(attributeValue, s => $"({s})")}"); - } - } + if (!component.Properties.TryGetValue(attributeName, out var value)) continue; + + renderArguments.Add( + IsStringType(value) + ? $"{attributeName}: {WrapStringInQuotes(ReplaceCurlyBraces(attributeValue, s => $"{{({s})}}"))}" + : $"{attributeName}: {ReplaceCurlyBraces(attributeValue, s => $"({s})")}"); } renderComponentCall.Append(string.Join(", ", renderArguments)); @@ -495,7 +511,7 @@ private string ProcessComponentNode( renderComponentCall.Append($"__slotRenderer_{slotName}: ") .Append("() => ") - .Append(GetSlotRenderMethodName(scope, slotName)); + .Append(GetSlotRenderMethodName(nodeName, scope, slotName, currentContext)); if (newScopeCreated) { @@ -527,7 +543,7 @@ private string ProcessComponentNode( renderComponentCall.Append( $", __slotRenderer_{parentSlotName}: __slotRenderer_{parentSlotName}"); } - + foreach (var propertyName in parentComponent.Properties.Keys) { renderComponentCall.Append($",{propertyName}: {propertyName}"); @@ -541,7 +557,7 @@ private string ProcessComponentNode( $", __slotRenderer_{parentSlotName}: () => DoNothing()"); } } - + renderComponentCall.Append(')'); i++; @@ -554,11 +570,11 @@ private string ProcessComponentNode( return renderComponentCall.ToString(); } - + /// /// Creates the method which renders the contents of a component. /// - private string CreateComponentRenderMethod(MtComponent component, string renderMethodName, string componentBody) + private string CreateComponentRenderMethod(MtComponent component, string renderMethodName, string componentBody, MtDataContext currentContext) { var renderMethod = new StringBuilder(_maniaTemplateLanguage.FeatureBlockStart()) .Append("void ") @@ -573,6 +589,15 @@ private string CreateComponentRenderMethod(MtComponent component, string renderM //add slot render methods AppendSlotRenderArgumentsToList(arguments, component); + + //Add fallthrough properties + foreach (var (variableName, variableType) in currentContext) + { + if (!component.Properties.ContainsKey(variableName)) + { + arguments.Add($"{variableType} {variableName}"); + } + } //add component properties as arguments with defaults AppendComponentPropertiesToMethodArgumentsList(component, arguments); @@ -775,7 +800,7 @@ private string CreateManiaScriptDirectivesBlock() } /// - /// Checks the attribute list for if-condition and returns it, else null. + /// Checks the attribute list for if-condition and if found removes & returns it, else null. /// private string? GetIfConditionFromNodeAttributes(MtComponentAttributes attributeList) { @@ -783,7 +808,7 @@ private string CreateManiaScriptDirectivesBlock() } /// - /// Checks the attribute list for name and returns it, else "default". + /// Checks the attribute list for name and if found removes & returns it, else "default". /// private string GetNameFromNodeAttributes(MtComponentAttributes attributeList) { @@ -922,7 +947,7 @@ private static string CreateContextClassProperties(MtDataContext context) /// /// Parses the attributes of a XmlNode to an MtComponentAttributes-instance. /// - private static MtComponentAttributes GetXmlNodeAttributes(XmlNode node) + public static MtComponentAttributes GetXmlNodeAttributes(XmlNode node) { var attributeList = new MtComponentAttributes(); if (node.Attributes == null) return attributeList; @@ -940,6 +965,8 @@ private static MtComponentAttributes GetXmlNodeAttributes(XmlNode node) /// private string BuildManiaScripts(MtComponent rootComponent) { + //TODO: check if method can be removed + var maniaScripts = rootComponent.Scripts.ToDictionary(script => script.ContentHash()); foreach (var (key, value) in _maniaScripts) { @@ -1005,7 +1032,7 @@ private static MtDataContext GetContextFromComponent(MtComponent component, stri /// Returns C# code representation of the type. /// /// The type. - private static string GetFormattedName(Type type) + public static string GetFormattedName(Type type) { if (type.IsSubclassOf(typeof(DynamicObject))) { @@ -1029,9 +1056,9 @@ private static string CreateMethodCall(string methodName, string methodArguments /// /// Returns the method name that renders the given component. /// - private string GetComponentRenderMethodName(MtComponent component) + private string GetComponentRenderMethodName(string nodeName, MtComponent component, MtDataContext dataContext) { - return $"Render_Component_{component.Id()}"; + return $"Component_{nodeName}_{dataContext}_{component.Id()}"; } /// @@ -1039,15 +1066,15 @@ private string GetComponentRenderMethodName(MtComponent component) /// private string GetComponentScriptsRenderMethodName(MtComponent component) { - return $"Render_ComponentScript_{component.Id()}"; + return $"ComponentScript_{component.Id()}"; } /// /// Returns the name of the method that renders the slot contents. /// - private static string GetSlotRenderMethodName(int scope, string name) + private static string GetSlotRenderMethodName(string nodeName, int scope, string name, MtDataContext context) { - return $"Render_Slot_{scope.GetHashCode()}_{name}"; + return $"Slot_{name}_of_{nodeName}_in_{context}_{scope.GetHashCode()}"; } /// @@ -1081,7 +1108,7 @@ public static string WrapStringInQuotes(string str) /// name = Shown in in-game debugger. /// version = Version for the markup language of Trackmania. /// - private static string ManiaLinkStart(string name, int version = 3, string? displayLayer = null) + public static string ManiaLinkStart(string name, int version = 3, string? displayLayer = null) { var layer = ""; if (displayLayer != null) @@ -1103,7 +1130,7 @@ private static string ManiaLinkEnd() /// /// Creates a xml opening tag for the given string and attribute list. /// - private string CreateXmlOpeningTag(string tag, MtComponentAttributes attributeList, bool hasChildren) + public string CreateXmlOpeningTag(string tag, MtComponentAttributes attributeList, bool hasChildren) { var output = $"<{tag}"; @@ -1132,7 +1159,7 @@ private static string CreateXmlClosingTag(string tag) /// /// Converts any valid XML-string into an XmlNode-element. /// - private static XmlNode XmlStringToNode(string content) + public static XmlNode XmlStringToNode(string content) { var doc = new XmlDocument(); doc.LoadXml($"{content}"); @@ -1145,6 +1172,9 @@ private static XmlNode XmlStringToNode(string content) /// public static string ReplaceCurlyBraces(string value, Func curlyContentWrapper) { + CheckForCurlyBraceCountMismatch(value); + CheckInterpolationRecursion(value); + var matches = TemplateInterpolationRegex.Match(value); var output = value; @@ -1161,10 +1191,59 @@ public static string ReplaceCurlyBraces(string value, Func curly return output; } + /// + /// Checks whether double interpolation exists ({{ {{ a }} {{ b }} }}) and throws exception if so. + /// + public static void CheckInterpolationRecursion(string value) + { + var openCurlyBraces = 0; + foreach (var character in value.ToCharArray()) + { + if (character == '{') + { + openCurlyBraces++; + + if (openCurlyBraces >= 4) + { + throw new InterpolationRecursionException( + $"Double interpolation found in: {value}. You must not use double curly braces inside other double curly braces."); + } + } + else if (character == '}') + { + openCurlyBraces--; + } + } + } + + /// + /// Checks whether double interpolation exists ({{ {{ a }} {{ b }} }}) and throws exception if so. + /// + public static void CheckForCurlyBraceCountMismatch(string value) + { + var openCurlyBraces = 0; + foreach (var character in value.ToCharArray()) + { + if (character == '{') + { + openCurlyBraces++; + } + else if (character == '}') + { + openCurlyBraces--; + } + } + + if (openCurlyBraces != 0) + { + throw new CurlyBraceCountMismatchException($"Found curly brace count mismatch in: {value}."); + } + } + /// /// Joins consecutive feature blocks to reduce generated code. /// - private static string JoinFeatureBlocks(string manialink) + public static string JoinFeatureBlocks(string manialink) { var match = TemplateFeatureControlRegex.Match(manialink); var output = new Snippet(); diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs b/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs index 6ffa376..de90772 100644 --- a/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs +++ b/tests/ManiaTemplates.Tests/IntegrationTests/ManialinkEngineTest.cs @@ -88,17 +88,20 @@ await Assert.ThrowsAsync(() => [Fact] public async void Should_Render_Component_Without_Content_For_Slot() { - var slotRecursionOuterTwoTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-outer-two.mt"); - var slotRecursionOuterTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-outer.mt"); - var slotRecursionInnerTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-inner.mt"); + var slotRecursionOuterTwoTemplate = + await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-outer-two.mt"); + var slotRecursionOuterTemplate = + await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-outer.mt"); + var slotRecursionInnerTemplate = + await File.ReadAllTextAsync("IntegrationTests/templates/slot-recursion-inner.mt"); var expected = await File.ReadAllTextAsync("IntegrationTests/expected/single-slot-unfilled.xml"); var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; - + _maniaTemplateEngine.AddTemplateFromString("SlotRecursionOuterTwo", slotRecursionOuterTwoTemplate); _maniaTemplateEngine.AddTemplateFromString("SlotRecursionOuter", slotRecursionOuterTemplate); _maniaTemplateEngine.AddTemplateFromString("SlotRecursionInner", slotRecursionInnerTemplate); - - var template = _maniaTemplateEngine.RenderAsync("SlotRecursionInner", new{}, assemblies).Result; + + var template = _maniaTemplateEngine.RenderAsync("SlotRecursionInner", new { }, assemblies).Result; Assert.Equal(expected, template, ignoreLineEndingDifferences: true); } @@ -110,15 +113,49 @@ public async void Should_Pass_Properties_To_Components_And_Slots() var testComponentTemplate = await File.ReadAllTextAsync("IntegrationTests/templates/component.mt"); var expected = await File.ReadAllTextAsync("IntegrationTests/expected/property-test.xml"); var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; - + _maniaTemplateEngine.AddTemplateFromString("PropertyTest", propertyTestTemplate); _maniaTemplateEngine.AddTemplateFromString("Wrapper", testWrapperTemplate); _maniaTemplateEngine.AddTemplateFromString("TestComponent", testComponentTemplate); - + var template = _maniaTemplateEngine.RenderAsync("PropertyTest", new { testVariable = "integration" }, assemblies).Result; Assert.Equal(expected, template, ignoreLineEndingDifferences: true); } + + [Fact] + public async void Should_Append_Parent_Attributes_To_Single_Component_Child() + { + var fallthroughComponent = await File.ReadAllTextAsync("IntegrationTests/templates/fallthrough-component.mt"); + var fallthroughWrapper = await File.ReadAllTextAsync("IntegrationTests/templates/fallthrough-wrapper.mt"); + var expected = await File.ReadAllTextAsync("IntegrationTests/expected/fallthrough.xml"); + var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; + + _maniaTemplateEngine.AddTemplateFromString("FallthroughComponent", fallthroughComponent); + _maniaTemplateEngine.AddTemplateFromString("FallthroughWrapper", fallthroughWrapper); + + var template = _maniaTemplateEngine.RenderAsync("FallthroughWrapper", new + { + testString = "unit", + index = -1 + }, assemblies).Result; + Assert.Equal(expected, template, ignoreLineEndingDifferences: true); + } + + [Fact] + public async void Should_Not_Append_Parent_Attributes_To_Multiple_Component_Children() + { + var fallthroughComponent = await File.ReadAllTextAsync("IntegrationTests/templates/fallthrough-component-multi-child.mt"); + var fallthroughWrapper = await File.ReadAllTextAsync("IntegrationTests/templates/fallthrough-wrapper.mt"); + var expected = await File.ReadAllTextAsync("IntegrationTests/expected/fallthrough-multi-child.xml"); + var assemblies = new[] { typeof(ManiaTemplateEngine).Assembly, typeof(ComplexDataType).Assembly }; + + _maniaTemplateEngine.AddTemplateFromString("FallthroughComponent", fallthroughComponent); + _maniaTemplateEngine.AddTemplateFromString("FallthroughWrapper", fallthroughWrapper); + + var template = _maniaTemplateEngine.RenderAsync("FallthroughWrapper", new { }, assemblies).Result; + Assert.Equal(expected, template, ignoreLineEndingDifferences: true); + } } \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough-multi-child.xml b/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough-multi-child.xml new file mode 100644 index 0000000..5d55ded --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough-multi-child.xml @@ -0,0 +1,8 @@ + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough.xml b/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough.xml new file mode 100644 index 0000000..38a68f7 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/expected/fallthrough.xml @@ -0,0 +1,5 @@ + + diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component-multi-child.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component-multi-child.mt new file mode 100644 index 0000000..e823c44 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component-multi-child.mt @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component.mt new file mode 100644 index 0000000..ff4c8e4 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-component.mt @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-wrapper.mt b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-wrapper.mt new file mode 100644 index 0000000..0bb2ad9 --- /dev/null +++ b/tests/ManiaTemplates.Tests/IntegrationTests/templates/fallthrough-wrapper.mt @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/Lib/MtTransformerTest.cs b/tests/ManiaTemplates.Tests/Lib/MtTransformerTest.cs index 445376e..ea61d45 100644 --- a/tests/ManiaTemplates.Tests/Lib/MtTransformerTest.cs +++ b/tests/ManiaTemplates.Tests/Lib/MtTransformerTest.cs @@ -1,6 +1,8 @@ using System.Reflection; using System.Text.RegularExpressions; +using System.Xml; using ManiaTemplates.Components; +using ManiaTemplates.Exceptions; using ManiaTemplates.Languages; using ManiaTemplates.Lib; using Xunit.Abstractions; @@ -13,6 +15,7 @@ public class MtTransformerTest private readonly MtTransformer _transformer; private readonly ManiaTemplateEngine _maniaTemplateEngine = new(); private readonly Regex _hashCodePattern = new("[0-9]{6,10}"); + private readonly MtComponent _testComponent = new() { Namespaces = new() { "namespace" }, @@ -131,7 +134,25 @@ public void Should_Import_Components_Recursively() } [Fact] - public void Should_Replace_Curly_Braces_Correctly() + public void Should_Throw_Interpolation_Recursion_Exception() + { + Assert.Throws(() => + MtTransformer.CheckInterpolationRecursion("{{ {{ a }} {{ b }} }}")); + Assert.Throws(() => + MtTransformer.CheckInterpolationRecursion("{{ {{ b }} }}")); + } + + [Fact] + public void Should_Throw_Curly_Brace_Count_Mismatch_Exception() + { + Assert.Throws(() => MtTransformer.CheckForCurlyBraceCountMismatch("{{ { }}")); + Assert.Throws(() => MtTransformer.CheckForCurlyBraceCountMismatch("{{ } }}")); + Assert.Throws(() => MtTransformer.CheckForCurlyBraceCountMismatch("{")); + Assert.Throws(() => MtTransformer.CheckForCurlyBraceCountMismatch("}}")); + } + + [Fact] + public void Should_Replace_Curly_Braces() { Assert.Equal("abcd", MtTransformer.ReplaceCurlyBraces("{{a}}{{ b }}{{c }}{{ d}}", s => s)); Assert.Equal("x y z", MtTransformer.ReplaceCurlyBraces("{{x}} {{ y }} {{z }}", s => s)); @@ -144,5 +165,69 @@ public void Should_Replace_Curly_Braces_Correctly() public void Should_Wrap_Strings_In_Quotes() { Assert.Equal(@"$""unit test""", MtTransformer.WrapStringInQuotes("unit test")); + Assert.Equal(@"$""""", MtTransformer.WrapStringInQuotes("")); + } + + [Fact] + public void Should_Join_Feature_Blocks() + { + Assert.Equal("<#+\n unittest \n#>", + MtTransformer.JoinFeatureBlocks("<#+#><#+\n #> <#+ unittest \n#><#+ \n\n\n#>")); + } + + [Fact] + public void Should_Convert_String_To_Xml_Node() + { + var node = MtTransformer.XmlStringToNode("test"); + Assert.IsAssignableFrom(node); + Assert.Equal("test", node.InnerText); + Assert.Equal("test", node.InnerXml); + Assert.Equal("test", node.OuterXml); + } + + [Fact] + public void Should_Create_Xml_Opening_Tag() + { + var attributeList = new MtComponentAttributes(); + Assert.Equal("", _transformer.CreateXmlOpeningTag("test", attributeList, false)); + Assert.Equal("", _transformer.CreateXmlOpeningTag("test", attributeList, true)); + + attributeList["prop"] = "value"; + Assert.Equal("""""", _transformer.CreateXmlOpeningTag("test", attributeList, false)); + Assert.Equal("""""", _transformer.CreateXmlOpeningTag("test", attributeList, true)); + } + + [Fact] + public void Should_Create_ManiaLink_Opening_Tag() + { + Assert.Equal("""""", + MtTransformer.ManiaLinkStart("Test", 99)); + Assert.Equal("""""", + MtTransformer.ManiaLinkStart("Test", 99, "SomeLayer")); + } + + [Fact] + public void Should_Convert_Xml_Node_Arguments_To_MtComponentAttributes_Instance() + { + var node = MtTransformer.XmlStringToNode("""testContent"""); + if (node.FirstChild == null) return; + + var attributes = MtTransformer.GetXmlNodeAttributes(node.FirstChild); + Assert.Equal(2, attributes.Count); + Assert.Equal("test1", attributes["arg1"]); + Assert.Equal("test2", attributes["arg2"]); + } + + [Fact] + public void Should_Detect_Correct_Type_String_For_CSharp_Scripting() + { + Assert.Equal("int", MtTransformer.GetFormattedName(0.GetType())); + Assert.Equal("double", MtTransformer.GetFormattedName(0.0.GetType())); + Assert.Equal("string", MtTransformer.GetFormattedName("test".GetType())); + Assert.Equal("System.Collections.Generic.List", + MtTransformer.GetFormattedName(new List().GetType())); + Assert.Equal("System.Collections.Generic.HashSet", + MtTransformer.GetFormattedName(new HashSet().GetType())); + Assert.Equal("dynamic", MtTransformer.GetFormattedName(new TestDynamicObject().GetType())); } } \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/Lib/TestDynamicObject.cs b/tests/ManiaTemplates.Tests/Lib/TestDynamicObject.cs new file mode 100644 index 0000000..471cbe1 --- /dev/null +++ b/tests/ManiaTemplates.Tests/Lib/TestDynamicObject.cs @@ -0,0 +1,7 @@ +using System.Dynamic; + +namespace ManiaTemplates.Tests.Lib; + +public class TestDynamicObject : DynamicObject +{ +} \ No newline at end of file diff --git a/tests/ManiaTemplates.Tests/Lib/expected.tt b/tests/ManiaTemplates.Tests/Lib/expected.tt index f49dc93..d43a854 100644 --- a/tests/ManiaTemplates.Tests/Lib/expected.tt +++ b/tests/ManiaTemplates.Tests/Lib/expected.tt @@ -32,18 +32,18 @@ i = data.i; } List __insertedOneTimeManiaScripts = new List(); List __maniaScriptRenderMethods = new List(); -string DoNothing(){return "";} +string DoNothing(){ return ""; } void RenderBody() { var __data = new CRoot { numbers = numbers,enabled = enabled }; var __outerIndex1 = 0; foreach (int i in numbers) { var __index = __outerIndex1; if (enabled) { -Render_Component_MtContext2(__data: __data, x: (20 * __index), __slotRenderer_default: () => Render_Slot_3_default(__data: new CRoot_ForEachLoop1(__data){__index = __index, i = i}, __slotRenderer_default: () => DoNothing())); +Component_Frame_CRoot_ForEachLoop1_2(__data: __data, __index: __index, i: i, x: (20 * __index), __slotRenderer_default: () => Slot_default_of_Frame_in_CRoot_ForEachLoop1_3(__data: new CRoot_ForEachLoop1(__data){__index = __index, i = i}, __slotRenderer_default: () => DoNothing())); } __outerIndex1++; } -Render_Component_MtContext2(__data: __data, __slotRenderer_default: () => Render_Slot_4_default(__data: __data, __slotRenderer_default: () => DoNothing())); +Component_Frame_CRoot_2(__data: __data, numbers: numbers, enabled: enabled, __slotRenderer_default: () => Slot_default_of_Frame_in_CRoot_4(__data: __data, __slotRenderer_default: () => DoNothing())); foreach(var maniaScriptRenderMethod in __maniaScriptRenderMethods){ maniaScriptRenderMethod(); } #> <#+ } -void Render_Component_MtContext5(CRoot __data, double x = 0.0, double y = 0.0, double w = 0.0, double h = 0.0, string halign = $"left", string valign = $"center", double opacity = 1.0, int zIndex = 0, int events = 0, string action = $"", string url = $"", string manialink = $"", string style = $"", string textfont = $"GameFont", double textsize = 1.0, string textcolor = $"", string focusareacolor1 = $"", string focusareacolor2 = $"", string text = $"", string textprefix = $"", int bold = 0, int autonewline = 0, int maxline = 0, int translate = 0, string textid = $"", string id = $"") { +void Component_Label_CRoot_ForEachLoop1_ForEachLoop1_5(CRoot __data, int __index, int j, double x = 0.0, double y = 0.0, double w = 0.0, double h = 0.0, string halign = $"left", string valign = $"center", double opacity = 1.0, int zIndex = 0, int events = 0, string action = $"", string url = $"", string manialink = $"", string style = $"", string textfont = $"GameFont", double textsize = 1.0, string textcolor = $"", string focusareacolor1 = $"", string focusareacolor2 = $"", string text = $"", string textprefix = $"", int bold = 0, int autonewline = 0, int maxline = 0, int translate = 0, string textid = $"", string id = $"") { #>