Skip to content
JPVenson edited this page May 10, 2022 · 3 revisions

With version 3.0 you can inject your own tokens into Morestachio.
This involes two steps:

  • Creating an CustomDocumentItemProvider that is responsible for Parsing and Tokenizing
  • Creating an IDocumentItem that will be used to render the Tag or Block

CustomDocumentItemProvider

To add your custom Tag(Single statement) or Block(Single statement with Children) you have to inhert from CustomDocumentItemProvider. The CustomDocumentItemProvider provides methods for Parsing and Tokenizing. You can use the provided base classes BlockDocumentItemProviderBase or TagDocumentItemProviderBase. Those two base classes are implemented to handle most of the common cases.

Example Simple Block (Less compiler)

public class LessCompilerDocumentItemProvider : BlockDocumentItemProviderBase
{
	public LessCompilerDocumentItemProvider() : base("#Less", "/Less")
	{
	}

	/// <inheritdoc />
	public override IDocumentItem CreateDocumentItem(string tag, string value, TokenPair token, ParserOptions options)
	{
		return new CompileLessDocumentItem();
	}
}

The less compiler block does not take any argument that needs to be parsed and creates an CompileLessDocumentItem.

IDocumentItem

The other Part of the process is creating your own DocumentItem that inherts IDocumentItem. When implementing this interface you must take care of some points.

  • Implement support for Serialization.
  • Implement IEquality Support

Support for Serialization is rather optional and if you do not plan to Serialize the document tree you can skip this step.

Serialization includes XML and Binary and is done by 2 steps: For Binary you must provide a constructor that matches this pattern:

public .ctor(SerializationInfo info, StreamingContext context) : base(info, c)
{...}

(don't forget to call the base constructor!)

you should also overwrite the SerializeBinaryCore(SerializationInfo info, StreamingContext context) method that will be called when serialization happens.

For xml support you can simply overwrite the SerializeXml(XmlWriter writer) and DeSerializeXml(XmlReader reader) methods. In the case of XML its worth noting that at the time of calling this methods, the xml element and its attributes are already written and after leaving your method the children of your DocumentItem will be written so do not close the parent tag at this point.

Morestachio has some base classes that it uses for all DocumentItems:

  • DocumentItemBase handles most of the Serialization work for the base properties like Location
  • BlockDocumentItemBase adds support for children to be added to a document item
  • ExpressionDocumentItemBase contains one IMorestachioExpression and handles its serialization (inherts from DocumentItemBase)
  • ValueDocumentItemBase contains one string property hand handles its serialization (inherts from DocumentItemBase)

As an extra, you can implement the IStringVisitor interface. This allows your DocumentItem to be parsed by the ToParsableStringDocumentVisitor

Example CompileLessDocumentItem

/// <summary>
///		Wraps the dotless into an Document provider 
/// </summary>
[Serializable]
public class CompileLessDocumentItem : BlockDocumentItemBase, ToParsableStringDocumentVisitor.IStringVisitor
{
	/// <summary>
	///		Binary serialization ctor
	/// </summary>
	internal CompileLessDocumentItem() : base(CharacterLocation.Unknown, null)
	{

	}
	
	/// <summary>
	/// 
	/// </summary>
	/// <param name="location"></param>
	/// <param name="tagTokenOptions"></param>
	public CompileLessDocumentItem(CharacterLocation location, IEnumerable<ITokenOption> tagTokenOptions) 
		: base(location, tagTokenOptions)
	{

	}

	/// <summary>
	///		Serialization Constructor
	/// </summary>
	/// <param name="info"></param>
	/// <param name="c"></param>
	protected CompileLessDocumentItem(SerializationInfo info, StreamingContext c)
		: base(info, c)
	{

	}

	/// <inheritdoc />
	public override async ItemExecutionPromise Render(IByteCounterStream outputStream, ContextObject context, ScopeData scopeData)
	{
		using (var tempStream = outputStream.GetSubStream())
		{
			await MorestachioDocument.ProcessItemsAndChildren(Children, tempStream, context, scopeData);
			var lessCode = tempStream.Read();
			outputStream.Write(Less.Parse(lessCode, new DotlessConfiguration()
			{
				CacheEnabled = false,
			}));
		}
		return Enumerable.Empty<DocumentItemExecution>();
	}

	/// <inheritdoc />
	public override void Accept(IDocumentItemVisitor visitor)
	{
		visitor.Visit(this);
	}

	/// <inheritdoc />
	public void Render(ToParsableStringDocumentVisitor visitor)
	{
		visitor.StringBuilder.Append("{{#LESS}}");
		visitor.VisitChildren(this);
		visitor.StringBuilder.Append("{{/LESS}}");
	}
}

What is ItemExecutionPromise

Morestachio is an NetStandard 2.0 lib but also wants to support the ValueTask<> from NetCore. Because of this there are a number of preprocessor variables that switches the normal Task with ValueTask. If you only want to support one or the other that is fine just replace ItemExecutionPromise with ether Task<IEnumerable<DocumentItemExecution>> or ValueTask<IEnumerable<DocumentItemExecution>>. if you want to support both like morestachio add this to the top of your cs file:

#if ValueTask
using ItemExecutionPromise = System.Threading.Tasks.ValueTask<System.Collections.Generic.IEnumerable<Morestachio.Document.Contracts.DocumentItemExecution>>;
using Promise = System.Threading.Tasks.ValueTask;
#else
using ItemExecutionPromise = System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Morestachio.Document.Contracts.DocumentItemExecution>>;
using Promise = System.Threading.Tasks.Task;
#endif

and add the corresponding PropertyGroups to your csproj:

  <PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
    <DefineConstants>ValueTask</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2'">
    <DefineConstants>ValueTask</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
    <DefineConstants>ValueTask; ValueTaskFromResult</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
    <DefineConstants>ValueTask; ValueTaskFromResult; Span</DefineConstants>
  </PropertyGroup>

Enable usage

To enable parsing of your custom document you must add them to the ParserOptionsBuilder you are using to parse your template by calling ParserOptionsBuilder.AddCustomDocument(new MyCustomDocumentItemProvider())