Inspecting and Manipulating the AST
Fluid templates are parsed into an abstract syntax tree (AST), which is modifiable before rendering.
So, your source is parsed into an AST which is rendered into output.
flowchart LR source["Source
(string)"] -->|".parse()"| ast["AST
(FluidTemplate)"] -->|".render()"| output["Output
(string)"]
The AST is a structured object representation of the template source (much like the DOM is an object representation of an HTML string).
You can inspect and manipulate the AST prior to rendering it.
For example, say you have hundreds of templates, and they have simple tokens for the first and last name of your customers.
Hi {{ first_name }} {{ last_name }}!
Later, you decide to change how the customer object is represented in the context – instead of setting individual values, you’re going to use a comprehensive object to represent all information about the customer, and you’re going to set this in the context as a variable called customer
.
You can “fix” this without changing your templates by inheriting from AstRewriter
to create an object which will “visit” the template after you parse it (this an example of the classic Visitor Pattern):
public class FixNameIdentifier : AstRewriter
{
protected override Expression VisitMemberExpression(MemberExpression memberExpression)
{
if(memberExpression.Segments.First() is IdentifierSegment idSegment && idSegment.Identifier.EndsWith("_name"))
{
var firstOrLast = idSegment.Identifier.Split('_').First();
return new MemberExpression(new List<MemberSegment>() {
new IdentifierSegment("customer"),
new IdentifierSegment("name"),
new IdentifierSegment(firstOrLast)
});
}
return memberExpression;
}
}
Then use it like this:
var template = FluidParser.Parse(source); // this returns an AST, remember
var nameFixer = new FixNameIdentifier();
template = nameFixer.VisitTemplate(template); // this modifies the AST
template.render(context); // this has no idea what the original source was
This code will find all instances of first_name
and last_name
and replace them with customer.name.first
and customer.name.last
.
…well, effectively replace them. The above sentence makes it seem like we’re messing with the source, but we’re not – we’re actually replacing the objects that were created from the source, not the original source itself. Either way, the template execution has no idea what the original source looked like. The render
method only operates on the AST you give it.
If you just want information about your template without changing anything, the AstVisitor
pattern will inspect the template without making any changes. For an example of that, see the IdentifierIsAccessedVisitor class in the testing code.
For example, this code will tell you if the name
property is referenced in a template.
var nameReferenceChecker = new PropertyIsAccessed("name");
nameReferenceChecker.VisitTemplate(template);
var templateUsesName = nameReferenceChecker.IsAccessed;
Finally, based on what you’ve learned in this section, you probably now realize that you can manually construct the AST if you want, and skip parsing entirely.
flowchart LR ast["AST
(FluidTemplate)"] -->|".render()"| output["Output
(string)"]
var statements = new List<Statement>() {
new TextSpanStatement("Hi "),
new OutputStatement(new MemberExpression(new IdentifierSegment("name"))),
new TextSpanStatement("!")
}
var context = new TemplateContext();
contact.SetValue("name", "Deane");
var template = new FluidTemplate(statements);
var result = template.Render(context); // Hi Deane!
I don’t personally know of a use case where this would be valuable, but I have no doubt one exists.