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)); + } + } +}