diff --git a/NMF.sln b/NMF.sln
index 21ca51ed..6520e043 100644
--- a/NMF.sln
+++ b/NMF.sln
@@ -218,7 +218,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PropertyService", "Services
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3A13497C-33A7-4658-B206-1C1989D562FB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelServicesTests", "Services\Tests\ModelServicesTests\ModelServicesTests.csproj", "{B62AF5A1-EA15-4226-8C22-193BCEDA198F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModelServicesTests", "Services\Tests\ModelServicesTests\ModelServicesTests.csproj", "{B62AF5A1-EA15-4226-8C22-193BCEDA198F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyText", "UserInterfaces\AnyText\AnyText.csproj", "{59BBC967-A1D3-4215-8111-8D7033CEECE8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnyText.Tests", "UserInterfaces\Tests\AnyText.Tests\AnyText.Tests.csproj", "{343546B3-D9C3-47E5-8927-961097F6BC75}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -760,6 +764,22 @@ Global
{B62AF5A1-EA15-4226-8C22-193BCEDA198F}.Signed Debug|Any CPU.Build.0 = Debug|Any CPU
{B62AF5A1-EA15-4226-8C22-193BCEDA198F}.Signed Release|Any CPU.ActiveCfg = Release|Any CPU
{B62AF5A1-EA15-4226-8C22-193BCEDA198F}.Signed Release|Any CPU.Build.0 = Release|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Signed Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Signed Debug|Any CPU.Build.0 = Debug|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Signed Release|Any CPU.ActiveCfg = Release|Any CPU
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8}.Signed Release|Any CPU.Build.0 = Release|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Release|Any CPU.Build.0 = Release|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Signed Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Signed Debug|Any CPU.Build.0 = Debug|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Signed Release|Any CPU.ActiveCfg = Release|Any CPU
+ {343546B3-D9C3-47E5-8927-961097F6BC75}.Signed Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -842,6 +862,8 @@ Global
{B40B7DC2-CFA8-4C1A-8289-ADF7507D651D} = {1E8C8464-ACBC-4201-9F38-FFBA1959A860}
{3A13497C-33A7-4658-B206-1C1989D562FB} = {1E8C8464-ACBC-4201-9F38-FFBA1959A860}
{B62AF5A1-EA15-4226-8C22-193BCEDA198F} = {3A13497C-33A7-4658-B206-1C1989D562FB}
+ {59BBC967-A1D3-4215-8111-8D7033CEECE8} = {6B587373-3C4C-4127-BFD1-DA9A89C0555B}
+ {343546B3-D9C3-47E5-8927-961097F6BC75} = {891A597A-A94C-43C7-BC65-3472326015DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7B6D2737-94B7-470E-B413-D9693622AA4D}
diff --git a/UserInterfaces/AnyText/AnyText.csproj b/UserInterfaces/AnyText/AnyText.csproj
new file mode 100644
index 00000000..ace424db
--- /dev/null
+++ b/UserInterfaces/AnyText/AnyText.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0;net8.0
+ false
+ Library
+ NMF.AnyText
+ NMF.AnyText
+ true
+ ..\..\Build
+
+
+
diff --git a/UserInterfaces/AnyText/Matcher.cs b/UserInterfaces/AnyText/Matcher.cs
new file mode 100644
index 00000000..cfb27d49
--- /dev/null
+++ b/UserInterfaces/AnyText/Matcher.cs
@@ -0,0 +1,141 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// Denotes a class that matches text using an incremental packrat parser
+ ///
+ public class Matcher
+ {
+ private readonly List _memoTable = new List();
+
+ private MemoLine GetLine(int line)
+ {
+ while (_memoTable.Count <= line)
+ {
+ _memoTable.Add(new MemoLine());
+ }
+ return _memoTable[line];
+ }
+
+ private static IEnumerable GetCol(SortedDictionary> line, int col)
+ {
+ if (line.TryGetValue(col, out var ruleApplications))
+ {
+ return ruleApplications;
+ }
+ return Enumerable.Empty();
+ }
+
+ internal RuleApplication MatchCore(Rule rule, ParseContext context, ref ParsePosition position)
+ {
+ var line = GetLine(position.Line);
+ var ruleApplications = GetCol(line.Columns, position.Col);
+
+ var ruleApplication = ruleApplications.FirstOrDefault(r => r.Rule == rule);
+ if (ruleApplication == null)
+ {
+ var col = position.Col;
+ ruleApplication = rule.Match(context, ref position);
+ if (!(ruleApplications is List ruleApplicationList))
+ {
+ // need to check again because another call could have created the column
+ ruleApplicationList = GetCol(line.Columns, col) as List;
+ if (ruleApplicationList == null)
+ {
+ ruleApplicationList = new List();
+ line.Columns.Add(col, ruleApplicationList);
+ }
+ }
+ line.MaxExaminedLength = ParsePositionDelta.Larger(line.MaxExaminedLength, PrependColOffset(ruleApplication.ExaminedTo, col));
+ ruleApplicationList.Add(ruleApplication);
+ }
+ else
+ {
+ position += ruleApplication.Length;
+ }
+ if (rule.TrailingWhitespaces)
+ {
+ RuleHelper.MoveOverWhitespace(context, ref position);
+ }
+ return ruleApplication;
+ }
+
+ private static ParsePositionDelta PrependColOffset(ParsePositionDelta examinedTo, int col)
+ {
+ if (examinedTo.Line == 0)
+ {
+ return new ParsePositionDelta(0, examinedTo.Col + col);
+ }
+ return examinedTo;
+ }
+
+ ///
+ /// Matches the provided rule with the given parse context
+ ///
+ /// The context in which the text is parsed, including the current input
+ /// A rule application for the entire text
+ public RuleApplication Match(ParseContext context)
+ {
+ var position = new ParsePosition(0, 0);
+ RuleHelper.MoveOverWhitespace(context, ref position);
+ var match = MatchCore(context.RootRule, context, ref position);
+ if (!match.IsPositive || position.Line == context.Input.Length)
+ {
+ return match;
+ }
+ return new FailedRuleApplication(context.RootRule, new ParsePositionDelta(position.Line, position.Col), position, "Unexpected content");
+ }
+
+ ///
+ /// Removes any memoization based on the given text edit
+ ///
+ /// The change in the input text
+ public void RemoveMemoizedRuleApplications(TextEdit edit)
+ {
+ for (int i = 0; i <= edit.Start.Line; i++)
+ {
+ var line = GetLine(i);
+
+ if (new ParsePosition(i, 0) + line.MaxExaminedLength < edit.Start)
+ {
+ continue;
+ }
+
+ var maxReach = default(ParsePositionDelta);
+
+ foreach (var col in line.Columns)
+ {
+ var pos = new ParsePosition(i, col.Key);
+
+ for (int j = col.Value.Count - 1; j >= 0; j--)
+ {
+ var ruleApplication = col.Value[j];
+ if (pos + ruleApplication.ExaminedTo < edit.Start)
+ {
+ maxReach = ParsePositionDelta.Larger(maxReach, PrependColOffset(ruleApplication.ExaminedTo, col.Key));
+ }
+ else
+ {
+ col.Value.RemoveAt(j);
+ }
+ }
+ }
+
+ line.MaxExaminedLength = maxReach;
+ }
+ }
+
+ private sealed class MemoLine
+ {
+ public SortedDictionary> Columns { get; } = new SortedDictionary>();
+
+ public ParsePositionDelta MaxExaminedLength { get; set; }
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Model/AddAssignRule.cs b/UserInterfaces/AnyText/Model/AddAssignRule.cs
new file mode 100644
index 00000000..1ec9d277
--- /dev/null
+++ b/UserInterfaces/AnyText/Model/AddAssignRule.cs
@@ -0,0 +1,87 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Model
+{
+ ///
+ /// Denotes a rule that adds the value of an inner rule to a collection of the semantic element
+ ///
+ /// The type of the context element
+ /// The type of the property value
+ public abstract class AddAssignRule : QuoteRule
+ {
+ ///
+ protected internal override void OnActivate(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement && application.GetValue(context) is TProperty propertyValue)
+ {
+ GetCollection(contextElement, context).Add(propertyValue);
+ }
+ }
+
+ ///
+ protected internal override void OnDeactivate(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement && application.GetValue(context) is TProperty propertyValue)
+ {
+ GetCollection(contextElement, context).Remove(propertyValue);
+ }
+ }
+
+ ///
+ protected internal override bool OnValueChange(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement && application.GetValue(context) is TProperty propertyValue)
+ {
+ GetCollection(contextElement, context).Add(propertyValue);
+ return true;
+ }
+ return false;
+ }
+
+
+ ///
+ /// Obtains the child collection
+ ///
+ /// the semantic element
+ /// the parse context in which the collection is obtained
+ /// a collection of values
+ public abstract ICollection GetCollection(TContext semanticElement, ParseContext context);
+
+ private sealed class AddAssignRuleApplication : SingleRuleApplication
+ {
+ public AddAssignRuleApplication(Rule rule, RuleApplication inner, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, inner, endsAt, examinedTo)
+ {
+ }
+
+ protected override void OnMigrate(RuleApplication oldValue, RuleApplication newValue, ParseContext context)
+ {
+ if (oldValue.IsActive)
+ {
+ oldValue.Deactivate(context);
+ newValue.Activate(context);
+ if (Rule is AddAssignRule addAssignRule && ContextElement is TContext contextElement)
+ {
+ var collection = addAssignRule.GetCollection(contextElement, context);
+ if (oldValue.GetValue(context) is TProperty oldProperty)
+ {
+ collection.Remove(oldProperty);
+ }
+ if (newValue.GetValue(context) is TProperty newProperty)
+ {
+ collection.Add(newProperty);
+ }
+ }
+ else
+ {
+ Rule.OnValueChange(this, context);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Model/AssignRule.cs b/UserInterfaces/AnyText/Model/AssignRule.cs
new file mode 100644
index 00000000..0d625ec9
--- /dev/null
+++ b/UserInterfaces/AnyText/Model/AssignRule.cs
@@ -0,0 +1,55 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Model
+{
+ ///
+ /// Denotes a rule that assigns the value of a child rule to a certain property
+ ///
+ /// The type of the context element
+ /// The type of the property value
+ public abstract class AssignRule : QuoteRule
+ {
+ ///
+ protected internal override void OnActivate(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement && application.GetValue(context) is TProperty propertyValue)
+ {
+ OnChangeValue(contextElement, propertyValue, context);
+ }
+ }
+
+ ///
+ protected internal override void OnDeactivate(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement)
+ {
+ OnChangeValue(contextElement, default, context);
+ }
+ }
+
+ ///
+ protected internal override bool OnValueChange(RuleApplication application, ParseContext context)
+ {
+ if (application.ContextElement is TContext contextElement && application.GetValue(context) is TProperty propertyValue)
+ {
+ OnChangeValue(contextElement, propertyValue, context);
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Gets called when the value changes
+ ///
+ /// the context element
+ /// the property value
+ /// the parsing context
+ protected abstract void OnChangeValue(TContext semanticElement, TProperty propertyValue, ParseContext context);
+
+ }
+}
diff --git a/UserInterfaces/AnyText/Model/ConvertRule.cs b/UserInterfaces/AnyText/Model/ConvertRule.cs
new file mode 100644
index 00000000..e5841d3a
--- /dev/null
+++ b/UserInterfaces/AnyText/Model/ConvertRule.cs
@@ -0,0 +1,60 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Model
+{
+ ///
+ /// Denotes a rule that applies custom conversions
+ ///
+ ///
+ public class ConvertRule : RegexRule
+ {
+ private static TypeConverter _converter = TypeDescriptor.GetConverter(typeof(T));
+
+
+ ///
+ public override RuleApplication CreateRuleApplication(string matched, ParsePosition position, ParsePositionDelta length, ParsePositionDelta examined, ParseContext context)
+ {
+ try
+ {
+ var converted = Convert(matched, context);
+ return new ConvertRuleApplication(this, matched, converted, length, examined);
+ }
+ catch (Exception ex)
+ {
+ return new FailedRuleApplication(this, examined, position, ex.Message);
+ }
+ }
+
+ ///
+ /// Converts the provided text to an element of type T
+ ///
+ /// the input text
+ /// the parse context
+ /// the parsed element
+ public virtual T Convert(string text, ParseContext context)
+ {
+ return (T)_converter.ConvertFromInvariantString(text);
+ }
+
+ private sealed class ConvertRuleApplication : LiteralRuleApplication
+ {
+ private readonly T _value;
+
+ public ConvertRuleApplication(Rule rule, string literal, T value, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, literal, endsAt, examinedTo)
+ {
+ _value = value;
+ }
+
+ public override object GetValue(ParseContext context)
+ {
+ return _value;
+ }
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Model/ModelElementRule.cs b/UserInterfaces/AnyText/Model/ModelElementRule.cs
new file mode 100644
index 00000000..15f1fee5
--- /dev/null
+++ b/UserInterfaces/AnyText/Model/ModelElementRule.cs
@@ -0,0 +1,39 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Model
+{
+ public class ModelElementRule : SequenceRule
+ {
+ protected virtual T CreateElement(IEnumerable inner)
+ {
+ return (T)Activator.CreateInstance(typeof(T));
+ }
+
+ protected override RuleApplication CreateRuleApplication(List inner, ParsePositionDelta length, ParsePositionDelta examined)
+ {
+ return new ModelElementRuleApplication(this, inner, CreateElement(inner), length, examined);
+ }
+
+ private class ModelElementRuleApplication : MultiRuleApplication
+ {
+ private readonly object _semanticElement;
+
+ public ModelElementRuleApplication(Rule rule, List inner, object semanticElement, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, inner, endsAt, examinedTo)
+ {
+ _semanticElement = semanticElement;
+ }
+
+ public override object ContextElement => _semanticElement;
+
+ public override object GetValue(ParseContext context)
+ {
+ return _semanticElement;
+ }
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Model/ParanthesesRule.cs b/UserInterfaces/AnyText/Model/ParanthesesRule.cs
new file mode 100644
index 00000000..1fba455f
--- /dev/null
+++ b/UserInterfaces/AnyText/Model/ParanthesesRule.cs
@@ -0,0 +1,33 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Model
+{
+ public class ParanthesesRule : SequenceRule
+ {
+ protected override RuleApplication CreateRuleApplication(List inner, ParsePositionDelta length, ParsePositionDelta examined)
+ {
+ return new ParanthesesRuleApplication(this, inner, length, examined);
+ }
+
+ private class ParanthesesRuleApplication : MultiRuleApplication
+ {
+ public ParanthesesRuleApplication(Rule rule, List inner, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, inner, endsAt, examinedTo)
+ {
+ }
+
+ public override object GetValue(ParseContext context)
+ {
+ if (Inner.Count < 3)
+ {
+ return null;
+ }
+ return Inner[1].GetValue(context);
+ }
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/ParseContext.cs b/UserInterfaces/AnyText/ParseContext.cs
new file mode 100644
index 00000000..aab998c5
--- /dev/null
+++ b/UserInterfaces/AnyText/ParseContext.cs
@@ -0,0 +1,48 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// The context in which a text is parsed
+ ///
+ public class ParseContext
+ {
+ ///
+ /// Creates a new instance
+ ///
+ /// the root rule
+ /// the matcher for the context
+ /// the string comparison mode
+ public ParseContext(Rule rootRule, Matcher matcher, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase)
+ {
+ RootRule = rootRule;
+ Matcher = matcher;
+ StringComparison = stringComparison;
+ }
+
+ ///
+ /// Gets the root rule of this parse context
+ ///
+ public Rule RootRule { get; }
+
+ ///
+ /// Gets or sets the input text in lines
+ ///
+ public string[] Input { get; internal set; }
+
+ ///
+ /// Gets the matcher used in this parse context
+ ///
+ public Matcher Matcher { get; }
+
+ ///
+ /// Gets the string comparison mode
+ ///
+ public StringComparison StringComparison { get; }
+ }
+}
diff --git a/UserInterfaces/AnyText/ParsePosition.cs b/UserInterfaces/AnyText/ParsePosition.cs
new file mode 100644
index 00000000..bf4155ca
--- /dev/null
+++ b/UserInterfaces/AnyText/ParsePosition.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// Denotes a position of a parser
+ ///
+ /// The line of the position
+ /// The column of the position
+ public record struct ParsePosition(int Line, int Col) : IComparable
+ {
+ ///
+ /// Proceeds the position by the given number of characters
+ ///
+ /// the numbers of characters to proceed
+ /// The updated parser position
+ public ParsePosition Proceed(int chars)
+ {
+ return new ParsePosition(Line, Col + chars);
+ }
+
+ ///
+ public int CompareTo(ParsePosition other)
+ {
+ var ret = Line.CompareTo(other.Line);
+ if (ret == 0) ret = Col.CompareTo(other.Col);
+ return ret;
+ }
+
+ ///
+ /// Decides which of the two positions is smaller
+ ///
+ /// the first position
+ /// the second position
+ /// true, if the first position is smaller, otherwise false
+ public static bool operator <(ParsePosition pos1, ParsePosition pos2)
+ {
+ return pos1.CompareTo(pos2) < 0;
+ }
+
+ ///
+ /// Decides which of the two positions is greater
+ ///
+ /// the first position
+ /// the second position
+ /// true, if the first position is greater, otherwise false
+ public static bool operator >(ParsePosition pos1, ParsePosition pos2)
+ {
+ return pos1.CompareTo(pos2) > 0;
+ }
+
+ ///
+ /// Subtracts two parse positions
+ ///
+ /// the forward position
+ /// the backward position
+ /// The delta between the given positions
+ public static ParsePositionDelta operator -(ParsePosition to, ParsePosition from)
+ {
+ if (from.Line == to.Line)
+ {
+ return new ParsePositionDelta(0, to.Col - from.Col);
+ }
+ return new ParsePositionDelta(to.Line - from.Line, to.Col);
+ }
+
+ ///
+ /// Adds the given delta to the current position
+ ///
+ /// the origin position
+ /// the position delta
+ /// the updated position
+ public static ParsePosition operator +(ParsePosition pos, ParsePositionDelta delta)
+ {
+ if (delta.Line == 0)
+ {
+ return new ParsePosition(pos.Line, pos.Col + delta.Col);
+ }
+ return new ParsePosition(pos.Line + delta.Line, delta.Col);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/ParsePositionDelta.cs b/UserInterfaces/AnyText/ParsePositionDelta.cs
new file mode 100644
index 00000000..c8f455cb
--- /dev/null
+++ b/UserInterfaces/AnyText/ParsePositionDelta.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// Denotes a delta between parser positions
+ ///
+ /// the line delta
+ /// the column delta
+ public record struct ParsePositionDelta(int Line, int Col)
+ {
+ ///
+ /// Calculates the larger of two diffs
+ ///
+ /// the first delta
+ /// the second delta
+ /// the larger delta of the two deltas
+ public static ParsePositionDelta Larger(ParsePositionDelta delta1, ParsePositionDelta delta2)
+ {
+ if (delta1.Line > delta2.Line) { return delta1; }
+ if (delta1.Line < delta2.Line) { return delta2; }
+ return delta1.Col > delta2.Col ? delta1 : delta2;
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Parser.cs b/UserInterfaces/AnyText/Parser.cs
new file mode 100644
index 00000000..ce067a46
--- /dev/null
+++ b/UserInterfaces/AnyText/Parser.cs
@@ -0,0 +1,61 @@
+using NMF.AnyText.Rules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// Denotes an incremental parser system
+ ///
+ public class Parser
+ {
+ private readonly Matcher _matcher;
+ private readonly ParseContext _context;
+ private RuleApplication _ruleApplication;
+
+ ///
+ /// Creates a new parser system
+ ///
+ /// the root rule
+ /// the string comparison mode
+ public Parser(Rule root, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase)
+ {
+ _matcher = new Matcher();
+ _context = new ParseContext(root, _matcher, stringComparison);
+ }
+
+ ///
+ /// Initializes the parser system
+ ///
+ /// the initial input
+ /// the value parsed for the given input
+ public object Initialize(string[] input)
+ {
+ _context.Input = input;
+ _ruleApplication = _matcher.Match(_context);
+ _ruleApplication.Activate(_context);
+ return _ruleApplication.GetValue(_context);
+ }
+
+ public object Update(IEnumerable edits)
+ {
+ var input = _context.Input;
+ foreach (TextEdit edit in edits)
+ {
+ input = edit.Apply(input);
+ _matcher.RemoveMemoizedRuleApplications(edit);
+ }
+ _context.Input = input;
+ var newRoot = _matcher.Match(_context);
+ if (newRoot.IsPositive)
+ {
+ _ruleApplication = newRoot.ApplyTo(_ruleApplication, _context);
+ _ruleApplication.Activate(_context);
+ }
+ return _ruleApplication.GetValue(_context);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/ChoiceRule.cs b/UserInterfaces/AnyText/Rules/ChoiceRule.cs
new file mode 100644
index 00000000..be5c5a28
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/ChoiceRule.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a choice of multiple alternative rules
+ ///
+ public class ChoiceRule : Rule
+ {
+ ///
+ /// Gets or sets the alternatives
+ ///
+ public Rule[] Alternatives { get; set; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var savedPosition = position;
+ var examined = new ParsePositionDelta();
+ foreach (var rule in Alternatives)
+ {
+ var match = context.Matcher.MatchCore(rule, context, ref position);
+ examined = ParsePositionDelta.Larger(examined, match.ExaminedTo);
+ if (match.IsPositive)
+ {
+ return new SingleRuleApplication(this, match, match.Length, examined);
+ }
+ position = savedPosition;
+ }
+ return new FailedRuleApplication(this, examined, position, "No viable choice");
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/FailedRuleApplication.cs b/UserInterfaces/AnyText/Rules/FailedRuleApplication.cs
new file mode 100644
index 00000000..f89d1284
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/FailedRuleApplication.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule application that failed
+ ///
+ internal class FailedRuleApplication : RuleApplication
+ {
+ ///
+ /// Creates a new failed rule application
+ ///
+ /// the rule that failed
+ /// the amount of text that was analyzed to draw the conclusion
+ /// The position of the error
+ /// the message to indicate why the rule application failed
+ public FailedRuleApplication(Rule rule, ParsePositionDelta examinedTo, ParsePosition errorPosition, string message) : base(rule, default, examinedTo)
+ {
+ Message = message;
+ ErrorPosition = errorPosition;
+ }
+
+ ///
+ /// Gets the message to indicate why the rule application failed
+ ///
+ public override string Message { get; }
+
+ ///
+ /// Gets the position of the error
+ ///
+ public override ParsePosition ErrorPosition { get; }
+
+ ///
+ public override bool IsPositive => false;
+
+ ///
+ public override RuleApplication ApplyTo(RuleApplication other, ParseContext context)
+ {
+ return this;
+ }
+
+ ///
+ public override object GetValue(ParseContext context)
+ {
+ return null;
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/LiteralRule.cs b/UserInterfaces/AnyText/Rules/LiteralRule.cs
new file mode 100644
index 00000000..220a42bc
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/LiteralRule.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule that matches a constant text
+ ///
+ public class LiteralRule : Rule
+ {
+ ///
+ /// Creates a new instance
+ ///
+ /// the literal that should be matched
+ public LiteralRule(string literal)
+ {
+ Literal = literal;
+ }
+
+ ///
+ /// Gets the literal that should be matched
+ ///
+ public string Literal { get; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ if (position.Line >= context.Input.Length)
+ {
+ return new FailedRuleApplication(this, default, position, Literal);
+ }
+ var line = context.Input[position.Line];
+ if (line.Length < position.Col + Literal.Length)
+ {
+ return new FailedRuleApplication(this, new ParsePositionDelta(0, Literal.Length), position, Literal);
+ }
+
+ if (MemoryExtensions.Equals(Literal, line.AsSpan(position.Col, Literal.Length), context.StringComparison))
+ {
+ position = position.Proceed(Literal.Length);
+ return new LiteralRuleApplication(this, Literal, new ParsePositionDelta(0, Literal.Length), new ParsePositionDelta(0, Literal.Length));
+ }
+
+ return new FailedRuleApplication(this, new ParsePositionDelta(0, Literal.Length), position, Literal);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/LiteralRuleApplication.cs b/UserInterfaces/AnyText/Rules/LiteralRuleApplication.cs
new file mode 100644
index 00000000..e7ea4fe9
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/LiteralRuleApplication.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule application that matches a given literal string
+ ///
+ internal class LiteralRuleApplication : RuleApplication
+ {
+ public LiteralRuleApplication(Rule rule, string literal, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, endsAt, examinedTo)
+ {
+ Literal = literal;
+ }
+
+ public string Literal { get; private set; }
+
+ public override RuleApplication ApplyTo(RuleApplication other, ParseContext context)
+ {
+ return other.MigrateTo(this, context);
+ }
+
+ internal override RuleApplication MigrateTo(LiteralRuleApplication literal, ParseContext context)
+ {
+ var old = Literal;
+ Literal = literal.Literal;
+ OnMigrate(old, Literal, context);
+ return this;
+ }
+
+ protected virtual void OnMigrate(string oldValue, string newValue, ParseContext context)
+ {
+ if (oldValue != newValue)
+ {
+ OnValueChange(this, context);
+ }
+ }
+
+ public override object GetValue(ParseContext context)
+ {
+ return Literal;
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/MultiRuleApplication.cs b/UserInterfaces/AnyText/Rules/MultiRuleApplication.cs
new file mode 100644
index 00000000..0872e542
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/MultiRuleApplication.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ internal class MultiRuleApplication : RuleApplication
+ {
+
+ public MultiRuleApplication(Rule rule, List inner, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, endsAt, examinedTo)
+ {
+ Inner = inner;
+ }
+
+ public List Inner { get; }
+
+ public override void Activate(ParseContext context)
+ {
+ base.Activate(context);
+ foreach (var inner in Inner)
+ {
+ inner.Parent = this;
+ if (!inner.IsActive)
+ {
+ inner.Activate(context);
+ }
+ }
+ }
+
+ public override RuleApplication ApplyTo(RuleApplication other, ParseContext context)
+ {
+ return other.MigrateTo(this, context);
+ }
+
+ public override void Deactivate(ParseContext context)
+ {
+ foreach (var inner in Inner)
+ {
+ inner.Parent = null;
+ if (inner.IsActive)
+ {
+ inner.Deactivate(context);
+ }
+ }
+ base.Deactivate(context);
+ }
+
+ internal override RuleApplication MigrateTo(MultiRuleApplication multiRule, ParseContext context)
+ {
+ var removed = new List();
+ var added = new List();
+ var tailOffset = multiRule.Inner.Count - Inner.Count;
+ int firstDifferentIndex = CalculateFirstDifferentIndex(multiRule);
+ int lastDifferentIndex = CalculateLastDifferentIndex(multiRule, tailOffset);
+
+ for (int i = firstDifferentIndex; i <= lastDifferentIndex; i++)
+ {
+ if (i < multiRule.Inner.Count)
+ {
+ var old = Inner[i];
+ var newApp = multiRule.Inner[i].ApplyTo(old, context);
+ Inner[i] = newApp;
+ if (old != newApp && old.IsActive)
+ {
+ old.Deactivate(context);
+ newApp.Activate(context);
+ }
+ }
+ else
+ {
+ removed.Add(Inner[i]);
+ Inner[i].Deactivate(context);
+ Inner.RemoveAt(i);
+ }
+ }
+ for (int i = 1; i <= tailOffset; i++)
+ {
+ var item = multiRule.Inner[lastDifferentIndex + i];
+ added.Add(item);
+ if (IsActive)
+ {
+ item.Activate(context);
+ }
+ Inner.Insert(lastDifferentIndex + i, item);
+ }
+ OnMigrate(removed, added);
+ return this;
+ }
+
+ protected virtual void OnMigrate(List removed, List added) { }
+
+ private int CalculateLastDifferentIndex(MultiRuleApplication multiRule, int tailOffset)
+ {
+ var lastIndex = Inner.Count - 1;
+ while (lastIndex > 0 && Inner[lastIndex] == multiRule.Inner[lastIndex + tailOffset])
+ {
+ lastIndex--;
+ }
+
+ return lastIndex;
+ }
+
+ private int CalculateFirstDifferentIndex(MultiRuleApplication multiRule)
+ {
+ var index = 0;
+ while (index < Inner.Count && index < multiRule.Inner.Count && Inner[index] == multiRule.Inner[index])
+ {
+ index++;
+ }
+
+ return index;
+ }
+
+ public override object GetValue(ParseContext context)
+ {
+ return Inner.Select(app => app.GetValue(context));
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/OneOrMoreRule.cs b/UserInterfaces/AnyText/Rules/OneOrMoreRule.cs
new file mode 100644
index 00000000..5c878143
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/OneOrMoreRule.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule that is matched at least once
+ ///
+ public class OneOrMoreRule : Rule
+ {
+ ///
+ /// Gets or sets the inner rule
+ ///
+ public Rule InnerRule { get; set; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var savedPosition = position;
+ var attempt = context.Matcher.MatchCore(InnerRule, context, ref position);
+ if (!attempt.IsPositive)
+ {
+ position = savedPosition;
+ return new FailedRuleApplication(this, attempt.ExaminedTo, attempt.ErrorPosition, attempt.Message);
+ }
+ var applications = new List();
+ var examined = attempt.ExaminedTo;
+ RuleHelper.Star(context, InnerRule, applications, ref position, ref examined);
+ return new MultiRuleApplication(this, applications, position - savedPosition, examined);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/QuoteRule.cs b/UserInterfaces/AnyText/Rules/QuoteRule.cs
new file mode 100644
index 00000000..7b0338e2
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/QuoteRule.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule that delegates to an inner rule without changes
+ ///
+ public class QuoteRule : Rule
+ {
+ ///
+ /// Gets or sets the inner rule
+ ///
+ public Rule Inner { get; set; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var app = context.Matcher.MatchCore(Inner, context, ref position);
+ if (app.IsPositive)
+ {
+ return CreateRuleApplication(app, context);
+ }
+ return new FailedRuleApplication(this, app.ExaminedTo, app.ErrorPosition, app.Message);
+ }
+
+ ///
+ /// Creates the rule application for this rule
+ ///
+ /// the inner rule application
+ /// the parse context
+ /// the new rule application
+ protected virtual RuleApplication CreateRuleApplication(RuleApplication app, ParseContext context)
+ {
+ return new SingleRuleApplication(this, app, app.Length, app.ExaminedTo);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/RegexRule.cs b/UserInterfaces/AnyText/Rules/RegexRule.cs
new file mode 100644
index 00000000..fc8dc3b8
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/RegexRule.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule that parses text based on regular expressions
+ ///
+ /// Regular expressions are always restricted to a single line, only
+ public class RegexRule : Rule
+ {
+ ///
+ /// Gets or sets the regular expression
+ ///
+ public Regex Regex { get; set; }
+
+ private const string RegexFailed = "Regular expression did not match";
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ if (position.Line >= context.Input.Length)
+ {
+ return new FailedRuleApplication(this, default, position, RegexFailed);
+ }
+ var line = context.Input[position.Line];
+ var match = Regex.Match(line.Substring(position.Col));
+ if (match.Success)
+ {
+ position = position.Proceed(match.Length);
+ return CreateRuleApplication(match.Value, position, new ParsePositionDelta(0, match.Length), new ParsePositionDelta(0, line.Length - position.Col + 1), context);
+ }
+ else
+ {
+ return new FailedRuleApplication(this, new ParsePositionDelta(0, line.Length - position.Col + 1), position, RegexFailed);
+ }
+ }
+
+ ///
+ /// Creates a new rule application
+ ///
+ /// the matched string content
+ /// the position where the rule matched
+ /// the length of the match
+ /// the examined length of text
+ /// the parse context
+ /// a rule application
+ public virtual RuleApplication CreateRuleApplication(string matched, ParsePosition position, ParsePositionDelta length, ParsePositionDelta examined, ParseContext context)
+ {
+ return new LiteralRuleApplication(this, matched, length, examined);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/Rule.cs b/UserInterfaces/AnyText/Rules/Rule.cs
new file mode 100644
index 00000000..063235cc
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/Rule.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule for parsing rules
+ ///
+ public abstract class Rule
+ {
+ ///
+ /// Matches the the context at the provided position
+ ///
+ /// the context in which the rule is matched
+ /// the position in the input
+ /// the rule application for the provided position
+ public abstract RuleApplication Match(ParseContext context, ref ParsePosition position);
+
+ ///
+ /// Gets called when a rule application is activated
+ ///
+ /// the rule application that is activated
+ /// the context in which the rule application is activated
+ protected internal virtual void OnActivate(RuleApplication application, ParseContext context) { }
+
+ ///
+ /// Gets called when a rule application is deactivated
+ ///
+ /// the rule application that is deactivated
+ /// the context in which the rule application is deactivated
+ protected internal virtual void OnDeactivate(RuleApplication application, ParseContext context) { }
+
+ ///
+ /// Gets called when the value of a rule application changes
+ ///
+ /// the rule application for which the value changed
+ /// the context in which the value changed
+ /// true, if the rule processed the value change, otherwise false (in which case the value change is propagated)
+ protected internal virtual bool OnValueChange(RuleApplication application, ParseContext context) => false;
+
+ ///
+ /// True, if the rule permits trailing whitespaces, otherwise false
+ ///
+ public virtual bool TrailingWhitespaces => true;
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/RuleApplication.cs b/UserInterfaces/AnyText/Rules/RuleApplication.cs
new file mode 100644
index 00000000..e441d2c6
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/RuleApplication.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes the application of a rule
+ ///
+ public abstract class RuleApplication
+ {
+ ///
+ /// Creates a new instance
+ ///
+ /// the rule that was matched
+ /// the length of the rule application
+ /// the amount of text that was analyzed to come to the conclusion of this rule application
+ ///
+ protected RuleApplication(Rule rule, ParsePositionDelta length, ParsePositionDelta examinedTo)
+ {
+ if (length.Line < 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ Rule = rule;
+ Length = length;
+ ExaminedTo = examinedTo;
+ }
+
+ ///
+ /// True, if the rule application was successful, otherwise false
+ ///
+ public virtual bool IsPositive => true;
+
+ ///
+ /// Gets the element that denotes the context for this rule application
+ ///
+ public virtual object ContextElement => Parent?.ContextElement;
+
+ ///
+ /// Gets the parsed value under the given context
+ ///
+ /// the parse context
+ /// the parsed value
+ public abstract object GetValue(ParseContext context);
+
+ ///
+ /// The rule that was matched
+ ///
+ public Rule Rule { get; }
+
+ ///
+ /// the length of the rule application
+ ///
+ public ParsePositionDelta Length { get; }
+
+ ///
+ /// the amount of text that was analyzed to come to the conclusion of this rule application
+ ///
+ public ParsePositionDelta ExaminedTo { get; }
+
+ ///
+ /// True, if the rule application is part of the current parse tree
+ ///
+ public bool IsActive { get; private set; }
+
+ ///
+ /// Activates the rule application, i.e. marks it as part of the current parse tree
+ ///
+ /// the context in which the parse tree exists
+ public virtual void Activate(ParseContext context)
+ {
+ if (!IsActive)
+ {
+ IsActive = true;
+ Rule.OnActivate(this, context);
+ }
+ }
+
+ ///
+ /// Deactivates the rule application, i.e. unmarks it as part of the parse tree
+ ///
+ /// the context in which the parse tree exists
+ public virtual void Deactivate(ParseContext context)
+ {
+ if (IsActive)
+ {
+ IsActive = false;
+ Rule.OnDeactivate(this, context);
+ }
+ }
+
+ ///
+ /// Gets the parent rule application in the parse tree
+ ///
+ public RuleApplication Parent { get; internal set; }
+
+ ///
+ /// Gets the position of the error
+ ///
+ public virtual ParsePosition ErrorPosition => default;
+
+ ///
+ /// Gets the message to indicate why the rule application failed
+ ///
+ public virtual string Message => null;
+
+ ///
+ /// Gets called when the value of the given rule application changes
+ ///
+ /// the changed rule application (either this or a child in the parse tree)
+ /// the parse context
+ protected internal virtual void OnValueChange(RuleApplication changedChild, ParseContext context)
+ {
+ if (!Rule.OnValueChange(this, context))
+ {
+ Parent?.OnValueChange(this, context);
+ }
+ }
+
+ ///
+ /// Applies the structure of the current rule application to the given other rule application
+ ///
+ /// the rule application to which the rule should be applied
+ /// the parse context
+ /// the merged rule application
+ public abstract RuleApplication ApplyTo(RuleApplication other, ParseContext context);
+
+ internal virtual RuleApplication MigrateTo(LiteralRuleApplication literal, ParseContext context) => throw new NotSupportedException("Cannot migrate to literals");
+
+ internal virtual RuleApplication MigrateTo(MultiRuleApplication multiRule, ParseContext context) => throw new NotSupportedException("Cannot migrate to multi");
+
+ internal virtual RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context) => throw new NotSupportedException("Cannot migrate to single");
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/RuleHelper.cs b/UserInterfaces/AnyText/Rules/RuleHelper.cs
new file mode 100644
index 00000000..ce68252d
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/RuleHelper.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ internal static class RuleHelper
+ {
+ public static void MoveOverWhitespace(ParseContext context, ref ParsePosition position)
+ {
+ var lineNo = position.Line;
+ var col = position.Col;
+ while (lineNo < context.Input.Length)
+ {
+ var line = context.Input[lineNo];
+
+ while (col < line.Length)
+ {
+ if (!char.IsWhiteSpace(line[col]))
+ {
+ position = new ParsePosition(lineNo, col);
+ return;
+ }
+ col++;
+ }
+ lineNo++;
+ col = 0;
+ }
+ position = new ParsePosition(lineNo, 0);
+ }
+
+ public static void Star(ParseContext context, Rule rule, List applications, ref ParsePosition position, ref ParsePositionDelta examined)
+ {
+ var savedPosition = position;
+ while (true)
+ {
+ var app = context.Matcher.MatchCore(rule, context, ref position);
+ if (app != null)
+ {
+ applications.Add(app);
+ savedPosition = position;
+ examined = ParsePositionDelta.Larger(examined, app.ExaminedTo);
+ }
+ else
+ {
+ break;
+ }
+ }
+ position = savedPosition;
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/SequenceRule.cs b/UserInterfaces/AnyText/Rules/SequenceRule.cs
new file mode 100644
index 00000000..6eaf5b4e
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/SequenceRule.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes a rule that other rules occur in sequence
+ ///
+ public class SequenceRule : Rule
+ {
+ ///
+ /// The rules that should occur in sequence
+ ///
+ public Rule[] Rules { get; set; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var savedPosition = position;
+ var applications = new List();
+ var examined = new ParsePositionDelta();
+ foreach (var rule in Rules)
+ {
+ var app = context.Matcher.MatchCore(rule, context, ref position);
+ examined = ParsePositionDelta.Larger(examined, app.ExaminedTo);
+ if (app.IsPositive)
+ {
+ applications.Add(app);
+ }
+ else
+ {
+ position = savedPosition;
+ return new FailedRuleApplication(this, examined, app.ErrorPosition, app.Message);
+ }
+ }
+ return CreateRuleApplication(applications, position - savedPosition, examined);
+ }
+
+ ///
+ /// Creates a rule application for a success
+ ///
+ /// the inner list of rule applications
+ /// the length of the match
+ /// the amount of text examined
+ /// a new rule application
+ protected virtual RuleApplication CreateRuleApplication(List inner, ParsePositionDelta length, ParsePositionDelta examined)
+ {
+ return new MultiRuleApplication(this, inner, length, examined);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/SingleRuleApplication.cs b/UserInterfaces/AnyText/Rules/SingleRuleApplication.cs
new file mode 100644
index 00000000..d7a1b94b
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/SingleRuleApplication.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ internal class SingleRuleApplication : RuleApplication
+ {
+ public SingleRuleApplication(Rule rule, RuleApplication inner, ParsePositionDelta endsAt, ParsePositionDelta examinedTo) : base(rule, endsAt, examinedTo)
+ {
+ Inner = inner;
+ }
+
+ public RuleApplication Inner { get; private set; }
+
+ public override void Activate(ParseContext context)
+ {
+ base.Activate(context);
+ if (Inner != null && !Inner.IsActive)
+ {
+ Inner.Parent = this;
+ Inner.Activate(context);
+ }
+ }
+
+ public override RuleApplication ApplyTo(RuleApplication other, ParseContext context)
+ {
+ return other.MigrateTo(this, context);
+ }
+
+ public override void Deactivate(ParseContext context)
+ {
+ if (Inner != null && Inner.IsActive )
+ {
+ Inner.Parent = null;
+ Inner.Deactivate(context);
+ }
+ base.Deactivate(context);
+ }
+
+ internal override RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context)
+ {
+ var old = Inner;
+ if (old.Rule == singleRule.Inner.Rule)
+ {
+ Inner = singleRule.Inner.ApplyTo(Inner, context);
+ }
+ else
+ {
+ Inner = singleRule.Inner;
+ }
+ if (old != Inner)
+ {
+ OnMigrate(old, Inner, context);
+ }
+ return this;
+ }
+
+ protected virtual void OnMigrate(RuleApplication oldValue, RuleApplication newValue, ParseContext context)
+ {
+ if (oldValue.IsActive)
+ {
+ oldValue.Deactivate(context);
+ newValue.Activate(context);
+ OnValueChange(this, context);
+ }
+ }
+
+ public override object GetValue(ParseContext context)
+ {
+ return Inner?.GetValue(context);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/ZeroOrMoreRule.cs b/UserInterfaces/AnyText/Rules/ZeroOrMoreRule.cs
new file mode 100644
index 00000000..ff994125
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/ZeroOrMoreRule.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes that another rule can occur an arbitrary number of times
+ ///
+ public class ZeroOrMoreRule : Rule
+ {
+ ///
+ /// Gets or sets the inner rule
+ ///
+ public Rule InnerRule { get; set; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var savedPosition = position;
+ var applications = new List();
+ var examined = new ParsePositionDelta();
+ RuleHelper.Star(context, InnerRule, applications, ref position, ref examined);
+ return new MultiRuleApplication(this, applications, position - savedPosition, examined);
+ }
+
+ }
+}
diff --git a/UserInterfaces/AnyText/Rules/ZeroOrOneRule.cs b/UserInterfaces/AnyText/Rules/ZeroOrOneRule.cs
new file mode 100644
index 00000000..e202afde
--- /dev/null
+++ b/UserInterfaces/AnyText/Rules/ZeroOrOneRule.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Rules
+{
+ ///
+ /// Denotes that a rule that can be matched at most once
+ ///
+ public class ZeroOrOneRule : Rule
+ {
+ ///
+ /// The inner rule
+ ///
+ public Rule InnerRule { get; }
+
+ ///
+ public override RuleApplication Match(ParseContext context, ref ParsePosition position)
+ {
+ var savedPosition = position;
+ var attempt = context.Matcher.MatchCore(InnerRule, context, ref position);
+ if (!attempt.IsPositive)
+ {
+ position = savedPosition;
+ }
+ return new SingleRuleApplication(this, attempt, attempt.IsPositive ? attempt.Length : default, attempt.ExaminedTo);
+ }
+ }
+}
diff --git a/UserInterfaces/AnyText/TextEdit.cs b/UserInterfaces/AnyText/TextEdit.cs
new file mode 100644
index 00000000..63610fd1
--- /dev/null
+++ b/UserInterfaces/AnyText/TextEdit.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText
+{
+ ///
+ /// Denotes an edit for text
+ ///
+ public class TextEdit
+ {
+ private static readonly string[] EmptyString = { string.Empty };
+
+ ///
+ /// Creates a new text edit
+ ///
+ /// the start of the edit
+ /// the end of the edit
+ /// the new text inserted between start and end
+ /// thrown if start is after end
+ public TextEdit(ParsePosition start, ParsePosition end, string[] newText)
+ {
+ if (start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col))
+ {
+ throw new ArgumentException("End cannot be before start");
+ }
+ Start = start;
+ End = end;
+ NewText = newText;
+ if (newText == null || newText.Length == 0)
+ {
+ NewText = EmptyString;
+ }
+ }
+
+ ///
+ /// Gets the start of the edit
+ ///
+ public ParsePosition Start { get; }
+
+ ///
+ /// Gets the end of the edit
+ ///
+ public ParsePosition End { get; }
+
+ ///
+ /// Gets the new text inserted between start and end
+ ///
+ public string[] NewText { get; }
+
+ ///
+ /// Applies the text edit to the given input
+ ///
+ /// an input array of string lines
+ /// a text array including the changes
+ public string[] Apply(string[] input)
+ {
+ if (Start.Line == End.Line && NewText.Length == 1)
+ {
+ return ApplyInlineChange(input);
+ }
+ if (input.Length == End.Line - Start.Line + NewText.Length - 1)
+ {
+ return ApplyInplaceChange(input);
+ }
+ return ApplyReconstructArray(input);
+ }
+
+ private string[] ApplyReconstructArray(string[] input)
+ {
+ var newArray = new string[input.Length + End.Line - Start.Line + NewText.Length - 1];
+ Array.Copy(input, 0, newArray, 0, Start.Line);
+ newArray[Start.Line] = input[Start.Line].Substring(0, Start.Col) + NewText[0];
+ if (NewText.Length > 2)
+ {
+ Array.Copy(NewText, 1, newArray, Start.Line + 1, NewText.Length - 2);
+ }
+ newArray[End.Line] = NewText[NewText.Length - 1] + input[End.Line].Substring(End.Col);
+ Array.Copy(input, End.Line + 1, newArray, Start.Line + NewText.Length, input.Length - End.Line);
+ return newArray;
+ }
+
+ private string[] ApplyInplaceChange(string[] input)
+ {
+ input[Start.Line] = ChangeLine(input[Start.Line], Start.Col, int.MaxValue, NewText[0]);
+ for (int i = 1; i < NewText.Length - 1; i++)
+ {
+ input[Start.Line + i] = ChangeLine(input[Start.Line + i], 0, int.MaxValue, NewText[i]);
+ }
+ input[End.Line] = ChangeLine(input[End.Line], 0, End.Col, NewText[End.Line]);
+ return input;
+ }
+
+ private string[] ApplyInlineChange(string[] input)
+ {
+ // inline change, most common case
+ input[Start.Line] = ChangeLine(input[Start.Line], Start.Col, End.Col, NewText.Length == 1 ? NewText[0] : string.Empty);
+ return input;
+ }
+
+ private static string ChangeLine(string line, int start, int end, string newText)
+ {
+ string result = string.Empty;
+ if (start > 0)
+ {
+ result = line.Substring(0, start);
+ }
+ if (!string.IsNullOrEmpty(newText))
+ {
+ result += newText;
+ }
+ if (end < line.Length - 1)
+ {
+ result += line.Substring(end);
+ }
+ return result;
+ }
+ }
+}
diff --git a/UserInterfaces/Tests/AnyText.Tests/AnyText.Tests.csproj b/UserInterfaces/Tests/AnyText.Tests/AnyText.Tests.csproj
new file mode 100644
index 00000000..c98e6d91
--- /dev/null
+++ b/UserInterfaces/Tests/AnyText.Tests/AnyText.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UserInterfaces/Tests/AnyText.Tests/ExpressionGrammarTests.cs b/UserInterfaces/Tests/AnyText.Tests/ExpressionGrammarTests.cs
new file mode 100644
index 00000000..d411d017
--- /dev/null
+++ b/UserInterfaces/Tests/AnyText.Tests/ExpressionGrammarTests.cs
@@ -0,0 +1,216 @@
+using NMF.AnyText.Model;
+using NMF.AnyText.Rules;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace NMF.AnyText.Tests
+{
+ [TestFixture]
+ public partial class ExpressionGrammarTests
+ {
+ public abstract class Expr { }
+
+ public class BinExpr : Expr
+ {
+ public Expr Left { get; set; }
+ public Expr Right { get; set; }
+
+ public BinOp Op { get; set; }
+ }
+
+ public enum BinOp
+ {
+ Add,
+ Mul
+ }
+
+ public class Lit : Expr
+ {
+ public int Val { get; set; }
+ }
+
+ private class AddRule : ModelElementRule
+ {
+ protected override BinExpr CreateElement(IEnumerable inner)
+ {
+ return new BinExpr { Op = BinOp.Add };
+ }
+ }
+
+ private class MulRule : ModelElementRule
+ {
+ protected override BinExpr CreateElement(IEnumerable inner)
+ {
+ return new BinExpr { Op = BinOp.Mul };
+ }
+ }
+
+ private class ConvertToLitRule : ConvertRule
+ {
+ public override Lit Convert(string text, ParseContext context)
+ {
+ return new Lit { Val = int.Parse(text) };
+ }
+ }
+
+ private class AssignLeft : AssignRule
+ {
+ protected override void OnChangeValue(BinExpr semanticElement, Expr propertyValue, ParseContext context)
+ {
+ semanticElement.Left = propertyValue;
+ }
+ }
+
+ private class AssignRight : AssignRule
+ {
+ protected override void OnChangeValue(BinExpr semanticElement, Expr propertyValue, ParseContext context)
+ {
+ semanticElement.Right = propertyValue;
+ }
+ }
+
+ private static Rule CreateParseRule()
+ {
+ var expr = new ChoiceRule();
+ var add = new AddRule();
+ var multiply = new MulRule();
+ var factor = new ChoiceRule();
+ var summand = new ChoiceRule();
+ var lit = new ConvertToLitRule();
+ var parantheses = new ParanthesesRule();
+
+ expr.Alternatives = new Rule[]
+ {
+ add,
+ summand
+ };
+ add.Rules = new Rule[]
+ {
+ new AssignLeft { Inner = summand },
+ new LiteralRule("+"),
+ new AssignRight { Inner = summand },
+ };
+ multiply.Rules = new Rule[]
+ {
+ new AssignLeft { Inner = factor },
+ new LiteralRule("*"),
+ new AssignRight { Inner = factor },
+ };
+ factor.Alternatives = new Rule[]
+ {
+ lit,
+ parantheses
+ };
+ summand.Alternatives = new Rule[]
+ {
+ multiply,
+ factor
+ };
+ lit.Regex = LitRegex();
+ parantheses.Rules = new Rule[]
+ {
+ new LiteralRule("("),
+ expr,
+ new LiteralRule(")")
+ };
+
+ return expr;
+ }
+
+ [GeneratedRegex("^\\d+", RegexOptions.Compiled)]
+ private static partial Regex LitRegex();
+
+ [Test]
+ public void Parser_CanParseSimpleArithmetics()
+ {
+ var parser = new Parser(CreateParseRule());
+ Assert.IsNotNull(parser.Initialize(new string[] { "1 + 2 * (3 + 4)" }));
+ }
+
+ [Test]
+ public void Parser_CreatesCorrectModel()
+ {
+ var parser = new Parser(CreateParseRule());
+ var expr = parser.Initialize(new string[] { "1 + 2 * (3 + 4)" });
+ Assert.That(expr, Is.InstanceOf());
+ var bin1 = (BinExpr)expr;
+ AssertLit(1, bin1.Left);
+ Assert.That(bin1.Op, Is.EqualTo(BinOp.Add));
+ Assert.That(bin1.Right, Is.InstanceOf());
+ var bin2 = (BinExpr)bin1.Right;
+ AssertLit(2, bin2.Left);
+ Assert.That(bin2.Op, Is.EqualTo(BinOp.Mul));
+ Assert.That(bin2.Right, Is.InstanceOf());
+ var bin3 = (BinExpr)bin2.Right;
+ AssertLit(3, bin3.Left);
+ AssertLit(4, bin3.Right);
+ Assert.That(bin3.Op, Is.EqualTo(BinOp.Add));
+ }
+
+ [Test]
+ public void Parser_ProcessesDeleteCorrectly()
+ {
+ var parser = new Parser(CreateParseRule());
+ var expr1 = parser.Initialize(new string[] { "1 + 2 * (3 + 4)" });
+ var expr2 = parser.Update(new[]
+ {
+ new TextEdit(new ParsePosition(0, 1), new ParsePosition(0,4), new string[] { "5" })
+ });
+ Assert.That(expr1, Is.Not.EqualTo(expr2));
+ Assert.That(expr2, Is.InstanceOf());
+ var bin1 = (BinExpr)expr2;
+ AssertLit(152, bin1.Left);
+ Assert.That(bin1.Right, Is.InstanceOf());
+ }
+
+ [Test]
+ public void Parser_ReusesElements()
+ {
+ var parser = new Parser(CreateParseRule());
+ var expr1 = parser.Initialize(new string[] { "1 + 2 * (3 + 4)" });
+ var expr2 = parser.Update(new[]
+ {
+ new TextEdit(new ParsePosition(0, 10), new ParsePosition(0,13), new string[] { "5" })
+ });
+ Assert.That(expr1, Is.EqualTo(expr2));
+ Assert.That(expr2, Is.InstanceOf());
+ var bin1 = (BinExpr)expr2;
+ AssertLit(1, bin1.Left);
+ Assert.That(bin1.Right, Is.InstanceOf());
+ var bin2 = (BinExpr)bin1.Right;
+ AssertLit(354, bin2.Right);
+ }
+
+ [Test]
+ public void Parser_RefusesIncorrectInput()
+ {
+ var parser = new Parser(CreateParseRule());
+ var expr = parser.Initialize(new string[] { "1 + 2 * (3 + 4" });
+ Assert.That(expr, Is.Null);
+ }
+
+ [Test]
+ public void Parser_KeepsPreviousAfterFailingChange()
+ {
+ var parser = new Parser(CreateParseRule());
+ var expr = parser.Initialize(new string[] { "1 + 2 * (3 + 4)" });
+ Assert.That(expr, Is.Not.Null);
+ var expr2 = parser.Update(new[]
+ {
+ new TextEdit(new ParsePosition(0,4), new ParsePosition(0,15), null)
+ });
+ Assert.That(expr, Is.EqualTo(expr2));
+ }
+
+ private static void AssertLit(int lit, Expr actual)
+ {
+ Assert.That(actual, Is.InstanceOf());
+ Assert.That(((Lit)actual).Val, Is.EqualTo(lit));
+ }
+ }
+}