Mustachio is a lightweight, powerful, flavorful, template engine. Morestachio extends the Mustachio syntax. With Morestachio each value can be formatted by adding formatter, templates will be parsed as streams and will create a new stream, and Morestachio accepts any object besides the Dictionary from Mustachio.
Introduction
Today, I want to introduce you to Morestachio. A Fork of Mustachio.
Morestachio is created upon Mustachio and is a Template formatting engine completely written in C#.
Github: Morestachio
Morestachio can be installed via NuGet:
Install-Package Morestachio
Key-Features
- Templating every Text based document
- Loops with
#EACH
, #FOREACH
, #DO
, #WHILE
, #REPEAT
- Stream based processing with a optional limit of generated bytes (best for Web-Server to limit memory)
- Processing of
object
, IDictionary<string,object>
and IEnumerables
alike - Cancellation of Template generation
- Async Generation
- All modern .NET versions are supported
- NetStandard (netstandard2.0, netstandard2.1)
- NET5.0 & NET6.0
- Capable of processing in every encoding
- Attach custom code to your template with the use of formatters or use the >300 build in ones
- Build in, Optional Localization support and Logging support
- Support for Template Partials
- Serializable Document Tree
Background
As most projects, this one was created in the need of a fast, simple but somehow extendable formatting syntax for template generation. First, I have found Mustachio. It worked great but had some drawbacks as every data must be prepared in code or at least before it was given to the engine. This might be ok for developers when you have that one template that always looks the same, but in my case, I needed more. I wanted to let users create templates on a single Data Source. That had brought some problems with it as there was no way in Filtering, Ordering or "formatting" the data in a general sense.
"Formatting? Is that not what the engine should do?"
Yes, but what about the user who wants only the day of the DateTime
object or wants to only display every 2nd item in a list or else. This was the more of formatting I was missing from Mustachio. So I decided to create a Fork from it and implement all these missing things and even some more.
Using the Code
var sourceTemplate = "Dear {{name}}, this is definitely a personalized note to you.
Very truly yours, {{sender}}";
var parserOptions = Morestachio.ParserOptionsBuilder
.New()
.WithTemplate(sourceTemplate)
.Build();
var template = await Morestachio.Parser.ParseWithOptionsAsync(parserOptions);
var renderer = template.CreateRenderer();
dynamic model = new ExpandoObject();
model.name = "John";
model.sender = "Sally";
var result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("Dynamic Object: " + result);
model = new Dictionary<string, object>();
model["name"] = "John";
model["sender"] = "Sally";
result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("Dictionary<string,object>: " + result);
model = new JohnAndSally();
model.name = "John";
model.sender = "Sally";
result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("object: " + result);
That is the most basic part you have to know. Take an object, ether an IDictionary<string,object>
or another class instance compared with a template and run it with the Parser
.
Nested Objects
Nested object can be simply called by using a dot. For example:
var sourceTemplate = "Dear {{other.name}}, this is definitely a personalized note to you.
Very truly yours, {{other.sender}}"
var template = await Morestachio.ParserOptionsBuilder.New().
WithTemplate(sourceTemplate).BuildAndParseAsync();
var otherModel = new Dictionary<string, object>();
otherModel["name"] = "John";
otherModel["sender"] = "Sally";
var model = new Dictionary<string, object>();
model["other"] = otherModel;
await template.RenderAndStringifyAsync(model)
Scoping
You can set the scope of a block of instructions to a particular value by scoping to it. With the {{#SCOPE ... }} ... {{/SCOPE}}
block, you will execute all included instructions with that scope, if the value does not meet the DefinetionOfFalse
. Falsey values are any values that are considered as either invalid or representations of boolean false.
A value is considered falsey if:
- it's either:
null
, boolean false
, numeric 0
, string empty
, collection empty
(whitespaces are allowed)
A scope will only be applied if the value is not falsey. You can apply a scope by prefixing it with #
.
Inverted Scope
An inverted scope is just a scope that will be applied if the value is falsey. An inverted scope can be used by prefixing the value with ^SCOPE
.
var sourceTemplate =
"{{#SCOPE other}} Other exists{{/SCOPE}}" +
"{{^SCOPE other}} Other does not exists{{/SCOPE}}," +
" And"+
"{{#SCOPE another}}Another exists {{/SCOPE}} " +
"{{^SCOPE another}}Another does not exists {{/SCOPE}}";
var template = await Morestachio.ParserOptionsBuilder.New().
WithTemplate(sourceTemplate).BuildAndParseAsync();
var otherModel = new Dictionary<string, object>();
otherModel["otherother"] = "Test";
var model = new Dictionary<string, object>();
model["other"] = otherModel;
model["navigateUp"] = "Test 2";
await template.RenderAndStringifyAsync(model);
While you are inside a scope, all paths you write are prefixed with the path you have used in the scope. So if you want to print the properties of other
while you are inside the scope of other
, you do not have to write the full path and can instead write the path direct like (from the example above):
{{#SCOPE other}} {{otherother}} {{/SCOPE}}
will yield "Test
".
Navigating Scopes Up
With the dot syntax, you can navigate down, with the ../
syntax, you can navigate one scope up like:
{{#SCOPE other}} {{../otherother}} {{/SCOPE}}
will yield "Test 2". You can go up more than one level by stacking the ../
expression more than one time like ../../../
to go 3 levels up. If you reach the root while doing this, you will stay there.
Lists and #Each
Lists can be looped with the #each
syntax. For example:
var sourceTemplate = "Names: {{#EACH names}} {{.}}, {{/EACH}}";
sourceTemplate = "Names: {{#FOREACH name IN names}} {{name}}, {{/FOREACH}}";
var template = await Morestachio.ParserOptionsBuilder.New().
WithTemplate(sourceTemplate).BuildAndParseAsync();
var otherModel = new List<string>();
otherModel.Add("John");
otherModel.Add("Sally");
var model = new Dictionary<string, object>();
model["names"] = otherModel;
Console.WriteLine(await template.RenderAndStringifyAsync(model));
In addition to Mustachio, there are some special keywords inside an loop:
Name | Type | Description |
$first | bool | Is the current item the first in the collection |
$index | int | The Index in the list |
$middel | bool | Is the current item not the first or last |
$last | bool | Is the current item the last one |
$odd | bool | Is the index odd |
$even | bool | Is the index even |
This is very helpful to get format your output like this:
var sourceTemplate = "Names: {{#EACH names}} {{.}} {{^IF $last}},{{/IF}} {{/EACH}}";
var template = await Morestachio.ParserOptionsBuilder.New().
WithTemplate(sourceTemplate).BuildAndParseAsync();
var model = new List<string>();
otherModel.Add("John");
otherModel.Add("Sally");
var model = new Dictionary<string, object> ();
model["names"] = otherModel;
await template.RenderAndStringifyAsync(model);
If - Else & Not If
The If
block behaves similar to the Scope
block by checking if the value set as the argument, does meet the DefinitionOfFalse
. If it does not meet the condition, all children will be executed.
{{#IF valueIsTrue}}
Then print this
{{#IFELSE orThisIsTrue}}
Then Print that
{{/IFELSE}}
{{#ELSE}}
Otherwise this
{{/ELSE}}
{{/IF}}
Formatter
Each primitive object such as:
string
bool
char
int
double
short
float
long
byte
sbyte
decimal
including DateTimes
or every object that implements IFormattable
can be formatted by default. Those default formatters are declared in the ContextObject.PrintableTypes
dictionary. You can add or remove any default formatter there globally. Formatters can be used to change how a value is rendered onto your template. The power of this syntax is quite powerful as you can declare a formatter that changes the appearance of every object or you can define a template where one of your objects should be displayed without repeating it in your template.
For example, can you change who a Byte
should be displayed? When you call byte.ToString()
, you will get the numeric representation of that byte
as a string
like:
0x101.ToString()
"257"
If you have a byte
in your model, you can call the default formatter:
var withoutFormatterTemplate = "{{no}}";
var withFormatterTemplate = "{{no('X2')}}";
var withoutFormatter =
await Morestachio.ParserOptionsBuilder.New()
.WithTemplate(withoutFormatterTemplate)
.BuildAndParseAsync();
var withFormatter =
await Morestachio.ParserOptionsBuilder.New()
.WithTemplate(withFormatterTemplate )
.BuildAndParseAsync();
var model = new Dictionary<string, object>();
model["no"] = 0x101;
withoutFormatter.RenderAndStringify(model);
withFormatter.RenderAndStringify(model);
Parser Option Formatter
You can add formatters that are bound to the ParserOptions
and therefore are only valid for one template by calling AddFormatter
on the particular ParserOptions.Formatters
object.
await Morestachio.ParserOptionsBuilder.New()
.WithTemplate("{{fooBaObject('test')}}")
.WithFormatter<FooBa, string, string>((value, argument) =>
{
return value.FooBaText;
})
.BuildAndParseAsync();
Note:
To supply a string
value in morestachio, you can ether use the "
or '
char
. You only have to close with that token that you have started the string
with, so when using double quotes to start a string
, you have to use double quotes to close it. You can also escape the string char
with \"
A formatter is bound to a type. If you define a formatter for a type in the ParserOption
, it will overwrite the formatter used in the global collection. You can also access the values of the formatted object like:
{{that.is.an.formattable(test).object.that.returns(foobArea).something.else}}
or:
{{#each list("groupBy f").also.by("propA")}} {{/each}}
or you can access other properties by writing a path:
{{obj.propA(obj.propB)}}
You can implement your own FormatterMatcher
by setting the ParserOptions.Formatters
property.
With the direct usage of the IFormatterMatcher
, you can only add exactly one formatter per type. This changes with the Formatter Framework. It takes care of a single parameter for mapping more than one Formatter to a single type by distinct it with a [Name]
parameter.
Points of Interest
Key Differences between Morestachio and mustachio
Morestachio is built upon Mustachio and extends the mustachio syntax in a few ways:
- Each value can be formatted by adding formatter to the morestachio.
- Templates will be parsed as streams and will create a new stream. This is better when creating larger templates and best for web as you can also limit the length of the "to be" created template to a certain size.
- Morestachio accepts any object besides the
Dictionary<string,object>
from mustachio.
Key Differences between Morestachio and Mustache
Morestachio contains a few modifications to the core Mustache language that are important.
FOREACH
blocks are recommended for handling arrays of values. - Complex paths are supported, for example
{{ this.is.a.valid.path }}
and {{ ../this.goes.up.one.level }}
- Template partials (
{{> secondary_template }}
) are supported by using the {{#DECLARE PartialName}} ... {{/DECLARE}}
block and {{#INCLUDE 'PartialName'}}
tag. But can be rebuilt by using formatter.
History
- Made Fork from Mustachio
- Changes to the Formatter Framework overview
- Updated version related changes