Skip to content

Commit

Permalink
Add docs for parser pipelines #31
Browse files Browse the repository at this point in the history
Also adds helper methods to the pipeline builder
  • Loading branch information
Romanx committed Sep 30, 2018
1 parent 025b527 commit ec8923b
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Tag parsing is performed in a very explicit method with a low garbage collection
New tag parsers are added to the `ParserPipelineBuilder` as either `InlineParsers` or `BlockParsers`.
These are handled in order and so if you're replacing a tag parser it's recommended to remove the existing parser before hand. If you're adding a new tag make sure the syntax doesn't collide with the opening syntax of any existing tags or that it comes before that tags parser.

For more information about building parser pipelines please see [docs here.](/parser-pipelines).

The built pipeline should be cached if it needs to be reused and can be passed in as an overload into the static `MustacheParser` class if all you want is to parse the tags into an AST.

`IRendererSettingsBuilder` contains a `SetParserPipeline(ParserPipeline)` method which allows the user to set a specific pipeline to be used during parsing allowing new tag parsers to be added during setup.
Expand Down
22 changes: 22 additions & 0 deletions docs/parser-pipelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Parser Pipelines

The parser pipelines defines which inline and block parsers will be run across the input generating tags from their output.

You may want to add custom tags to your output or remove existing ones if you don't want to use that functionality.

We provide no method of building a parser pipeline yourself since we wanted to provide helpers so that users could easily fall into the best patterns.

With this in mind you build parser pipelines using the `Parser Pipline Builder` which has some helper methods for replacing or adding parsers before or after other parsers.

```c#
var builder = new ParserPipelineBuilder();
builder.Replace<InterpolationTagParser>(new MyCustomParser());
// OR
builder.Remove<InterpolationTagParser>();
// OR
builder.AddAfter<InterpolationTagParser>(new MyCustomParser());
builder.AddBefore<InterpolationTagParser>(new MyCustomParser());
var pipeline = builder.Build();
```

