Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Morestachio. More Mustachio, Less Mustache - Extending the Mustachio Syntax

0.00/5 (No votes)
16 May 2022 1  
Morstachio, the upstarter of Mustachio. Formatter and best friend
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

  1. Templating every Text based document
  2. Loops with #EACH, #FOREACH, #DO, #WHILE, #REPEAT
  3. Stream based processing with a optional limit of generated bytes (best for Web-Server to limit memory)
  4. Processing of object, IDictionary<string,object> and IEnumerables alike
  5. Cancellation of Template generation
  6. Async Generation
  7. All modern .NET versions are supported
    • NetStandard (netstandard2.0, netstandard2.1)
    • NET5.0 & NET6.0
  8. Capable of processing in every encoding
  9. Attach custom code to your template with the use of formatters or use the >300 build in ones
  10. Build in, Optional Localization support and Logging support
  11. Support for Template Partials
  12. 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

C#
var sourceTemplate = "Dear {{name}}, this is definitely a personalized note to you. 
                      Very truly yours, {{sender}}";
// Build and Options object that contains all input parameters
var parserOptions = Morestachio.ParserOptionsBuilder
    .New()
    .WithTemplate(sourceTemplate)
    .Build();

// Tokenizes and Parses the template
var template = await Morestachio.Parser.ParseWithOptionsAsync(parserOptions);

// Creates an object based renderer
var renderer = template.CreateRenderer();

// Create the values for the template model:
dynamic model = new ExpandoObject();
model.name = "John";
model.sender = "Sally";
var result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("Dynamic Object: " + result); // Dear John, this is definitely 
                                                // a personalized note to you. 
                                                // Very truly yours, Sally
        
// or with dictionaries
model = new Dictionary<string, object>();
model["name"] = "John";
model["sender"] = "Sally";
result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("Dictionary<string,object>: " + result); // Dear John, this is 
                                                           // definitely a personalized
                                                           // note to you. Very truly 
                                                           // yours, Sally
        
//or with any other object
model = new JohnAndSally();        
model.name = "John";
model.sender = "Sally";
result = await renderer.RenderAndStringifyAsync(model);
Console.WriteLine("object: " + result); // Dear John, this is definitely 
                                        // a personalized note to you. 
                                        // Very truly yours, Sally

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:

C#
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;

// Combine the model with the template to get content:
await template.RenderAndStringifyAsync(model) // Dear John, this is definitely 
                                              // a personalized note to you. 
                                              // Very truly yours, Sally

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.

C#
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";

// Combine the model with the template to get content:
await template.RenderAndStringifyAsync(model); // Other exists and 
                                               // Another does not exist

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:

C#
var sourceTemplate = "Names: {{#EACH names}} {{.}}, {{/EACH}}";
//or
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;

// Combine the model with the template to get content:
Console.WriteLine(await template.RenderAndStringifyAsync(model)); // Names: John, Sally,

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:

C#
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; 
// Combine the model with the template to get content: 
await template.RenderAndStringifyAsync(model); // Names: John, Sally

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:

C#
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:

C#
0x101.ToString()
"257"

If you have a byte in your model, you can call the default formatter:

C#
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;

// Combine the model with the template to get content:
withoutFormatter.RenderAndStringify(model); //257 
withFormatter.RenderAndStringify(model); //101

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.

C#
await Morestachio.ParserOptionsBuilder.New()
  .WithTemplate("{{fooBaObject('test')}}")
  .WithFormatter<FooBa, string, string>((value, argument) => 
{ 
   //this is the callback for your formatter 
   //the value is the instance of the kind of object 
   //you have specified in the first generic argument 
   //the argument is the string or object that is defined in the template 
   //the last generic argument is the return type 
   //value = instance of FooBa 
   //argument = "Test" 
   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:

  1. Each value can be formatted by adding formatter to the morestachio.
  2. 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.
  3. 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.

  1. FOREACH blocks are recommended for handling arrays of values.
  2. Complex paths are supported, for example {{ this.is.a.valid.path }} and {{ ../this.goes.up.one.level }}
  3. 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

  1. Made Fork from Mustachio
  2. Changes to the Formatter Framework overview
  3. Updated version related changes

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here