From 894a15ec684bc0c269b1334cabc39a271cc0b031 Mon Sep 17 00:00:00 2001 From: Roman Kondrat'ev <62770895+RomeCore@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:43:11 +0500 Subject: [PATCH 1/2] feat: Add null-safe navigation, coalescing, has-property and length operators --- .../DataAccessors/TemplateArrayAccessor.cs | 6 +- .../TemplateDictionaryAccessor.cs | 8 +- .../DataAccessors/TemplateObjectAccessor.cs | 6 +- .../DataAccessors/TemplateStringAccessor.cs | 6 +- .../TemplateIndexExpressionNode.cs | 12 +- .../TemplateMethodCallExpressionNode.cs | 16 +- .../TemplatePropertyExpressionNode.cs | 53 ++++- .../TemplateUnaryOperatorExpressionNode.cs | 1 + src/LLTSharp/LLTParser.cs | 64 ++++-- src/LLTSharp/LLTSharp.csproj | 2 +- src/LLTSharp/Metadata/MetadataExtensions.cs | 3 +- src/LLTSharp/OperatorTypes.cs | 6 +- src/LLTSharp/TemplateContextAccessor.cs | 10 +- src/LLTSharp/TemplateDataAccessor.cs | 27 ++- src/LLTSharp/TemplateFunction.cs | 32 +-- src/LLTSharp/TemplateFunctionSet.cs | 26 ++- src/LLTSharp/TemplateLibrary.cs | 208 ++++++++++++++++-- src/LLTSharp/TemplateLibrary.retrieval.cs | 2 +- 18 files changed, 380 insertions(+), 108 deletions(-) diff --git a/src/LLTSharp/DataAccessors/TemplateArrayAccessor.cs b/src/LLTSharp/DataAccessors/TemplateArrayAccessor.cs index 95d48f9..20bb4db 100644 --- a/src/LLTSharp/DataAccessors/TemplateArrayAccessor.cs +++ b/src/LLTSharp/DataAccessors/TemplateArrayAccessor.cs @@ -27,15 +27,15 @@ public TemplateArrayAccessor(IEnumerable array) _array = array?.ToArray() ?? throw new ArgumentNullException(nameof(array)); } - public override TemplateDataAccessor Index(TemplateDataAccessor index) + public override TemplateDataAccessor Index(TemplateDataAccessor index, bool safe) { try { var i = Convert.ToInt32(index.GetValue()); if (i >= 0 && i < _array.Length) - { return _array[i]; - } + if (safe) + return TemplateNullAccessor.Instance; throw new TemplateRuntimeException($"Index out of range: {index}, Length: {_array.Length}", dataAccessor: this); } catch (TemplateRuntimeException) diff --git a/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs b/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs index 418bd41..9192286 100644 --- a/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs +++ b/src/LLTSharp/DataAccessors/TemplateDictionaryAccessor.cs @@ -41,9 +41,9 @@ public TemplateDictionaryAccessor(IDictionary dict Dictionary = new ReadOnlyDictionary(_dictionary); } - public override TemplateDataAccessor Index(TemplateDataAccessor index) + public override TemplateDataAccessor Index(TemplateDataAccessor index, bool safe) { - return Property(index.ToString()); + return Property(index.ToString(), safe); } public override bool HasProperty(string name) @@ -51,11 +51,11 @@ public override bool HasProperty(string name) return _dictionary.ContainsKey(name); } - public override TemplateDataAccessor Property(string key) + public override TemplateDataAccessor Property(string key, bool safe) { if (_dictionary.TryGetValue(key, out var accessor)) return accessor; - return base.Property(key); + return base.Property(key, safe); } public override bool AsBoolean() diff --git a/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs b/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs index 94f0c8f..b6db479 100644 --- a/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs +++ b/src/LLTSharp/DataAccessors/TemplateObjectAccessor.cs @@ -59,7 +59,7 @@ public override bool HasProperty(string name) return _propertyAccessors.ContainsKey(name); } - public override TemplateDataAccessor Property(string key) + public override TemplateDataAccessor Property(string key, bool safe) { if (_propertyAccessors.TryGetValue(key, out var accessor)) { @@ -70,6 +70,8 @@ public override TemplateDataAccessor Property(string key) } catch (Exception ex) { + if (safe) + return TemplateNullAccessor.Instance; throw new TemplateRuntimeException( $"Error accessing property '{key}' on {_target.GetType().Name}", ex, @@ -77,7 +79,7 @@ public override TemplateDataAccessor Property(string key) } } - return base.Property(key); + return base.Property(key, safe); } public override bool AsBoolean() => true; diff --git a/src/LLTSharp/DataAccessors/TemplateStringAccessor.cs b/src/LLTSharp/DataAccessors/TemplateStringAccessor.cs index fc6ed0b..9571f18 100644 --- a/src/LLTSharp/DataAccessors/TemplateStringAccessor.cs +++ b/src/LLTSharp/DataAccessors/TemplateStringAccessor.cs @@ -31,15 +31,15 @@ public override object GetValue() return Value; } - public override TemplateDataAccessor Index(TemplateDataAccessor index) + public override TemplateDataAccessor Index(TemplateDataAccessor index, bool safe) { try { var i = Convert.ToInt32(index.GetValue()); if (i >= 0 && i < Value.Length) - { return new TemplateStringAccessor(Value[i].ToString()); - } + if (safe) + return new TemplateStringAccessor(string.Empty); throw new TemplateRuntimeException($"Index out of range: {index}, Length: {Value.Length}", dataAccessor: this); } catch (TemplateRuntimeException) diff --git a/src/LLTSharp/ExpressionNodes/TemplateIndexExpressionNode.cs b/src/LLTSharp/ExpressionNodes/TemplateIndexExpressionNode.cs index 76c6d1a..185aaa0 100644 --- a/src/LLTSharp/ExpressionNodes/TemplateIndexExpressionNode.cs +++ b/src/LLTSharp/ExpressionNodes/TemplateIndexExpressionNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using LLTSharp.DataAccessors; namespace LLTSharp.ExpressionNodes { @@ -19,23 +20,30 @@ public class TemplateIndexExpressionNode : TemplateExpressionNode /// public TemplateExpressionNode Index { get; } + /// + /// Whether to return instead of exception if index does not exist. + /// + public bool SafeMode { get; } + /// /// Creates a new instance of the TemplateIndexExpressionNode class. /// /// The child expression node that is being indexed. /// The index expression node that specifies the index to access. + /// Whether to return instead of exception if index does not exist. /// - public TemplateIndexExpressionNode(TemplateExpressionNode child, TemplateExpressionNode index) + public TemplateIndexExpressionNode(TemplateExpressionNode child, TemplateExpressionNode index, bool safe) { Child = child ?? throw new ArgumentNullException(nameof(child)); Index = index ?? throw new ArgumentNullException(nameof(index)); + SafeMode = safe; } public override TemplateDataAccessor Evaluate(TemplateContextAccessor data) { var child = Child.Evaluate(data); var index = Index.Evaluate(data); - return child.Index(index); + return child.Index(index, SafeMode); } public override string ToString() diff --git a/src/LLTSharp/ExpressionNodes/TemplateMethodCallExpressionNode.cs b/src/LLTSharp/ExpressionNodes/TemplateMethodCallExpressionNode.cs index 59a2467..19a8d4d 100644 --- a/src/LLTSharp/ExpressionNodes/TemplateMethodCallExpressionNode.cs +++ b/src/LLTSharp/ExpressionNodes/TemplateMethodCallExpressionNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using LLTSharp.DataAccessors; namespace LLTSharp.ExpressionNodes { @@ -19,6 +20,11 @@ public class TemplateMethodCallExpressionNode : TemplateExpressionNode /// public string MethodName { get; } + /// + /// Whether to return instead of exception if method does not exist. + /// + public bool SafeMode { get; } + /// /// The arguments to the method call expression node. /// @@ -29,20 +35,26 @@ public class TemplateMethodCallExpressionNode : TemplateExpressionNode /// /// The child expression node that is passed as caller to the method. /// The name of the method to be called. + /// Whether to return instead of exception if method does not exist. /// The arguments to the method call expression node. /// Thrown when the parameter is null. /// Thrown when the parameter is null or empty. - public TemplateMethodCallExpressionNode(TemplateExpressionNode child, string methodName, IEnumerable arguments) + public TemplateMethodCallExpressionNode(TemplateExpressionNode child, string methodName, bool safe, IEnumerable arguments) { Child = child ?? throw new ArgumentNullException(nameof(child)); MethodName = string.IsNullOrEmpty(methodName) ? throw new ArgumentException("Method name cannot be null or empty.", nameof(methodName)) : methodName; + SafeMode = safe; Arguments = arguments.ToArray(); } public override TemplateDataAccessor Evaluate(TemplateContextAccessor context) { var child = Child.Evaluate(context); - return child.Call(MethodName, Arguments.Select(arg => arg.Evaluate(context)).ToArray()); + + var function = context.Functions.TryGetFunction(MethodName); + if (function != null && function.CanBeMethod) + return function.Call(child, Arguments.Select(arg => arg.Evaluate(context)).ToArray()); + return child.Call(MethodName, SafeMode, Arguments.Select(arg => arg.Evaluate(context)).ToArray()); } public override string ToString() diff --git a/src/LLTSharp/ExpressionNodes/TemplatePropertyExpressionNode.cs b/src/LLTSharp/ExpressionNodes/TemplatePropertyExpressionNode.cs index 620895b..3ad681f 100644 --- a/src/LLTSharp/ExpressionNodes/TemplatePropertyExpressionNode.cs +++ b/src/LLTSharp/ExpressionNodes/TemplatePropertyExpressionNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using LLTSharp.DataAccessors; namespace LLTSharp.ExpressionNodes { @@ -19,23 +20,30 @@ public class TemplatePropertyExpressionNode : TemplateExpressionNode /// public string PropertyName { get; } + /// + /// Whether to return instead of exception if property does not exist. + /// + public bool SafeMode { get; } + /// /// Creates a new instance of the class. /// /// The child expression node that has the property to access. /// The name of the property to access. + /// Whether to return instead of exception if property does not exist. /// /// - public TemplatePropertyExpressionNode(TemplateExpressionNode child, string propertyName) + public TemplatePropertyExpressionNode(TemplateExpressionNode child, string propertyName, bool safe) { Child = child ?? throw new ArgumentNullException(nameof(child)); PropertyName = string.IsNullOrEmpty(propertyName) ? throw new ArgumentException("Property name cannot be null or empty.", nameof(propertyName)) : propertyName; + SafeMode = safe; } public override TemplateDataAccessor Evaluate(TemplateContextAccessor context) { var child = Child.Evaluate(context); - return child.Property(PropertyName); + return child.Property(PropertyName, SafeMode); } public override string ToString() @@ -43,4 +51,45 @@ public override string ToString() return $"{Child}.{PropertyName}"; } } + + /// + /// Represents a property check expression node in a template. + /// + public class TemplateHasPropertyExpressionNode : TemplateExpressionNode + { + /// + /// Gets the child expression node that has the property to check. + /// + public TemplateExpressionNode Child { get; } + + /// + /// Gets the name of the property to check. + /// + public TemplateExpressionNode PropertyName { get; } + + /// + /// Creates a new instance of the class. + /// + /// The child expression node that has the property to check. + /// The name of the property to check. + /// + /// + public TemplateHasPropertyExpressionNode(TemplateExpressionNode child, TemplateExpressionNode propertyName) + { + Child = child ?? throw new ArgumentNullException(nameof(child)); + PropertyName = propertyName ?? throw new ArgumentException("Property name cannot be null or empty.", nameof(propertyName)); + } + + public override TemplateDataAccessor Evaluate(TemplateContextAccessor context) + { + var child = Child.Evaluate(context); + var propertyName = PropertyName.Evaluate(context).ToString(); + return new TemplateBooleanAccessor(child.HasProperty(propertyName)); + } + + public override string ToString() + { + return $"{Child} ? {PropertyName}"; + } + } } \ No newline at end of file diff --git a/src/LLTSharp/ExpressionNodes/TemplateUnaryOperatorExpressionNode.cs b/src/LLTSharp/ExpressionNodes/TemplateUnaryOperatorExpressionNode.cs index 3df63e3..d86986b 100644 --- a/src/LLTSharp/ExpressionNodes/TemplateUnaryOperatorExpressionNode.cs +++ b/src/LLTSharp/ExpressionNodes/TemplateUnaryOperatorExpressionNode.cs @@ -38,6 +38,7 @@ private static string OperatorToString(UnaryOperatorType type) { UnaryOperatorType.Negate => "-", UnaryOperatorType.LogicalNot => "!", + UnaryOperatorType.LengthOf => "#", _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } diff --git a/src/LLTSharp/LLTParser.cs b/src/LLTSharp/LLTParser.cs index b7f0197..d44a9dc 100644 --- a/src/LLTSharp/LLTParser.cs +++ b/src/LLTSharp/LLTParser.cs @@ -106,6 +106,7 @@ private static void DeclareValues(ParserBuilder builder) builder.CreateRule("function_access") .Identifier() + .Optional(b => b.Literal('?')) .Literal('(') .ZeroOrMoreSeparated(b => b .Rule("expression"), b => b.Literal(',')) @@ -113,20 +114,20 @@ private static void DeclareValues(ParserBuilder builder) .Transform(v => { var functionName = v.Children[0].Text; - var arguments = v.Children[2].SelectValues(); + var arguments = v.Children[3].SelectValues(); + var safe = v.Children[1].Length > 0; return new TemplateMethodCallExpressionNode(new TemplateContextAccessExpressionNode(), - functionName, arguments); + functionName, safe, arguments); }); builder.CreateRule("context_access") .Choice( - b => b - .Literal("ctx") + b => b.Literal("ctx") .Transform(_ => new TemplateContextAccessExpressionNode()), - b => b - .Identifier() + b => b.Optional(b => b.Literal('?')) + .Identifier() .Transform(v => new TemplatePropertyExpressionNode( - new TemplateContextAccessExpressionNode(), v.Text))); + new TemplateContextAccessExpressionNode(), v[1].Text, v[0].Length > 0))); builder.CreateRule("primary") .Choice( @@ -152,16 +153,19 @@ private static void DeclareExpressions(ParserBuilder builder) builder.CreateRule("postfix_member") .Rule("primary") .ZeroOrMore(b => b.Choice( - b => b.Literal('.') // Method call + b => b.Optional(b => b.Literal('?')) // Method call + .Literal('.') .Token("method_name") .Literal('(') .ZeroOrMoreSeparated(a => a.Rule("expression"), s => s.Literal(',')) .Literal(')'), - b => b.Literal('.') // Field access + b => b.Optional(b => b.Literal('?')) // Field access + .Literal('.') .Token("field_name"), - b => b.Literal('[') // Index access + b => b.Optional(b => b.Literal('?')) // Index access + .Literal('[') .Rule("expression") .Literal(']') )) @@ -172,27 +176,28 @@ private static void DeclareExpressions(ParserBuilder builder) foreach (var _member in v.Children[1]) { var member = _member.Children[0]; + var safe = member.Children[0].Length > 0; switch (member.Result.occurency) { case 0: target = new TemplateMethodCallExpressionNode(target, - member.GetValue(1), - member.Children[3].SelectValues()); + member.GetValue(2), safe, + member.Children[4].SelectValues()); break; case 1: target = new TemplatePropertyExpressionNode(target, - member.GetValue(1)); + member.GetValue(2), safe); break; case 2: target = new TemplateIndexExpressionNode(target, - member.GetValue(1)); + member.GetValue(2), safe); break; } @@ -202,7 +207,7 @@ private static void DeclareExpressions(ParserBuilder builder) }); builder.CreateRule("prefix_operator") - .ZeroOrMore(b => b.LiteralChoice("+", "-", "!")) + .ZeroOrMore(b => b.LiteralChoice("+", "-", "!", "#")) .Rule("postfix_member") .Transform(v => { @@ -222,6 +227,9 @@ private static void DeclareExpressions(ParserBuilder builder) case "!": target = new TemplateUnaryOperatorExpressionNode(UnaryOperatorType.LogicalNot, target); break; + case "#": + target = new TemplateUnaryOperatorExpressionNode(UnaryOperatorType.LengthOf, target); + break; } } @@ -236,13 +244,16 @@ private static void DeclareExpressions(ParserBuilder builder) static object? OperatorFactory(ParsedRuleResultBase result) { var children = result.Children; - var target = children[0].GetValue(); + var left = children[0].GetValue(); for (int i = 1; i < children.Count; i += 2) { var right = children[i + 1].GetValue(); var opStr = children[i].GetIntermediateValue(); + if (opStr is "?:") + return new TemplateHasPropertyExpressionNode(left, right); + var op = opStr switch { "*" => BinaryOperatorType.Multiply, @@ -258,13 +269,14 @@ private static void DeclareExpressions(ParserBuilder builder) "!=" => BinaryOperatorType.NotEqual, "&&" => BinaryOperatorType.LogicalAnd, "||" => BinaryOperatorType.LogicalOr, + "??" => BinaryOperatorType.Coalesce, _ => throw new InvalidOperationException($"Unknown operator '{opStr}'"), }; - target = new TemplateBinaryOperatorExpressionNode(op, target, right); + left = new TemplateBinaryOperatorExpressionNode(op, left, right); } - return target; + return left; } builder.CreateRule("multiplicative_operator") // multiplicative @@ -285,8 +297,14 @@ private static void DeclareExpressions(ParserBuilder builder) includeSeparatorsInResult: true) .Transform(OperatorFactory); - builder.CreateRule("equality_operator") // equality (==, !=) + builder.CreateRule("has_operator") // has (?:) .OneOrMoreSeparated(b => b.Rule("relational_operator"), + o => o.Literal("?:"), + includeSeparatorsInResult: true) + .Transform(OperatorFactory); + + builder.CreateRule("equality_operator") // equality (==, !=) + .OneOrMoreSeparated(b => b.Rule("has_operator"), o => o.LiteralChoice("==", "!="), includeSeparatorsInResult: true) .Transform(OperatorFactory); @@ -303,8 +321,14 @@ private static void DeclareExpressions(ParserBuilder builder) includeSeparatorsInResult: true) .Transform(OperatorFactory); + builder.CreateRule("coalesce_operator") // coalesce (??) + .OneOrMoreSeparated(b => b.Rule("logical_or_operator"), + o => o.Literal("??"), + includeSeparatorsInResult: true) + .Transform(OperatorFactory); + builder.CreateRule("ternary_operator") // ternary (? :) - .Rule("logical_or_operator") + .Rule("coalesce_operator") .Optional(b => b .Literal('?') .Rule("expression") diff --git a/src/LLTSharp/LLTSharp.csproj b/src/LLTSharp/LLTSharp.csproj index e27d404..a973d08 100644 --- a/src/LLTSharp/LLTSharp.csproj +++ b/src/LLTSharp/LLTSharp.csproj @@ -8,7 +8,7 @@ LLTSharp - 1.3.0 + 1.4.0 Roman K. RomeCore LLTSharp diff --git a/src/LLTSharp/Metadata/MetadataExtensions.cs b/src/LLTSharp/Metadata/MetadataExtensions.cs index 05af310..d51ce33 100644 --- a/src/LLTSharp/Metadata/MetadataExtensions.cs +++ b/src/LLTSharp/Metadata/MetadataExtensions.cs @@ -31,8 +31,7 @@ public static MetadataCollection ToMetadataCollection(this IEnumerable enu { foreach (var metadata in collection.GetAll()) if (metadata.Key == key) - if (metadata.Value is T value) - return value; + return (T)metadata.Value; return default; } diff --git a/src/LLTSharp/OperatorTypes.cs b/src/LLTSharp/OperatorTypes.cs index 22a556a..2686d00 100644 --- a/src/LLTSharp/OperatorTypes.cs +++ b/src/LLTSharp/OperatorTypes.cs @@ -6,7 +6,8 @@ public enum UnaryOperatorType { Negate, - LogicalNot + LogicalNot, + LengthOf } /// @@ -26,6 +27,7 @@ public enum BinaryOperatorType Equal, NotEqual, LogicalAnd, - LogicalOr + LogicalOr, + Coalesce } } \ No newline at end of file diff --git a/src/LLTSharp/TemplateContextAccessor.cs b/src/LLTSharp/TemplateContextAccessor.cs index 093aa01..6f7b931 100644 --- a/src/LLTSharp/TemplateContextAccessor.cs +++ b/src/LLTSharp/TemplateContextAccessor.cs @@ -173,7 +173,7 @@ public void AssignVariable(string variable, TemplateDataAccessor value) throw new TemplateRuntimeException("Variable not found: " + variable, dataAccessor: this); } - public override TemplateDataAccessor Property(string key) + public override TemplateDataAccessor Property(string key, bool safe) { foreach (var frame in _frames) { @@ -182,10 +182,10 @@ public override TemplateDataAccessor Property(string key) return data; } - return Context.Property(key); + return Context.Property(key, safe); } - public override TemplateDataAccessor Call(string methodName, TemplateDataAccessor[] arguments) + public override TemplateDataAccessor Call(string methodName, bool safe, TemplateDataAccessor[] arguments) { return Functions.CallFunction(methodName, this, arguments); } @@ -217,7 +217,7 @@ public override string ToString(string? format = null) public string RenderTemplate(string identifier, TemplateDataAccessor? newContext) { ITemplate? template = null; - if (Context.HasProperty(identifier) && Context.Property(identifier).GetValue() is ITemplate ctxValueTemplate) + if (Context.HasProperty(identifier) && Context.Property(identifier, safe: false).GetValue() is ITemplate ctxValueTemplate) template = ctxValueTemplate; if (template == null) template = Library.TryRetrieve(identifier); @@ -245,7 +245,7 @@ public string RenderTemplate(string identifier, TemplateDataAccessor? newContext public IEnumerable RenderMessagesTemplate(string identifier, TemplateDataAccessor? newContext) { ITemplate? template = null; - if (Context.HasProperty(identifier) && Context.Property(identifier).GetValue() is ITemplate ctxValueTemplate) + if (Context.HasProperty(identifier) && Context.Property(identifier, safe: false).GetValue() is ITemplate ctxValueTemplate) template = ctxValueTemplate; if (template == null) template = Library.TryRetrieve(identifier); diff --git a/src/LLTSharp/TemplateDataAccessor.cs b/src/LLTSharp/TemplateDataAccessor.cs index f0e7afd..a45d341 100644 --- a/src/LLTSharp/TemplateDataAccessor.cs +++ b/src/LLTSharp/TemplateDataAccessor.cs @@ -29,26 +29,33 @@ public abstract class TemplateDataAccessor : IDisposable /// Gets the template data property associated with the specified property name. /// /// The property name to retrieve the template data for. + /// Indicates whether to return a null accessor if the property does not exist. /// The template data that is associated with the specified property name. - public virtual TemplateDataAccessor Property(string name) => - throw new TemplateRuntimeException($"Cannot get value for key '{name}'"); + public virtual TemplateDataAccessor Property(string name, bool safe) => + safe ? TemplateNullAccessor.Instance : + throw new TemplateRuntimeException($"Cannot get value for key '{name}'", this); /// /// Gets the template data associated with the specified index. /// /// The index to retrieve the template data for. + /// Indicates whether to return a null accessor if the index does not exist. /// The template data that is at the specified index. - public virtual TemplateDataAccessor Index(TemplateDataAccessor index) => - throw new TemplateRuntimeException("Indexing not supported."); + public virtual TemplateDataAccessor Index(TemplateDataAccessor index, bool safe) => + safe ? TemplateNullAccessor.Instance : + throw new TemplateRuntimeException($"Cannot get value for index '{index}'.", this); /// /// Calls a method or function on the current data. /// /// The name of the method to call. + /// Indicates whether to return a null accessor if the method does not exist. /// The arguments to pass to the method. /// The result of the method call. - public virtual TemplateDataAccessor Call(string methodName, TemplateDataAccessor[] arguments) + public virtual TemplateDataAccessor Call(string methodName, bool safe, TemplateDataAccessor[] arguments) { + if (safe) + return TemplateNullAccessor.Instance; throw new TemplateRuntimeException( $"Method '{methodName}' is not supported on type '{GetType().Name}'", dataAccessor: this); @@ -69,6 +76,9 @@ public virtual TemplateDataAccessor Operator(UnaryOperatorType type) case UnaryOperatorType.LogicalNot: return new TemplateBooleanAccessor(!AsBoolean()); + case UnaryOperatorType.LengthOf: + return new TemplateNumberAccessor(Length); + default: throw new TemplateRuntimeException("Invalid operator type.", dataAccessor: this); } @@ -107,6 +117,9 @@ public virtual TemplateDataAccessor Operator(TemplateDataAccessor other, BinaryO case BinaryOperatorType.LogicalOr: return new TemplateBooleanAccessor(AsBoolean() || other.AsBoolean()); + case BinaryOperatorType.Coalesce: + return this is TemplateNullAccessor ? other : this; + default: throw new TemplateRuntimeException("Invalid operator type.", dataAccessor: this); } @@ -130,6 +143,10 @@ public virtual TemplateDataAccessor Operator(TemplateDataAccessor other, BinaryO /// The format to use for converting the template data, or to use the default format. /// A string representing the template data. public abstract string ToString(string? format = null); + public override string ToString() + { + return ToString(null); + } private bool isDisposed; protected virtual void Dispose(bool disposing) diff --git a/src/LLTSharp/TemplateFunction.cs b/src/LLTSharp/TemplateFunction.cs index a9d722d..e46ee99 100644 --- a/src/LLTSharp/TemplateFunction.cs +++ b/src/LLTSharp/TemplateFunction.cs @@ -13,40 +13,24 @@ public class TemplateFunction /// /// Gets the name of the function. /// - public string? Name { get; } + public string Name { get; } /// - /// Initializes a new instance of the class. - /// - /// The function to be called. - /// - public TemplateFunction(Func function) - { - Name = null; - _function = function ?? throw new ArgumentNullException(nameof(function)); - } - - /// - /// Initializes a new instance of the class. + /// Gets a value indicating whether the function can be used as a method. /// - /// The function to be called. - /// - public TemplateFunction(Func function) - { - Name = null; - if (function == null) throw new ArgumentNullException(nameof(function)); - _function = (self, args) => TemplateDataAccessor.Create(function(self, args)); - } + public bool CanBeMethod { get; } /// /// Initializes a new instance of the class. /// /// The name of the function. /// The function to be called. + /// Whether the function can be used as a method. /// - public TemplateFunction(string? name, Func function) + public TemplateFunction(string? name, Func function, bool canBeMethod = false) { Name = name; + CanBeMethod = canBeMethod; _function = function ?? throw new ArgumentNullException(nameof(function)); } @@ -55,10 +39,12 @@ public TemplateFunction(string? name, Func /// The name of the function. /// The function to be called. + /// Whether the function can be used as a method. /// - public TemplateFunction(string? name, Func function) + public TemplateFunction(string? name, Func function, bool canBeMethod = false) { Name = name; + CanBeMethod = canBeMethod; if (function == null) throw new ArgumentNullException(nameof(function)); _function = (self, args) => TemplateDataAccessor.Create(function(self, args)); } diff --git a/src/LLTSharp/TemplateFunctionSet.cs b/src/LLTSharp/TemplateFunctionSet.cs index 5d9cf11..dcaf4fd 100644 --- a/src/LLTSharp/TemplateFunctionSet.cs +++ b/src/LLTSharp/TemplateFunctionSet.cs @@ -1,14 +1,16 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; +using LLTSharp.DataAccessors; namespace LLTSharp { /// /// A set of functions that can be called inside a template. /// - public class TemplateFunctionSet + public class TemplateFunctionSet : IEnumerable { private Dictionary _functions; @@ -33,13 +35,13 @@ public TemplateFunctionSet(params TemplateFunction[] functions) } /// - /// Initializes a new instance of the class. + /// Determines whether a function with the specified name exists in this set. /// - /// A dictionary of function names and their corresponding implementations. - /// Thrown when the parameter is null. - public TemplateFunctionSet(IDictionary functions) + /// The name of the function to check. + /// if a function with the specified name exists in this set; otherwise, . + public bool Exists(string functionName) { - _functions = functions?.ToDictionary(k => k.Key, v => v.Value) ?? throw new ArgumentNullException(nameof(functions)); + return _functions.ContainsKey(functionName); } /// @@ -88,7 +90,17 @@ public TemplateDataAccessor CallFunction(string functionName, TemplateDataAccess public TemplateDataAccessor CallFunction(string functionName, TemplateDataAccessor? self, TemplateDataAccessor[] args) { var function = GetFunction(functionName); - return function.Call(self, args); + return function?.Call(self, args) ?? TemplateNullAccessor.Instance; + } + + public IEnumerator GetEnumerator() + { + return _functions.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } /// diff --git a/src/LLTSharp/TemplateLibrary.cs b/src/LLTSharp/TemplateLibrary.cs index 57e5fae..416c318 100644 --- a/src/LLTSharp/TemplateLibrary.cs +++ b/src/LLTSharp/TemplateLibrary.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -21,11 +22,11 @@ namespace LLTSharp /// public partial class TemplateLibrary : IEnumerable { - private static readonly ConcurrentDictionary _templateParsers = new(); + private static readonly ConcurrentDictionary _sharedTemplateParsers = new(); static TemplateLibrary() { - _templateParsers.TryAdd("llt", new LLTParser()); + _sharedTemplateParsers.TryAdd("llt", new LLTParser()); } /// @@ -34,21 +35,33 @@ static TemplateLibrary() /// The language code of the template language, e.g., "llt". /// The template parser to register. /// Thrown when a parser is already registered for the specified language code. - public static void RegisterParser(string languageCode, ITemplateParser parser) + public static void RegisterSharedParser(string languageCode, ITemplateParser parser) { languageCode = languageCode.ToLowerInvariant(); - if (_templateParsers.ContainsKey(languageCode)) + if (_sharedTemplateParsers.ContainsKey(languageCode)) throw new ArgumentException($"Parser already registered for language: '{languageCode}'"); - _templateParsers.TryAdd(languageCode, parser); + _sharedTemplateParsers.TryAdd(languageCode, parser); + } + + /// + /// Removes a template parser for the specified language. + /// + /// The language code of the template language to remove, e.g., "llt". + /// True if the parser was successfully removed; otherwise, false. + public static bool RemoveSharedParser(string languageCode) + { + languageCode = languageCode.ToLowerInvariant(); + return _sharedTemplateParsers.TryRemove(languageCode, out _); } + private readonly ConcurrentDictionary _instanceTemplateParsers = new(); private readonly Dictionary> _templates = new(); private readonly HashSet _allTemplates = new(); private readonly Dictionary _fallbackSchemes = new(); - private readonly Dictionary> _fallbackMetadatas = new(); + private readonly Dictionary> _fallbackMetadatas = new(); private readonly List _metadataFactories = new() { new VersionMetadataFactory(), @@ -103,6 +116,31 @@ public TemplateLibrary(ILanguageFallbackScheme? languageFallbackScheme) SetLanguageFallbackScheme(languageFallbackScheme ?? new MajorLanguageFallbackScheme()); } + /// + /// Registers a template parser for the specified language. + /// + /// The language code of the template language, e.g., "llt". + /// The template parser to register. + /// Thrown when a parser is already registered for the specified language code. + public void RegisterParser(string languageCode, ITemplateParser parser) + { + languageCode = languageCode.ToLowerInvariant(); + if (_instanceTemplateParsers.ContainsKey(languageCode)) + throw new ArgumentException($"Parser already registered for language: '{languageCode}'"); + _instanceTemplateParsers.TryAdd(languageCode, parser); + } + + /// + /// Removes a template parser for the specified language. + /// + /// The language code of the template language to remove, e.g., "llt". + /// True if the parser was successfully removed; otherwise, false. + public bool RemoveParser(string languageCode) + { + languageCode = languageCode.ToLowerInvariant(); + return _instanceTemplateParsers.TryRemove(languageCode, out _); + } + /// /// Adds a template to the library and associates it with its metadata. /// @@ -124,8 +162,11 @@ public void Add(ITemplate template) { var metadataType = metadata.GetType(); if (!_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) - _fallbackMetadatas[metadataType] = fallbackMetadatas = new HashSet(); - fallbackMetadatas.Add(metadata); + _fallbackMetadatas[metadataType] = fallbackMetadatas = new Dictionary(); + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount)) + fallbackMetadatas[metadata] = existingCount + 1; + else + fallbackMetadatas[metadata] = 1; if (!_templates.TryGetValue(metadata, out var templatesByMetadata)) _templates[metadata] = templatesByMetadata = new List(); @@ -156,8 +197,11 @@ public bool TryAdd(ITemplate template) { var metadataType = metadata.GetType(); if (!_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) - _fallbackMetadatas[metadataType] = fallbackMetadatas = new HashSet(); - fallbackMetadatas.Add(metadata); + _fallbackMetadatas[metadataType] = fallbackMetadatas = new Dictionary(); + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount)) + fallbackMetadatas[metadata] = existingCount + 1; + else + fallbackMetadatas[metadata] = 1; if (!_templates.TryGetValue(metadata, out var templatesByMetadata)) _templates[metadata] = templatesByMetadata = new List(); @@ -168,8 +212,6 @@ public bool TryAdd(ITemplate template) return true; } - - /// /// Adds a template to the library and associates it with its metadata. /// @@ -184,6 +226,9 @@ public void AddRange(IEnumerable templates) lock (_lockObject) foreach (var template in templates) { + if (template == null) + continue; + if (!_allTemplates.Add(template)) throw new ArgumentException("Template already exists in the library.", nameof(template)); @@ -191,8 +236,11 @@ public void AddRange(IEnumerable templates) { var metadataType = metadata.GetType(); if (!_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) - _fallbackMetadatas[metadataType] = fallbackMetadatas = new HashSet(); - fallbackMetadatas.Add(metadata); + _fallbackMetadatas[metadataType] = fallbackMetadatas = new Dictionary(); + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount)) + fallbackMetadatas[metadata] = existingCount + 1; + else + fallbackMetadatas[metadata] = 1; if (!_templates.TryGetValue(metadata, out var templatesByMetadata)) _templates[metadata] = templatesByMetadata = new List(); @@ -206,7 +254,6 @@ public void AddRange(IEnumerable templates) /// /// The templates to add. Cannot be . /// Thrown if is . - /// Thrown if a template exists in the library. public void TryAddRange(IEnumerable templates) { if (templates == null) @@ -215,6 +262,9 @@ public void TryAddRange(IEnumerable templates) lock (_lockObject) foreach (var template in templates) { + if (template == null) + continue; + if (!_allTemplates.Add(template)) continue; @@ -222,8 +272,11 @@ public void TryAddRange(IEnumerable templates) { var metadataType = metadata.GetType(); if (!_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) - _fallbackMetadatas[metadataType] = fallbackMetadatas = new HashSet(); - fallbackMetadatas.Add(metadata); + _fallbackMetadatas[metadataType] = fallbackMetadatas = new Dictionary(); + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount)) + fallbackMetadatas[metadata] = existingCount + 1; + else + fallbackMetadatas[metadata] = 1; if (!_templates.TryGetValue(metadata, out var templatesByMetadata)) _templates[metadata] = templatesByMetadata = new List(); @@ -232,6 +285,104 @@ public void TryAddRange(IEnumerable templates) } } + /// + /// Removes a template from the library. + /// + /// The template to remove. Cannot be . + /// if the template was removed; otherwise, . + /// + public bool Remove(ITemplate template) + { + if (template == null) + throw new ArgumentNullException(nameof(template)); + + lock (_lockObject) + { + if (!_allTemplates.Remove(template)) + return false; + + foreach (var metadata in template.Metadata) + { + var metadataType = metadata.GetType(); + if (_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) + { + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount) && existingCount == 1) + fallbackMetadatas.Remove(metadata); + else + fallbackMetadatas[metadata] = existingCount - 1; + if (fallbackMetadatas.Count == 0) + _fallbackMetadatas.Remove(metadataType); + } + + if (_templates.TryGetValue(metadata, out var templatesByMetadata)) + { + templatesByMetadata.Remove(template); + if (templatesByMetadata.Count == 0) + _templates.Remove(metadata); + } + } + } + + return true; + } + + /// + /// Removes a range of templates from the library. + /// + /// The templates to remove. Cannot be . + /// + public void RemoveRange(IEnumerable templates) + { + if (templates == null) + throw new ArgumentNullException(nameof(templates)); + + lock (_lockObject) + { + foreach (var template in templates) + { + if (template == null) + continue; + + if (!_allTemplates.Remove(template)) + continue; + + foreach (var metadata in template.Metadata) + { + var metadataType = metadata.GetType(); + if (_fallbackMetadatas.TryGetValue(metadataType, out var fallbackMetadatas)) + { + if (fallbackMetadatas.TryGetValue(metadata, out var existingCount) && existingCount == 1) + fallbackMetadatas.Remove(metadata); + else + fallbackMetadatas[metadata] = existingCount - 1; + if (fallbackMetadatas.Count == 0) + _fallbackMetadatas.Remove(metadataType); + } + + if (_templates.TryGetValue(metadata, out var templatesByMetadata)) + { + templatesByMetadata.Remove(template); + if (templatesByMetadata.Count == 0) + _templates.Remove(metadata); + } + } + } + } + } + + /// + /// Clears all templates from the library. + /// + public void Clear() + { + lock (_lockObject) + { + _allTemplates.Clear(); + _fallbackMetadatas.Clear(); + _templates.Clear(); + } + } + /// /// Sets the fallback scheme for the specified type of metadata. /// @@ -288,6 +439,15 @@ IEnumerator IEnumerable.GetEnumerator() + private bool TryGetParser(string languageCode, out ITemplateParser parser) + { + if (_instanceTemplateParsers.TryGetValue(languageCode, out parser)) + return true; + if (_sharedTemplateParsers.TryGetValue(languageCode, out parser)) + return true; + return false; + } + /// /// Imports a set of templates from the specified string contents. /// @@ -297,7 +457,7 @@ IEnumerator IEnumerable.GetEnumerator() /// Thrown when the template contents cannot be parsed. public void ImportFromString(string templateContents, string languageCode = "llt") { - if (!_templateParsers.TryGetValue(languageCode, out var parser)) + if (!TryGetParser(languageCode, out var parser)) throw new ArgumentException($"No parser registered for language: '{languageCode}'."); var templates = parser.Parse(templateContents, MetadataFactories); @@ -314,7 +474,7 @@ public void ImportFromString(string templateContents, string languageCode = "llt /// Thrown when the template contents cannot be parsed. public void ImportFromReader(TextReader reader, string languageCode = "llt") { - if (!_templateParsers.TryGetValue(languageCode, out var parser)) + if (!TryGetParser(languageCode, out var parser)) throw new ArgumentException($"No parser registered for language: '{languageCode}'."); var templateContents = reader?.ReadToEnd() ?? throw new ArgumentNullException(nameof(reader)); @@ -332,7 +492,7 @@ public void ImportFromReader(TextReader reader, string languageCode = "llt") /// Thrown when the template contents cannot be parsed. public void ImportFromStream(Stream stream, string languageCode = "llt") { - if (!_templateParsers.TryGetValue(languageCode, out var parser)) + if (!TryGetParser(languageCode, out var parser)) throw new ArgumentException($"No parser registered for language: '{languageCode}'."); using var reader = new StreamReader(stream); @@ -360,7 +520,7 @@ public void ImportFromLibrary(TemplateLibrary library) public void ImportFromFile(string filename) { var languageCode = Path.GetExtension(filename)?.TrimStart('.')?.ToLowerInvariant() ?? "llt"; - if (!_templateParsers.TryGetValue(languageCode, out var parser)) + if (!TryGetParser(languageCode, out var parser)) throw new ArgumentException($"No parser registered for language: '{languageCode}'."); var templateContents = File.ReadAllText(filename); @@ -377,7 +537,7 @@ public void ImportFromFile(string filename) /// Thrown when the template contents cannot be parsed. public void ImportFromFile(string filename, string languageCode) { - if (!_templateParsers.TryGetValue(languageCode, out var parser)) + if (!TryGetParser(languageCode, out var parser)) throw new ArgumentException($"No parser registered for language: '{languageCode}'."); var templateContents = File.ReadAllText(filename); @@ -411,7 +571,7 @@ public IEnumerable ImportFromFolder(string folderPath, SearchO { var languageCode = Path.GetExtension(file)?.TrimStart('.')?.ToLowerInvariant(); - if (languageCode != null && _templateParsers.TryGetValue(languageCode, out var parser)) + if (languageCode != null && TryGetParser(languageCode, out var parser)) { ParsingException? _ex = null; @@ -455,7 +615,7 @@ public List ImportFromAssembly(Assembly assembly, string? fold continue; var languageCode = Path.GetExtension(resource)?.TrimStart('.')?.ToLowerInvariant(); - if (_templateParsers.TryGetValue(languageCode, out var parser)) + if (TryGetParser(languageCode, out var parser)) { try { diff --git a/src/LLTSharp/TemplateLibrary.retrieval.cs b/src/LLTSharp/TemplateLibrary.retrieval.cs index 2c7e84e..212f7bd 100644 --- a/src/LLTSharp/TemplateLibrary.retrieval.cs +++ b/src/LLTSharp/TemplateLibrary.retrieval.cs @@ -23,7 +23,7 @@ private bool TryRetrieve(IMetadata metadata, bool useFallbackSchemes, out List Date: Sun, 21 Jun 2026 00:44:52 +0500 Subject: [PATCH 2/2] fix: TryGetAdditional in metadata extensions --- src/LLTSharp/Metadata/MetadataExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LLTSharp/Metadata/MetadataExtensions.cs b/src/LLTSharp/Metadata/MetadataExtensions.cs index d51ce33..037a1fa 100644 --- a/src/LLTSharp/Metadata/MetadataExtensions.cs +++ b/src/LLTSharp/Metadata/MetadataExtensions.cs @@ -31,7 +31,7 @@ public static MetadataCollection ToMetadataCollection(this IEnumerable enu { foreach (var metadata in collection.GetAll()) if (metadata.Key == key) - return (T)metadata.Value; + return (T)Convert.ChangeType(metadata.Value, typeof(T)); return default; }