These methods will work regardless of the type of parser you provide it and will correctly adapt the underlying parser listings.
152 changes: 150 additions & 2 deletions src/Stubble.Core/Builders/ParserPipelineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class ParserPipelineBuilder
/// </summary>
public ParserPipelineBuilder()
{
InlineParsers = new List<Parser.Interfaces.InlineParser>
InlineParsers = new List<InlineParser>
{
new CommentTagParser(),
new DelimiterTagParser(),
Expand All @@ -41,7 +41,7 @@ public ParserPipelineBuilder()
/// <summary>
/// Gets the inline parsers
/// </summary>
public List<Parser.Interfaces.InlineParser> InlineParsers { get; }
public List<InlineParser> InlineParsers { get; }

/// <summary>
/// Gets the block parsers
Expand All @@ -63,5 +63,153 @@ public ParserPipeline Build()
pipeline = new ParserPipeline(InlineParsers, BlockParsers);
return pipeline;
}

/// <summary>
/// Finds a parser with the provided type and replaces it with the new parser
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to replace the provided one with</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder Replace<T>(InlineParser parser)
where T : InlineParser
{
Replace<T, InlineParser>(InlineParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and replaces it with the new parser
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to replace the provided one with</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder Replace<T>(BlockParser parser)
where T : BlockParser
{
Replace<T, BlockParser>(BlockParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and adds the new parser after it
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to add after</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder AddAfter<T>(InlineParser parser)
where T : InlineParser
{
AddAfter<T, InlineParser>(InlineParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and adds the new parser after it
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to add after</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder AddAfter<T>(BlockParser parser)
where T : BlockParser
{
AddAfter<T, BlockParser>(BlockParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and adds the new parser before it
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to add before</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder AddBefore<T>(InlineParser parser)
where T : InlineParser
{
AddBefore<T, InlineParser>(InlineParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and adds the new parser before it
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <param name="parser">The parser to add before</param>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder AddBefore<T>(BlockParser parser)
where T : BlockParser
{
AddBefore<T, BlockParser>(BlockParsers, parser);
return this;
}

/// <summary>
/// Finds a parser with the provided type and adds the new parser before it
/// </summary>
/// <typeparam name="T">The type to replace</typeparam>
/// <returns>The builder for chaining</returns>
public ParserPipelineBuilder Remove<T>()
{
if (typeof(InlineParser).IsAssignableFrom(typeof(T)))
{
Remove<T, InlineParser>(InlineParsers);
}
else if (typeof(BlockParser).IsAssignableFrom(typeof(T)))
{
Remove<T, BlockParser>(BlockParsers);
}

return this;
}

private static void Remove<T, TItem>(IList<TItem> collection)
{
for (var i = 0; i < collection.Count; i++)
{
var item = collection[i];
if (item is T)
{
collection.RemoveAt(i);
break;
}
}
}

private static void Replace<T, TItem>(IList<TItem> collection, TItem value)
{
for (var i = 0; i < collection.Count; i++)
{
var item = collection[i];
if (item is T)
{
collection[i] = value;
}
}
}

private static void AddAfter<T, TItem>(IList<TItem> collection, TItem value)
{
for (var i = 0; i < collection.Count; i++)
{
var item = collection[i];
if (item is T)
{
collection.Insert(i + 1, value);
break;
}
}
}

private static void AddBefore<T, TItem>(IList<TItem> collection, TItem value)
{
for (var i = 0; i < collection.Count; i++)
{
var item = collection[i];
if (item is T)
{
collection.Insert(i, value);
break;
}
}
}
}
}
139 changes: 139 additions & 0 deletions test/Stubble.Core.Tests/ParserPipelineBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Stubble.Core.Builders;
using Stubble.Core.Imported;
using Stubble.Core.Parser;
using Stubble.Core.Parser.Interfaces;
using Stubble.Core.Parser.TokenParsers;
using Stubble.Core.Tokens;
using Xunit;

namespace Stubble.Core.Tests
{
public class ParserPipelineBuilderTest
{
[Fact]
public void It_Should_Be_Able_To_Remove_InlineParsers()
{
var builder = new ParserPipelineBuilder();
builder.Remove<InterpolationTagParser>();
var pipeline = builder.Build();

Assert.DoesNotContain(pipeline.InlineParsers, p => p is InterpolationTagParser);
}

[Fact]
public void It_Should_Be_Able_To_Remove_BlockParsers()
{
var builder = new ParserPipelineBuilder();
builder.Remove<SectionTagParser>();
var pipeline = builder.Build();

Assert.DoesNotContain(pipeline.BlockParsers, p => p is SectionTagParser);
}

[Fact]
public void It_Should_Be_Able_To_Replace_InlineParsers()
{
var builder = new ParserPipelineBuilder();
builder.Replace<InterpolationTagParser>(new MyCustomInlineParser());
var pipeline = builder.Build();

Assert.DoesNotContain(pipeline.InlineParsers, p => p is InterpolationTagParser);
}

[Fact]
public void It_Should_Be_Able_To_Replace_BlockParsers()
{
var builder = new ParserPipelineBuilder();
builder.Replace<SectionTagParser>(new MyCustomBlockParser());
var pipeline = builder.Build();

Assert.DoesNotContain(pipeline.BlockParsers, p => p is SectionTagParser);
}

[Fact]
public void It_Should_Be_Able_To_AddAfter_InlineParsers()
{
var builder = new ParserPipelineBuilder();
builder.AddAfter<InterpolationTagParser>(new MyCustomInlineParser());
var pipeline = builder.Build();

Assert.Contains(pipeline.InlineParsers, p => p is InterpolationTagParser);
Assert.Contains(pipeline.InlineParsers, p => p is MyCustomInlineParser);

var interpolationIndex = pipeline.InlineParsers.FindIndex(p => p is InterpolationTagParser);
var customIndex = pipeline.InlineParsers.FindIndex(p => p is MyCustomInlineParser);
Assert.True(customIndex > interpolationIndex);
}

[Fact]
public void It_Should_Be_Able_To_AddAfter_BlockParsers()
{
var builder = new ParserPipelineBuilder();
builder.AddAfter<SectionTagParser>(new MyCustomBlockParser());
var pipeline = builder.Build();

Assert.Contains(pipeline.BlockParsers, p => p is SectionTagParser);
Assert.Contains(pipeline.BlockParsers, p => p is MyCustomBlockParser);

var sectionIndex = pipeline.BlockParsers.FindIndex(p => p is SectionTagParser);
var customIndex = pipeline.BlockParsers.FindIndex(p => p is MyCustomBlockParser);
Assert.True(customIndex > sectionIndex);
}

[Fact]
public void It_Should_Be_Able_To_AddBefore_InlineParsers()
{
var builder = new ParserPipelineBuilder();
builder.AddBefore<InterpolationTagParser>(new MyCustomInlineParser());
var pipeline = builder.Build();

Assert.Contains(pipeline.InlineParsers, p => p is InterpolationTagParser);
Assert.Contains(pipeline.InlineParsers, p => p is MyCustomInlineParser);

var interpolationIndex = pipeline.InlineParsers.FindIndex(p => p is InterpolationTagParser);
var customIndex = pipeline.InlineParsers.FindIndex(p => p is MyCustomInlineParser);
Assert.True(customIndex < interpolationIndex);
}

[Fact]
public void It_Should_Be_Able_To_AddBefore_BlockParsers()
{
var builder = new ParserPipelineBuilder();
builder.AddBefore<SectionTagParser>(new MyCustomBlockParser());
var pipeline = builder.Build();

Assert.Contains(pipeline.BlockParsers, p => p is SectionTagParser);
Assert.Contains(pipeline.BlockParsers, p => p is MyCustomBlockParser);

var sectionIndex = pipeline.BlockParsers.FindIndex(p => p is SectionTagParser);
var customIndex = pipeline.BlockParsers.FindIndex(p => p is MyCustomBlockParser);
Assert.True(customIndex < sectionIndex);
}

private class MyCustomInlineParser : InlineParser
{
public override bool Match(Processor processor, ref StringSlice slice)
{
throw new System.NotImplementedException();
}
}

private class MyCustomBlockParser : BlockParser
{
public override void EndBlock(Processor processor, BlockToken token, BlockCloseToken closeToken, StringSlice content)
{
throw new System.NotImplementedException();
}

public override bool TryClose(Processor processor, ref StringSlice slice, BlockToken token)
{
throw new System.NotImplementedException();
}

public override ParserState TryOpenBlock(Processor processor, ref StringSlice slice)
{
throw new System.NotImplementedException();
}
}
}
}

0 comments on commit ec8923b

Please sign in to comment.