Custom Tags and Blocks

We can write our own language constructs to drastically extend Fluid’s functionality.

First, we need to understand the nomenclature.

  • A tag is something that doesn’t close. It’s just a standalone token.
  • A block is a tag that that has a corresponding endwhatever tag and therefore encloses something. The contents can be string literals or executable Fluid code or any combination of that.

There are three types of each – empty, identifier, and expression.

An empty tag/block doesn’t take anything in.

Empty Tag:   {% hostname %}
Empty Block: {% hostname %}{% endhostname %}

An identifier tag/block takes a single value that can be used internally.

Identifier Tag:   {% hello formal %}
Identifier Block: {% hello formal %}{% endhello %}

An expression tag/block takes a Fluid expression. That expression can be evaluated internally to provide a value.

Expression Tag:   {% bold name | upcase %}
Expression Block: {% bold name | upcase %}{% endbold %}

An empty tag is the simplest. It takes no arguments – it’s the simplest possible token.

(Note that the “empty” here doesn’t refer to the tag itself, because tags don’t close, so all tags are technically empty. The “empty” refers to the fact that it doesn’t take any arguments. So its argument list is “empty.”)

The function params are:

  • w: A TextWriter through which you can write output
  • e: An encoder of some kind (honestly, I don’t know what this is, and I’ve never used it)
  • c: The current TemplateContext
parser.RegisterEmptyTag("hostname", (w, e, c) =>
{
  w.Write(System.Net.Dns.GetHostName());
  return Statement.Normal();
});
This computer's hostname is {% hostname %}.

However, this is a bad example of where to use an empty tag, because you can do this more easily by simply injecting data into the model.

content.SetValue("hostname", System.Net.Dns.GetHostName());
This computer's hostname is {{ hostname }}.

So why use an empty tag at all? An empty tag is seemingly only helpful and appropriate when you’re doing something that cannot be done prior to template execution. If the data can be determined when the TemplateContext is being formed, just set the value there.

(There are some other applications reusable/distributable code as well, if you don’t have access to the C# which forms the model. The opinion above assumes this is your own code, and you have access to model formation. If you don’t, then sure, empty tags could be helpful.)

An identifier tag is like an empty tag, but it takes in a single “identifier,” which is passed in as a string value. This is not an expression. See below for more on this.

The function signature is the same as an empty tag, you just have a string parameter at the front:

  • i: A string
parser.RegisterIdentifierTag("hi", (i, w, e, c) =>
{
  w.Write($"Hi {i}!");
  return Statement.Normal();
});
{% hi deane %}

Remember that the “identifier” (i in the above sample) is a string, even if you don’t quote it, which is kind of different. Let me say this again: it is not an expression.

Even if you do this:

{% assign deane = "This is my name." %}
{% hi deane %}

The identifier is still going to be the string “deane,” even though it looks like you’re trying to reference a variable. Remember, it’s an identifier and nothing more – just some random string you do whatever you want with.

(Note: if you expect the identifier to be something else, you probably want an “Expression Tag”; see below.)

For example, you could treat it as a variable name if you wanted and search the TemplateContext (which is also passed in as c below).

parser.RegisterIdentifierTag("hi", (i, w, e, c) =>
{
  var name = c.GetValue(i).ToStringValue();
  w.Write($"Hi {name}");
  return Statement.Normal();
});
{% hi deane %}

An identifier tag takes a single token, and that’s all. If you attempt to pass more than one (so, with any whitespace in the identifier), this will halt template execution.

An expression tag takes in an executable snippet of Fluid code. So, the first parameter is an Expression which can be evaluated (executed) inside the tag to provide a result.

parser.RegisterExpressionTag("hi", (exp, w, e, c) =>
{
  var value = exp.EvaluateAsync(c).Result.ToStringValue();
  w.Write($"Hi {value}");
  return Statement.Normal();
});
{% assign name = "Deane" %}
{% hi name | upcase %}
Hi DEANE

In the above example, the exp parameter is the expression name | upcase, which represents something that can be executed to provide a result (which we do on the first line of the method). The execution gets the value of the name variable from the current context, processes it with the upcase filter, and returns the result.

(Would you ever not evaluate the expression? …I don’t think so? I think the first thing you do inside the tag is always going to be to evaluate the expression. What would be the point of an expression you don’t evaluate?)

The three types of blocks are roughly equivalent to the three types of tags, they just include an extra parameter to represent the statements that are enclosed. Those statements can be executed at any point during the block execution.

The s below is a list of parsed Fluid statements. Some of these “statements” might just be literal text (which is, in itself, a kind of statement).

parser.RegisterEmptyBlock("bold", (s,w,e,c) =>
{
  w.Write($"<strong>");
  s.RenderStatementsAsync(w,e,c);
  w.Write($"</strong>");
  return Statement.Normal();
});
{% bold %}Deane{% endbold %}
<strong>Deane</strong>

Fluid inside the tag will be executed, so this works too.

{% bold %}
  {% assign name = "Deane" %}
  {{ name | upcase }}
{% endbold %}
<strong>DEANE</strong>

You can nest blocks and they will be called recursively.

{% bold %}
  {% bold %}
    Deane
  {% endbold %}
{% endbold %}
<strong><strong>Deane</strong></strong>