diff --git a/docs/parsers.md b/docs/parsers.md index 220445ae..720aaaf2 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -21,7 +21,9 @@ Terms and Literals are accessed using the `Terms` and `Literals` properties from ### WhiteSpace -Matches blank spaces, optionally including new lines. Returns a `TextSpan` with the matched spaces. This parser is not available in the `Terms` static class. +#### Literals.WhiteSpace() + +Matches blank spaces, optionally including new lines. Returns a `TextSpan` with the matched spaces. ```c# Parser WhiteSpace(bool includeNewLines = false) @@ -40,6 +42,26 @@ Result: " \t" ``` +#### Terms.WhiteSpace() + +Matches the `WhiteSpaceParser` configured in the current context. + +```c# +Parser WhiteSpace() +``` + +When used from `Terms` it parses whitespace (or comments) as defined in the `ParseContext` or from `WithWhiteSpaceParser(parser)`. + +When used from `Literals` it parses standard white spaces. + +Usage: + +```c# +var parser = Literals.Text("hello").And(Terms.WhiteSpace()).And(Literals.Text("world")); +parser.Parse("hello world"); // success +parser.Parse("helloworld"); // failure +``` + ### NonWhiteSpace Matches any non-blank spaces, optionally including new lines. Returns a `TextSpan` with the matched characters. @@ -617,7 +639,7 @@ Result: ### SkipWhiteSpace -Matches a parser after any blank spaces. This parser respects the `Scanner` options related to multi-line grammars. +Matches a parser after any blank spaces. This parser respects the `Scanner.WhiteSpaceParser` option. ```c# diff --git a/src/Parlot/Fluent/Parsers.cs b/src/Parlot/Fluent/Parsers.cs index ac53be0c..1f6eff4a 100644 --- a/src/Parlot/Fluent/Parsers.cs +++ b/src/Parlot/Fluent/Parsers.cs @@ -146,12 +146,12 @@ public static partial class Parsers public class LiteralBuilder { /// - /// Builds a parser that matches whitespaces. + /// Builds a parser that matches whitespaces. This doesn't use the . /// public Parser WhiteSpace(bool includeNewLines = false) => new WhiteSpaceLiteral(includeNewLines); /// - /// Builds a parser that matches anything until whitespaces. + /// Builds a parser that matches anything until whitespaces. This doesn't use the . /// public Parser NonWhiteSpace(bool includeNewLines = true) => new NonWhiteSpaceLiteral(includeNewLines: includeNewLines); @@ -311,7 +311,12 @@ public Parser Identifier(Func? extraStart = null, Func - /// Builds a parser that matches anything until whitespaces. + /// Builds a parser that matches whitespaces as defined in . + /// + public Parser WhiteSpace() => new WhiteSpaceParser(); + + /// + /// Builds a parser that matches anything until whitespaces. This doesn't use the . /// public Parser NonWhiteSpace(bool includeNewLines = true) => Parsers.SkipWhiteSpace(new NonWhiteSpaceLiteral(includeNewLines: includeNewLines)); diff --git a/src/Parlot/Fluent/SkipWhiteSpace.cs b/src/Parlot/Fluent/SkipWhiteSpace.cs index 256507e2..dbdeed97 100644 --- a/src/Parlot/Fluent/SkipWhiteSpace.cs +++ b/src/Parlot/Fluent/SkipWhiteSpace.cs @@ -1,11 +1,9 @@ -using Parlot.Compilation; using Parlot.Rewriting; using System; -using System.Linq.Expressions; namespace Parlot.Fluent; -public sealed class SkipWhiteSpace : Parser, ICompilable, ISeekable +public sealed class SkipWhiteSpace : Parser, ISeekable { public Parser Parser { get; } @@ -56,36 +54,5 @@ public override bool Parse(ParseContext context, ref ParseResult result) return false; } - public CompilationResult Compile(CompilationContext context) - { - var result = context.CreateCompilationResult(); - - var startBeforeWhitespace = context.DeclarePositionVariable(result); - - var parserCompileResult = Parser.Build(context); - - result.Body.Add( - Expression.Block( - parserCompileResult.Variables, - context.ParserSkipWhiteSpace(), - Expression.Block( - Expression.Block(parserCompileResult.Body), - Expression.IfThenElse( - parserCompileResult.Success, - Expression.Block( - context.DiscardResult ? - Expression.Empty() : - Expression.Assign(result.Value, parserCompileResult.Value), - Expression.Assign(result.Success, Expression.Constant(true, typeof(bool))) - ), - context.ResetPosition(startBeforeWhitespace) - ) - ) - ) - ); - - return result; - } - public override string ToString() => $"{Parser} (Skip WS)"; } diff --git a/src/Parlot/Fluent/WhiteSpaceParser.cs b/src/Parlot/Fluent/WhiteSpaceParser.cs new file mode 100644 index 00000000..39f069ce --- /dev/null +++ b/src/Parlot/Fluent/WhiteSpaceParser.cs @@ -0,0 +1,40 @@ +using Parlot.Compilation; +using Parlot.Rewriting; +using System; +using System.Linq.Expressions; + +namespace Parlot.Fluent; + +/// +/// A parser that succeeds when parsing whitespaces as defined in . +/// +public sealed class WhiteSpaceParser : Parser +{ + public WhiteSpaceParser() + { + } + + public override bool Parse(ParseContext context, ref ParseResult result) + { + context.EnterParser(this); + + var start = context.Scanner.Cursor.Offset; + + context.SkipWhiteSpace(); + + var end = context.Scanner.Cursor.Offset; + + if (start == end) + { + context.ExitParser(this); + return false; + } + + result.Set(start, end, new TextSpan(context.Scanner.Buffer, start, end - start)); + + context.ExitParser(this); + return true; + } + + public override string ToString() => $"WhiteSpaceParser"; +} diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 132a8bc3..8d69d846 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -1588,4 +1588,51 @@ public void DisableLoopDetectionShouldAllowInfiniteRecursion() // We can't safely test the DisableLoopDetection = true case to completion without stack overflow, // but the implementation is verified by the fact that the flag is properly checked in the code } + + [Fact] + public void WhiteSpaceParserShouldUseParseContextParser() + { + // Test that the whitespace parser respects the ParseContext settings + var hello = Literals.Text("hello"); + var world = Literals.Text("world"); + var parser = Terms.WhiteSpace().And(hello).And(Terms.WhiteSpace()).And(world).WithWhiteSpaceParser(Capture(ZeroOrMany(Literals.Char('.')))); + + // Should use the custom whitespace parser from the context + Assert.True(parser.TryParse("..hello....world", out var result, out var _)); + Assert.Equal("..", result.Item1.ToString()); + Assert.Equal("hello", result.Item2.ToString()); + Assert.Equal("....", result.Item3.ToString()); + Assert.Equal("world", result.Item4.ToString()); + } + + [Fact] + public void WithWhiteSpaceParserDoesntRepeat() + { + // Test that the whitespace parser can fail and propagate the failure + var hello = Literals.Text("hello"); + var world = Terms.Text("world"); + var ws = Capture(Literals.Text("!!!")); // This whitespace parser only matches "!!!" + var parser = hello.And(world).WithWhiteSpaceParser(ws); + + Assert.True(parser.TryParse("hello!!!world", out var _, out var _)); + Assert.False(parser.TryParse("hello world", out var _, out var _)); + Assert.False(parser.TryParse("hello!!! world", out var _, out var _)); + Assert.False(parser.TryParse("hello!!!!!!world", out var _, out var _)); + } + + [Fact] + public void WithWhiteSpaceParserRepeatingPattern() + { + // Test that the whitespace parser can fail and propagate the failure + var hello = Literals.Text("hello"); + var world = Terms.Text("world"); + var ws = Capture(Literals.Text("!!!").OneOrMany()); + var parser = hello.And(world).WithWhiteSpaceParser(ws); + + Assert.True(parser.TryParse("hello!!!world", out var _, out var _)); + Assert.False(parser.TryParse("hello world", out var _, out var _)); + Assert.False(parser.TryParse("hello!!! world", out var _, out var _)); + Assert.True(parser.TryParse("hello!!!!!!world", out var _, out var _)); + Assert.False(parser.TryParse("hello!!!!!world", out var _, out var _)); + } }