Coder Social home page Coder Social logo

sebastienros / fluid Goto Github PK

View Code? Open in Web Editor NEW
1.3K 34.0 170.0 1.43 MB

Fluid is an open-source .NET template engine based on the Liquid template language.

License: MIT License

C# 99.59% Liquid 0.39% Mustache 0.02%
liquid parser dotnet template shopify view-engine

fluid's Introduction

NuGet MIT MyGet

Basic Overview

Fluid is an open-source .NET template engine based on the Liquid template language. It's a secure template language that is also very accessible for non-programmer audiences.

The following content is based on the 2.0.0-beta version, which is the recommended version even though some of its API might vary significantly. To see the corresponding content for v1.0 use this version


Tutorials

Deane Barker wrote a very comprehensive tutorial on how to write Liquid templates with Fluid. For a high-level overview, read The Four Levels of Fluid Development describing different stages of usages of Fluid.


Features

  • Very fast Liquid parser and renderer (no-regexp), with few allocations. See benchmarks.
  • Secure templates by allow-listing all the available properties in the template. User templates can't break your application.
  • Supports async filters. Templates can execute database queries more efficiently under load.
  • Customize filters and tag with your own. Even with complex grammar constructs. See Customizing tags and blocks
  • Parses templates in a concrete syntax tree that lets you cache, analyze and alter the templates before they are rendered.
  • Register any .NET types and properties, or define custom handlers to intercept when a named variable is accessed.

Contents


Source

<ul id="products">
  {% for product in products %}
    <li>
      <h2>{{product.name}}</h2>
      Only {{product.price | price }}

      {{product.description | prettyprint | paragraph }}
    </li>
  {% endfor %}
</ul>

Result

<ul id="products">
    <li>
      <h2>Apple</h2>
      $329

      Flat-out fun.
    </li>
    <li>
      <h2>Orange</h2>
      $25

      Colorful. 
    </li>
    <li>
      <h2>Banana</h2>
      $99

      Peel it.
    </li>
</ul>

Notice

  • The <li> tags are at the same index as in the template, even though the {% for } tag had some leading spaces
  • The <ul> and <li> tags are on contiguous lines even though the {% for } is taking a full line.

Using Fluid in your project

You can directly reference the Nuget package.

Hello World

Source

var parser = new FluidParser();

var model = new { Firstname = "Bill", Lastname = "Gates" };
var source = "Hello {{ Firstname }} {{ Lastname }}";

if (parser.TryParse(source, out var template, out var error))
{   
    var context = new TemplateContext(model);

    Console.WriteLine(template.Render(context));
}
else
{
    Console.WriteLine($"Error: {error}");
}

Result

Hello Bill Gates

Thread-safety

A FluidParser instance is thread-safe, and should be shared by the whole application. A common pattern is declare the parser in a local static variable:

    private static readonly FluidParser _parser = new FluidParser();

A IFluidTemplate instance is thread-safe and can be cached and reused by multiple threads concurrently.

A TemplateContext instance is not thread-safe and an instance should be created every time an IFluidTemplate instance is used.


Adding custom filters

Filters can be async or not. They are defined as a delegate that accepts an input, a set of arguments and the current context of the rendering process.

Here is the downcase filter as defined in Fluid.

Source

public static ValueTask<FluidValue> Downcase(FluidValue input, FilterArguments arguments, TemplateContext context)
{
    return new StringValue(input.ToStringValue().ToLower());
}

Registration

Filters are registered in an instance of TemplateOptions. This options object can be reused every time a template is rendered.

var options = new TemplateOptions();
options.Filters.AddFilter('downcase', Downcase);

var context = new TemplateContext(options);

Allow-listing object members

Liquid is a secure template language which will only allow a predefined set of members to be accessed, and where model members can't be changed. Property are added to the TemplateOptions.MemberAccessStrategy property. This options object can be reused every time a template is rendered.

Alternatively, the MemberAccessStrategy can be assigned an instance of UnsafeMemberAccessStrategy which will allow any property to be accessed.

Allow-listing a specific type

This will allow any public field or property to be read from a template.

var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person>();

Note: When passing a model with new TemplateContext(model) the type of the model object is automatically registered. This behavior can be disable by calling new TemplateContext(model, false)

Allow-listing specific members

This will only allow the specific fields or properties to be read from a template.

var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person>("Firstname", "Lastname");

Intercepting a type access

This will provide a method to intercept when a member is accessed and either return a custom value or prevent it.

NB: If the model implements IDictionary or any similar generic dictionary types the dictionary access has priority over the custom accessors.

This example demonstrates how to intercept calls to a Person and always return the same property.

var model = new Person { Name = "Bill" };

var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person, object>((obj, name) => obj.Name);

Customizing object accessors

To provide advanced customization for specific types, it is recommended to use value converters and a custom FluidValue implementation by inheriting from ObjectValueBase.

The following example show how to provide a custom transformation for any Person object:

private class PersonValue : ObjectValueBase
{
    public PersonValue(Person value) : base(value)
    {
    }

    public override ValueTask<FluidValue> GetIndexAsync(FluidValue index, TemplateContext context)
    {
        return Create(((Person)Value).Firstname + "!!!" + index.ToStringValue(), context.Options);
    }
}

This custom type can be used with a converter such that any time a Person is used, it is wrapped as a PersonValue.

var options = new TemplateOptions();
options.ValueConverters.Add(o => o is Person p ? new PersonValue(p) : null);

It can also be used to replace custom member access by customizing GetValueAsync, or do custom conversions to standard Fluid types.

Inheritance

All the members of the class hierarchy are registered. Besides, all inherited classes will be correctly evaluated when a base class is registered and a member of the base class is accessed.


Object members casing

By default, the properties of a registered object are case sensitive and registered as they are in their source code. For instance, the property FirstName would be access using the {{ p.FirstName }} tag.

However it can be necessary to register these properties with different cases, like Camel case (firstName), or Snake case (first_name).

The following example configures the templates to use Camel casing.

var options = new TemplateOptions();
options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;

Execution limits

Limiting templates recursion

When invoking {% include 'sub-template' %} statements it is possible that some templates create an infinite recursion that could block the server. To prevent this the TemplateOptions class defines a default MaxRecursion = 100 that prevents templates from being have a depth greater than 100.

Limiting templates execution

Template can inadvertently create infinite loop that could block the server by running indefinitely. To prevent this the TemplateOptions class defines a default MaxSteps. By default this value is not set.


Converting CLR types

Whenever an object is manipulated in a template it is converted to a specific FluidValue instance that provides a dynamic type system somehow similar to the one in JavaScript.

In Liquid they can be Number, String, Boolean, Array, Dictionary, or Object. Fluid will automatically convert the CLR types to the corresponding Liquid ones, and also provides specialized ones.

To be able to customize this conversion you can add value converters.

Adding a value converter

When the conversion logic is not directly inferred from the type of an object, a value converter can be used.

Value converters can return:

  • null to indicate that the value couldn't be converted
  • a FluidValue instance to stop any further conversion and use this value
  • another object instance to continue the conversion using custom and internal type mappings

The following example shows how to convert any instance implementing an interface to a custom string value:

var options = new TemplateOptions();

options.ValueConverters.Add((value) => value is IUser user ? user.Name : null);

Note: Type mapping are defined globally for the application.


Encoding

By default Fluid doesn't encode the output. Encoders can be specified when calling Render() or RenderAsync() on the template.

HTML encoding

To render a template with HTML encoding use the System.Text.Encodings.Web.HtmlEncoder.Default instance.

This encoder is used by default for the MVC View engine.

Disabling encoding contextually

When an encoder is defined you can use a special raw filter or {% raw %} ... {% endraw %} tag to prevent a value from being encoded, for instance if you know that the content is HTML and is safe.

Source

{% assign html = '<em>This is some html</em>' %}

Encoded: {{ html }}
Not encoded: {{ html | raw }

Result

&lt;em%gt;This is some html&lt;/em%gt;
<em>This is some html</em>

Captured blocks are not double-encoded

When using capture blocks, the inner content is flagged as pre-encoded and won't be double-encoded if used in a {{ }} tag.

Source

{% capture breaktag %}<br />{% endcapture %}

{{ breaktag }}

Result

<br />

Localization

By default templates are rendered using an invariant culture so that the results are consistent across systems. This is important for instance when rendering dates, times and numbers.

However it is possible to define a specific culture to use when rendering a template using the TemplateContext.CultureInfo property.

Source

var options = new TemplateOptions();
options.CultureInfo = new CultureInfo("en-US");
var context = new TemplateContext(options);
var result = template.Render(context);
{{ 1234.56 }}
{{ "now" | date: "%v" }}

Result

1234.56
Tuesday, August 1, 2017

Time zones

System time zone

TemplateOptions and TemplateContext provides a property to define a default time zone to use when parsing date and times. The default value is the current system's time zone. Setting a custom one can also prevent different environments (data centers) from generating different results.

  • When dates and times are parsed and don't specify a time zone, the configured one is assumed.
  • When a time zone is provided in the source string, the resulting date time uses it.

Note: The date filter conforms to the Ruby date and time formats https://ruby-doc.org/core-3.0.0/Time.html#method-i-strftime. To use the .NET standard date formats, use the format_date filter.

Source

var context = new TemplateContext { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") } ;
var result = template.Render(context);
{{ '1970-01-01 00:00:00' | date: '%c' }}

Result

Wed Dec 31 19:00:00 -08:00 1969

Converting time zones

Dates and times can be converted to specific time zones using the time_zone: <iana> filter.

Example

var context = new TemplateContext();
context.SetValue("published", DateTime.UtcNow);
{{ published | time_zone: 'America/New_York' | date: '%+' }}

Result

Tue Aug  1 17:04:36 -05:00 2017

Customizing tags and blocks

Fluid's grammar can be modified to accept any new tags and blocks with any custom parameters. The parser is based on Parlot which makes it completely extensible.

Unlike blocks, tags don't have a closing element (e.g., cycle, increment). A closing element will match the name of the opening tag with and end suffix, like endfor. Blocks are useful when manipulating a section of a a template as a set of statements.

Fluid provides helper method to register common tags and blocks. All tags and block always start with an identifier that is the tag name.

Each custom tag needs to provide a delegate that is evaluated when the tag is matched. Each delegate will be able to use these properties:

  • writer, a TextWriter instance that is used to render some text.
  • encode, a TextEncoder instance, like HtmlEncoder, or NullEncoder. It's defined by the caller of the template.
  • context, a TemplateContext instance.

Registering a custom tag

  • Empty: Tag with no parameter, like {% renderbody %}
  • Identifier: Tag taking an identifier as parameter, like {% increment my_variable %}
  • Expression: Tag taking an expression as parameter, like {% layout 'home' | append: '.liquid' %}

Here are some examples:

Source

parser.RegisterIdentifierTag("hello", (identifier, writer, encoder, context) =>
{
    writer.Write("Hello ");
    writer.Write(identifier);
});
{% hello you %}

Result

Hello you

Registering a custom block

Blocks are created the same way as tags, and the lambda expression can then access the list of statements inside the block.

Source

parser.RegisterExpressionBlock("repeat", async (value, statements, writer, encoder, context) =>
{
    var fluidValue = await value.EvaluateAsync(context);

    for (var i = 0; i < fluidValue.ToNumberValue(); i++)
    {
        await statements.RenderStatementsAsync(writer, encoder, context);
    }

    return Completion.Normal;
});
{% repeat 1 | plus: 2 %}Hi! {% endrepeat %}

Result

Hi! Hi! Hi!

Custom parsers

If identifier, empty and expression parsers are not sufficient, the methods RegisterParserBlock and RegisterParserTag accept any custom parser construct. These can be the standard ones defined in the FluidParser class, like Primary, or any other composition of them.

For instance, RegisterParseTag(Primary.AndSkip(Comma).And(Primary), ...) will expect two Primary elements separated by a comma. The delegate will then be invoked with a ValueTuple<Expression, Expression> representing the two Primary expressions.

Registering a custom operator

Operator are used to compare values, like > or contains. Custom operators can be defined if special comparisons need to be provided.

Source

The following example creates a custom xor operator that will evaluate to true if only one of the left and right expressions is true when converted to booleans.

XorBinaryExpression.cs

using Fluid.Ast;
using Fluid.Values;
using System.Threading.Tasks;

namespace Fluid.Tests.Extensibility
{
    public class XorBinaryExpression : BinaryExpression
    {
        public XorBinaryExpression(Expression left, Expression right) : base(left, right)
        {
        }

        public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext context)
        {
            var leftValue = await Left.EvaluateAsync(context);
            var rightValue = await Right.EvaluateAsync(context);

            return BooleanValue.Create(leftValue.ToBooleanValue() ^ rightValue.ToBooleanValue());
        }
    }
}

Parser configuration

parser.RegisteredOperators["xor"] = (a, b) => new XorBinaryExpression(a, b);

Usage

{% if true xor false %}Hello{% endif %}

Result

Hello

Accessing the concrete syntax tree

The syntax tree is accessible by casting the template to its concrete FluidTemplate type and using the Statements property.

Source

var template = (FluidTemplate)iTemplate;
var statements = template.Statements;

ASP.NET MVC View Engine

The package Fluid.MvcViewEngine provides a convenient way to use Liquid as a replacement or in combination of Razor in ASP.NET MVC.

Configuration

Registering the view engine

  1. Reference the Fluid.MvcViewEngine NuGet package
  2. Add a using statement on Fluid.MvcViewEngine
  3. Call AddFluid() in your Startup.cs.

Sample

using Fluid.MvcViewEngine;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddFluid();
    }
}

Registering view models

Because the Liquid language only accepts known members to be accessed, the View Model classes need to be registered in Fluid. Usually from a static constructor such that the code is run only once for the application.

View Model registration

View models are automatically registered and available as the root object in liquid templates. Custom model registrations can be added when calling AddFluid().

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddFluid(o => o.TemplateOptions.Register<Person>());
    }
}

More way to register types and members can be found in the Allow-listing object members section.

Registering custom tags

When using the MVC View engine, custom tags can still be added to the parser. Refer to this section on how to create custom tags.

It is recommended to create a custom class inheriting from FluidViewParser, and to customize the tags in the constructor of this new class. This class can then be registered as the default parser for the MVC view engine.

using Fluid.Ast;
using Fluid.MvcViewEngine;

namespace Fluid.MvcSample
{
    public class CustomFluidViewParser : FluidViewParser
    {
        public CustomFluidViewParser()
        {
            RegisterEmptyTag("mytag", static async (s, w, e, c) =>
            {
                await w.WriteAsync("Hello from MyTag");

                return Completion.Normal;
            });
        }
    }
}
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<MvcViewOptions>(options =>
        {
            options.Parser = new CustomFluidViewParser();
        });

        services.AddMvc().AddFluid();
    }
}

Layouts

Index.liquid

{% layout '_layout.liquid' %}

This is the home page

The {% layout [template] %} tag accepts one argument which can be any expression that return the relative location of a liquid template that will be used as the master template.

The layout tag is optional in a view. It can also be defined multiple times or conditionally.

From a layout template the {% renderbody %} tag is used to depict the location of the view's content inside the layout itself.

Layout.liquid

<html>
  <body>
    <div class="menu"></div>
    
    <div class="content">
      {% renderbody %}
    </div>
    
    <div class="footer"></div>
  </body>
</html>

Sections

Sections are defined in a layout as for views to render content in specific locations. For instance a view can render some content in a menu or a footer section.

Rendering content in a section

{% layout '_layout.liquid' %}

This is is the home page

{% section menu %}
  <a href="h#">This link goes in the menu</a>
{% endsection %}

{% section footer %}
  This text will go in the footer
{% endsection %}

Rendering the content of a section

<html>
  <body>
    <div class="menu">
      {% rendersection menu %}
    </div>
    
    <div class="content">
      {% renderbody %}
    </div>
    
    <div class="footer">
      {% rendersection footer %}
    </div>
  </body>
</html>

ViewStart files

Defining the layout template in each view might me cumbersome and make it difficult to change it globally. To prevent that it can be defined in a _ViewStart.liquid file.

When a view is rendered all _ViewStart.liquid files from its current and parent directories are executed before. This means multiple files can be defined to defined settings for a group of views.

_ViewStart.liquid

{% layout '_layout.liquid' %}
{% assign background = 'ffffff' }

You can also define other variables or render some content.

Custom views locations

It is possible to add custom file locations containing views by adding them to FluidMvcViewOptions.ViewsLocationFormats.

The default ones are:

  • Views/{1}/{0}.liquid
  • Views/Shared/{0}.liquid

Where {0} is the view name, and {1} is the controller name.

For partials, the list is defined in FluidMvcViewOptions.PartialsLocationFormats:

  • Views/{0}.liquid
  • Views/Partials/{0}.liquid
  • Views/Partials/{1}/{0}.liquid
  • Views/Shared/Partials/{0}.liquid

Layouts will be searched in the same locations as Views.

Execution

The content of a view is parsed once and kept in memory until the file or one of its dependencies changes. Once parsed, the tag are executed every time the view is called. To compare this with Razor, where views are first compiled then instantiated every time they are rendered. This means that on startup or when the view is changed, views with Fluid will run faster than those in Razor, unless you are using precompiled Razor views. In all cases Razor views will be faster on subsequent calls as they are compiled directly to C#.

This difference makes Fluid very adapted for rapid development cycles where the views can be deployed and updated frequently. And because the Liquid language is secure, developers give access to them with more confidence.


View Engine

The Fluid ASP.NET MVC View Engine is based on an MVC agnostic view engine provided in the Fluid.ViewEngine package. The same options and features are available, but without requiring ASP.NET MVC. This is useful to provide the same experience to build template using layouts and sections.

Usage

Use the class FluidViewRenderer : IFluidViewRender and FluidViewEngineOptions.

Whitespace control

Liquid follows strict rules with regards to whitespace support. By default all spaces and new lines are preserved from the template. The Liquid syntax and some Fluid options allow to customize this behavior.

Hyphens

For example:

{%  assign name = "Bill" %}
{{ name }}

There is a new line after the assign tag which will be preserved.

Outputs:


Bill

Tags and values can use hyphens to strip whitespace.

Example:

{%  assign name = "Bill" -%}
{{ name }}

Outputs:

Bill

The -%} strips the whitespace from the right side of the assign tag.

Template Options

Fluid provides the TemplateOptions.Trimming property that can be set with predefined preferences for when whitespace should be stripped automatically, even if hyphens are not present in tags and output values.

Greedy Mode

When greedy model is disabled in TemplateOptions.Greedy, only the spaces before the first new line are stripped. Greedy mode is enabled by default since this is the standard behavior of the Liquid language.


Custom filters

Some non-standard filters are provided by default

format_date

Formats date and times using standard .NET date and time formats. It uses the current culture of the system.

Input

"now" | format_date: "G"

Output

6/15/2009 1:45:30 PM

Documentation: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings

format_number

Formats numbers using standard .NET number formats.

Input

123 | format_number: "N"

Output

123.00

Documentation: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings

format_string

Formats custom string using standard .NET format strings.

Input

"hello {0} {1:C}" | format_string: "world" 123

Output

hello world $123.00

Documentation: https://docs.microsoft.com/en-us/dotnet/api/system.string.format


Functions

Fluid provides optional support for functions, which is not part of the standard Liquid templating language. As such it is not enabled by default.

Enabling functions

When instantiating a FluidParser set the FluidParserOptions.AllowFunction property to true.

var parser = new FluidParser(new FluidParserOptions { AllowFunctions = true });

When functions are used while the feature is not enabled, a parse error will be returned.

Declaring local functions with the macro tag

macro allows you to define reusable chunks of content invoke with local function.

{% macro field(name, value='', type='text') %}
<div class="field">
  <input type="{{ type }}" name="{{ name }}"
         value="{{ value }}" />
</div>
{% endmacro %}

Now field is available as a local property of the template and can be invoked as a function.

{{ field('user') }}
{{ field('pass', type='password') }}

Macros need to be defined before they are used as they are discovered as the template is executed.

Importing functions from external templates

Macros defined in an external template must be imported before they can be invoked.

{% from 'forms' import field %}

{{ field('user') }}
{{ field('pass', type='password') }}

Extensibility

Functions are FluidValue instances implementing the InvokeAsync method. It allows any template to be provided custom function values as part of the model, the TemplateContext or globally with options.

A FunctionValue type is also available to provide out of the box functions. It takes a delegate that returns a ValueTask<FluidValue> as the result.

var lowercase = new FunctionValue((args, context) => 
{
  var firstArg = args.At(0).ToStringValue();
  var lower = firstArg.ToLowerCase();
  return new ValueTask<FluidValue>(new StringValue(lower));
});

var context = new TemplateContext();
context.SetValue("tolower", lowercase);

var parser = new FluidParser(new FluidParserOptions { AllowFunctions = true });
parser.TryParse("{{ tolower('HELLO') }}", out var template, out var error);
template.Render(context);

Visiting and altering a template

Fluid provides a Visitor pattern allowing you to analyze what a template is made of, but also altering it. This can be used for instance to check if a specific identifier is used, replace some filters by another one, or remove any expression that might not be authorized.

Visiting a template

The Fluid.Ast.AstVisitor class can be used to create a custom visitor.

Here is an example of a visitor class which records if an identifier is accessed anywhere in a template:

  public class IdentifierIsAccessedVisitor : AstVisitor
  {
      private readonly string _identifier;

      public IdentifierIsAccessedVisitor(string identifier)
      {
          _identifier = identifier;
      }

      public bool IsAccessed { get; private set; }

      public override IFluidTemplate VisitTemplate(IFluidTemplate template)
      {
          // Initialize the result each time a template is visited with the same visitor instance

          IsAccessed = false;
          return base.VisitTemplate(template);
      }

      protected override Expression VisitMemberExpression(MemberExpression memberExpression)
      {
          var firstSegment = memberExpression.Segments.FirstOrDefault() as IdentifierSegment;

          if (firstSegment != null)
          {
              IsAccessed |= firstSegment.Identifier == _identifier;
          }

          return base.VisitMemberExpression(memberExpression);
      }
  }

And its usage:

var template = new FluidParser().Parse("{{ a.b | plus: 1}}");

var visitor = new IdentifierIsAccessedVisitor("a");
visitor.VisitTemplate(template);

Console.WriteLine(visitor.IsAccessed); // writes True

Rewriting a template

The Fluid.Ast.AstRewriter class can be used to create a custom rewriter.

Here is an example of a visitor class which replaces any plus filter with a minus one:

  public class ReplacePlusFiltersVisitor : AstRewriter
  {
      protected override Expression VisitFilterExpression(FilterExpression filterExpression)
      {
          if (filterExpression.Name == "plus")
          {
              return new FilterExpression(filterExpression.Input, "minus", filterExpression.Parameters);
          }

          return filterExpression;
      }
  }

And its usage:

var template = new FluidParser().Parse("{{ 1 | plus: 2 }}");

var visitor = new ReplacePlusFiltersVisitor();
var changed = visitor.VisitTemplate(template);

var result = changed.Render();

Console.WriteLine(result); // writes -1

Performance

Caching

Some performance boost can be gained in your application if you decide to cache the parsed templates before they are rendered. Even though parsing is memory-safe as it won't induce any compilation (meaning all the memory can be collected if you decide to parse a lot of templates), you can skip the parsing step by storing and reusing the FluidTemplate instance.

These object are thread-safe as long as each call to Render() uses a dedicated TemplateContext instance.

Benchmarks

A benchmark application is provided in the source code to compare Fluid, Scriban, DotLiquid, Liquid.NET and Handlebars.NET. Run it locally to analyze the time it takes to execute specific templates.

Results

Fluid is faster and allocates less memory than all other well-known .NET Liquid parsers. For parsing, Fluid is 19% faster than the second, Scriban, allocating nearly 3 times less memory. For rendering, Fluid is 26% faster than the second, Handlebars, 5 times faster than Scriban, but allocates half the memory. Compared to DotLiquid, Fluid renders 11 times faster, and allocates 35 times less memory.

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3593/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 9.0.100-preview.4.24209.11
  [Host]     : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2


| Method             | Mean          | Error       | StdDev      | Ratio  | RatioSD | Gen0      | Gen1     | Gen2    | Allocated   | Alloc Ratio |
|------------------- |--------------:|------------:|------------:|-------:|--------:|----------:|---------:|--------:|------------:|------------:|
| Fluid_Parse        |      2.849 us |   0.0191 us |   0.0159 us |   1.00 |    0.00 |    0.3052 |        - |       - |     2.81 KB |        1.00 |
| Scriban_Parse      |      3.297 us |   0.0407 us |   0.0381 us |   1.16 |    0.01 |    0.7744 |   0.0267 |       - |     7.14 KB |        2.54 |
| DotLiquid_Parse    |      6.558 us |   0.1118 us |   0.1046 us |   2.30 |    0.03 |    1.7624 |   0.0229 |       - |    16.21 KB |        5.76 |
| LiquidNet_Parse    |     25.064 us |   0.1409 us |   0.1100 us |   8.80 |    0.07 |    6.7444 |   0.6104 |       - |    62.04 KB |       22.06 |
| Handlebars_Parse   |  2,401.901 us |  41.1672 us |  38.5079 us | 843.36 |   15.09 |   15.6250 |   7.8125 |       - |   156.52 KB |       55.65 |
|                    |               |             |             |        |         |           |          |         |             |             |
| Fluid_ParseBig     |     16.257 us |   0.1450 us |   0.1357 us |   1.00 |    0.00 |    1.2512 |   0.0305 |       - |    11.64 KB |        1.00 |
| Scriban_ParseBig   |     18.521 us |   0.1000 us |   0.0886 us |   1.14 |    0.01 |    3.4790 |   0.4883 |       - |    32.07 KB |        2.75 |
| DotLiquid_ParseBig |     27.612 us |   0.4320 us |   0.4041 us |   1.70 |    0.03 |   10.2539 |   0.4883 |       - |    94.36 KB |        8.11 |
| LiquidNet_ParseBig | 12,206.204 us | 188.5327 us | 176.3536 us | 750.86 |   12.96 | 3093.7500 |  15.6250 |       - | 28543.38 KB |    2,452.05 |
|                    |               |             |             |        |         |           |          |         |             |             |
| Fluid_Render       |    134.311 us |   1.5910 us |   1.4104 us |   1.00 |    0.00 |   10.2539 |   0.4883 |       - |    95.86 KB |        1.00 |
| Scriban_Render     |    615.143 us |   5.4851 us |   4.5803 us |   4.58 |    0.06 |   68.3594 |  68.3594 | 68.3594 |   498.64 KB |        5.20 |
| DotLiquid_Render   |  1,403.693 us |  27.4426 us |  40.2251 us |  10.63 |    0.27 |  351.5625 | 140.6250 | 23.4375 |  3368.09 KB |       35.13 |
| LiquidNet_Render   |    825.819 us |   8.6639 us |   7.6803 us |   6.15 |    0.08 |  339.8438 | 160.1563 |       - |   3130.8 KB |       32.66 |
| Handlebars_Render  |    238.959 us |   4.7119 us |  11.5585 us |   1.68 |    0.06 |   20.9961 |   3.4180 |       - |   194.92 KB |        2.03 |

Tested on May 28, 2024 with

  • Scriban 5.10.0
  • DotLiquid 2.2.692
  • Liquid.NET 0.10.0
  • Handlebars.Net 2.1.6
Legend
  • Parse: Parses a simple HTML template containing filters and properties
  • ParseBig: Parses a Blog Post template.
  • Render: Renders a simple HTML template containing filters and properties, with 500 products.

Used by

Fluid is known to be used in the following projects:

  • Orchard Core CMS Open Source .NET modular framework and CMS
  • MaltReport OpenDocument/OfficeOpenXML powered reporting engine for .NET and Mono
  • Elsa Workflows .NET Workflows Library
  • FluentEmail All in one email sender for .NET
  • NJsonSchema Library to read, generate and validate JSON Schema draft v4+ schemas
  • NSwag Swagger/OpenAPI 2.0 and 3.0 toolchain for .NET
  • Optimizely An enterprise .NET CMS
  • Rock Relationship Management System
  • TemplateTo Powerful Template Based Document Generation
  • Weavo Liquid Loom A Liquid Template generator/editor + corresponding Azure Logic Apps Connector / Microsoft Power Automate Connector
  • Semantic Kernel Integrate cutting-edge LLM technology quickly and easily into your apps

Please create a pull-request to be listed here.

fluid's People

Contributors

alexgirarddev avatar awyl avatar dafergu2 avatar danielmpetrov avatar deanebarker avatar deanmarcussen avatar dependabot[bot] avatar dshabetia avatar ericgarzagzz avatar guillaume86 avatar hishamco avatar ifle avatar jafin avatar jeffolmstead avatar jeremycook avatar jtkech avatar justusgreiberorgadata avatar lahma avatar lukaskabrt avatar markeverard avatar matthewsre avatar michaelpetrinolis avatar noonamer avatar paynecrl97 avatar sebastianstehle avatar sebastienros avatar soar360 avatar sotvokun avatar themicster avatar ymor avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fluid's Issues

Include variables written to caller's scope

The following is an example of this behavior. You can see that the second time I include 'template' I do not set the value of varB but varB's value from the first include of 'template' appears. I also have a similar example of using with.

I'm not sure if that is how Shopify's implementation of include behaves. So I don't know that this is strictly a bug. For my purposes it causes problems, and is an unwanted side-effect.

Your thoughts on whether or not this is a bug, or how I could scope variables in a way that they don't leak from one include to another would be appreciated.

var folder = Path.Combine(Path.GetTempPath(), "IncludeScopeExample");
Directory.CreateDirectory(folder);
File.WriteAllText(Path.Combine(folder, "template.liquid"),
@"-----------------
varA = {{ varA }}
varB = {{ varB }}
template = {{ template }}
");

TemplateContext.GlobalFileProvider = new PhysicalFileProvider(folder);

var model = new { Firstname = "Bill", Lastname = "Gates" };
var source =
@"Hello {{ p.Firstname }} {{ p.Lastname }}

{% include 'template', varA: 'first call of varA', varB: 'first call of varB' %}
{% include 'template', varA: 'second call of varA' %}

{% include 'template' with 'third call of with' %}
{% include 'template' %}";

if (FluidTemplate.TryParse(source, out var template))
{
    var context = new TemplateContext();
    context.MemberAccessStrategy.Register(model.GetType());
    context.SetValue("p", model);

    Console.Write(template.Render(context));
}

Output:

Hello Bill Gates

-----------------
varA = first call of varA
varB = first call of varB
template = 
-----------------
varA = second call of varA
varB = first call of varB
template = 

-----------------
varA = second call of varA
varB = first call of varB
template = third call of with
-----------------
varA = second call of varA
varB = first call of varB
template = third call of with

Fluid.MvcSample not listing person data

I develop my project with Fluid and passing model to view
it does not display my listing data

And then i pulled and try to run Fluid.MvcSample
it' same, does not display

screen shot 2561-11-19 at 19 13 46

Implement a faster parser

Even though Fluid is the fastest liquid engine on .NET, there is still a lot to gain from having an optimized parser. Irony is very flexible to use but not fast enough.

We still need to support parser extensibility, probably giving an access to the decision tree after tokenization, so we can add custom tags with special constructs. This could even be done in a way that all the tags are actually an extension to the parser, such that the main parser logic will be to detect tags and blocks, and switch to a tag specific implementation to parse the content.

include tag behavior

The include tag only writes the contents of the included file, but does not parse the file. This varies from Shopify's include tag implementation. The with and for syntax are also not supported at this time.

At a minimum should Fluid render the contents of the included file using the existing context?

Question - Mixing Razor Pages and Fluid

Can Razor Pages and Fluid be use I'm the same application?

Example
I have a SaaS multi tenant application. The general pages I want to use Razor Pages to have full controll of the html. But, tenant specific pages I want tenants to controll their layout using Fluid.

Should {% capture %} encode its content

When capture is called, the block is encoded. Then if the captured variable is rendered it will also be encoded. To be able to not have a double-encoding issue we should then use {{ x | raw}}.

Then the content would still be encoded once. In case we need to capture html, and not encode even once, we would also need to wrap the captured block in a raw tag like this:

{% capture x %}
  {% raw %}
  some <b>bold</b> text
  {% endraw %}
{% endcapture %}
{{ x | raw }}

This looks cumbersome. Some research needs to be done on the specification and shopify/LiquidJS to understand how capture should behave, and how these frameworks handle HTML encoding.

Register JObject no work

var expression = "{{ Context.User }}";
//var expression = "{{ Context.User.Name }}";
var data = JObject.Parse("{\"User\": \"Bill\"}");
//var data = JObject.Parse("{\"User\": {\"Name\": \"Bill\"}}");

TemplateContext.GlobalMemberAccessStrategy.Register<JObject>((source, name) => source[name]);

FluidTemplate template = null;
if (FluidTemplate.TryParse(expression, out template))
{
    var context = new TemplateContext();
    context.SetValue("Context", data);

    var text = template.Render(context);

    Console.WriteLine(text);
}
<package id="Fluid.Core" version="1.0.0-beta-9422" targetFramework="net461" />

Custom tags via AddMvc?

Apologies in advance for what is probably a dumb question, I'm working with a client on a .NET Core MVC project for the first time and trying to provide a bit of guidance.

We have a project right now that's set up very similarly to the Fluid.MvcSample.

We have one custom filter and one custom tag we'd like to add. The filter was super easy, but we're having trouble with the tag.

There's a step that involves defining a new template type. But we aren't currently calling TryParse anywhere... we're calling services.AddMvc().AddFluid() from Startup.cs.

Is there a simple example available of registering a custom tag in an MVC project?

Thanks in advance for your help! ๐Ÿ™‚

Include Tag Perf

IMHO we need to cache parsing the included template instead of parse it each time

Create an MVC view engine

@jtkech is already working on it.

Right now the goal is to parse and render dynamically, and then cache the parsed AST.

Ultimately we could compile the AST to C# like Razor is doing to get the most of the performance. This should be pretty simple as there are only a few statements to support in the AST.

Static constructor invokation

Leaving this here for reference from the source code:

The static constructor in the sub classes is not called until it's instantiated or one of its members called. We can see that there are two Factory instances but the types' static constructors are not invoked. We need to call new T() in the base class static ctor to force it.

using System;

namespace TestConstructor
{
    class Program
    {
        static void Main(string[] args)
        {
            Template.Render();
            Template2.Render();
            Console.WriteLine(ReferenceEquals(Template.Factory, Template2.Factory));
        }
    }

    public class BaseTemplate<T> where T : new()
    {
        static BaseTemplate()
        {
            // new T();
            Console.WriteLine("BaseTemplate() " + typeof(T).Name);
        }

        public static object Factory = new object();
        
        public static void Render()
        {
            Console.WriteLine("Render()");
        }
    }

    public class Template : BaseTemplate<Template>
    {
        static Template()
        {
            Console.WriteLine("Static ctor()");
        }
    }

    public class Template2 : BaseTemplate<Template2>
    {
        static Template2()
        {
            Console.WriteLine("Static ctor() 2");
        }
    }
}

Feature Request: Unsafe TemplateContext

Hello:

Recently, I am porting my document generation library from NVelocity to Fluid for the .NET Core compatibility purpose. Fluid's white-list is good, but in my server side rendering circumstance, the TemplateContext's white-list and type registration thing which makes a bit inconvenient for me.

For example, I load my EntityFramework's entity from database and rendering it into a DOCX file using Fluid, to do that I have to register all complex types in the object-graph of my entity into the TemplateContext. Since my templates are made by developer and rendered in server side, there is no any security issue will be caused.

So my questions is:

Is there any way to turn off the white-list based member accessing strategy?

Custom tags

Define a way to add custom tags.

We could add support for simple ones, with just a name, or we could define something more complex to expose a mini grammar and let tags require expressions.

Support .NET Standard 1.3

Context: I'm evaluating different template engines for NSwag to generate C#/TypeScript code. I need a template engine which can load templates at runtime (this is why I want to replace the currently used T4 templates) and should work almost everywhere (full .NET 4.5.1+, .NET Core 1.0+). I wanted to use DotLiquid but @rynowak pointed me to this library - it looks much better performance wise and the white space handling seems also better for my needs.

My only problem is that it requires .NET Standard 1.6 - is it possible to change it to target .NET Standard 1.3? I checked some dependencies but couldn't find one which does not support it. Is there a reason why it targets .NET Standard 1.6?

include tag whitespace

Hi,

When using the include tag, I would expect the contents of the included liquid file to render with the whitespace of the root file appended.
For example:
Class.liquid:

...
{% for col in Table.Columns %}
        {% include 'AutoProperty' %}
{% endfor %}
...

AutoProperty.liquid:

public {{ col.PropertyType }} {{ col.PropertyName }} { get; set; }

Currently outputs like this:

public string Name { get; set; }
public int PrincipalId { get; set; }
public int DiagramId { get; set; }
public int Version { get; set; }

where I would expect this:

        public string Name { get; set; }
        public int PrincipalId { get; set; }
        public int DiagramId { get; set; }
        public int Version { get; set; }

Could not locate System.Diagnostics.DiagnosticSource, Version=4.0.0.0

Steps to reproduce:

  • Create a new .NET Core console application
  • Change the target framework in .csproj to net461
  • Add Fluid.Core NuGet package
  • Add an assembly reference to System.Net.Http
  • Compile this code in Program.cs:
class Program
{
    static void Main(string[] args)
    {
        var client = new HttpClient();
        client.GetStringAsync("https://tyrrrz.me").GetAwaiter().GetResult();
    }
}
  • Run

There seems to be some reference conflict or something.

Placeholders in custom filters

Hello,

I think it would be a great feature to allow the user to have placeholders as values inside (custom) filters.

Currently I have the Min and Max as custom filter. But when I try the following template, it does not work when I parse it with the MyTemplate parser:

image
Here is the MyTemplate parser class:

image

Is it possible to implement this feature?

Upgrading to Microsoft.Extensions.FileProviders.Abstractions v.2.1.1.0 breaks fluid

We upgraded our standard libraries all up to 2.1.1.0, after doing so fluid wouldn't work and was throwing the following:

----> System.TypeInitializationException : The type initializer for 'Fluid.TemplateContext' threw an exception.
----> System.IO.FileLoadException : Could not load file or assembly 'Microsoft.Extensions.FileProviders.Abstractions, Version=1.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

Maintain whitespace in attributes (single-line blocks)

I know that according to #42 and the README, whitespace is stripped from blocks, but this results in some unfortunate scenarios, particularly in HTML attributes.

As an example, if class is set to is-current:

<div class="Example{% if class %} {{class}}{% endif %}"></div>

Expected:

<div class="Example is-current"></div>

Actual:

<div class="Exampleis-current"></div>

One short-term fix is to move the space outside of the if block, but then the attribute will have an unnecessary trailing space when class is nil:

<div class="Example "></div>

The only way to avoid the space and get the same behavior is to prepare the attribute contents beforehand:

{% assign _class = 'Example' %}
{% if class %}
  {% assign _class = _class | append: ' ' | append: class %}
{% endif %}

<div class="{{_class}}"></div>

But that's a lot more extra logic in the template.

Since the whitespace stripping was likely implemented to reduce the amount of unnecessary spaces, I thought I'd flag the issue.

Updated with more accurate test case.

Examples or nuget are broken

I installed latest nuget, copy-paste the example and it fails to compile. "Register" method is absent. Also as I understand to template.Render() should be passed context, but it just my guess.

Validate context model calls when rendering

If I access a non-existing context property in a template, like this:

{{ nonExistingProperty }}

then I don't get any errors when calling FluidTemplate.Render. Such objects are just ignored and not rendered.

Is there a way to validate them and either throw exception or render error text in a place of invalid call?

Thanks in advance

Incorrect position in validation messages

When parsing the following block of code:

<div>
	<span>
		{{{ model }}
	</span>
</div>

I'm getting an error: Invalid character: '{'. at line:0, col:2

This is a little unclear, since the error actually occurred in line 2. It looks like the parser returns character position in this particular Liquid expression, but not in the whole template.

It would be great to track the original position.

Using layout/section tags without using Fluid as ViewEngine

We are in a pure API stack and use fluid to generate E-Mail contents.
We would like to use the extensibility features of the templates (namely layout, renderbody, section and rendersection) to compose our e-mails, so my question is, whether this can be easily accomplished.

I tried simply referencing Fluid.MvcViewEngine and calling the following:

    FluidViewTemplate.TryParse(template, out var parsedTemplate, out var parseErrors)
    var context = new TemplateContext();
    // setting up context here ...
    var renderedTemplate = await parsedTemplate.RenderAsync(context);

And while the TryParse works (meaning it is recognizing the template tag), that RenderAsync call fails with

   at System.ThrowHelper.ThrowKeyNotFoundException()
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Fluid.MvcViewEngine.Tags.LayoutTag.<WriteToAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Fluid.BaseFluidTemplate`1.<RenderAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Fluid.FluidTemplateExtensions.<RenderAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at DocuSafe.Services.TemplateEngine.<RenderAsync>d__3`1.MoveNext() in C:\Development\docu-safe\DocuSafe\src\DocuSafe.Services\TemplateEngine.cs:line 45

I probably miss quite some setup which is normally done in the "AddFluid" call in the startup.

Custom ellipsis with truncate & truncatewords

According to shopify docs, we can add a custom ellipsis for both truncate and truncatewords
Input

{{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | truncate: 18, ", and so on" }}
{{ "The cat came back the very next day" | truncatewords: 4, "--" }}

Output

ABCDEFG, and so on
The cat came back--

Also we can support no ellipsis by using empty string as the following:

{{ "I'm a little teapot, short and stout." | truncate: 15, "" }}
{{ "The cat came back the very next day" | truncatewords: 4, "" }}

Add a timeout for rendering

Or limit how many statements can be executed.
There would otherwise be a risk of taking a server down if a script is badly written.

See Jint on how to implement it.

Bug: variable values assigned inside blocks revert to their previous value after exiting the block

Repro: change the for loop in the Index.liquid file in the sample project to this:

{% assign secondPerson = Model | first %}
{% for p in Model %}
    <p>
        {{ p.Firstname }} {{ p.Lastname }}
        {% if forloop.index == 2 %}
            {% assign secondPerson = p %}
            (This is the second person - {{ secondPerson.Firstname }} {{ secondPerson.Lastname }})
        {% endif %}
    </p>
{% endfor %}

Second person was: {{ secondPerson.Firstname }} {{ secondPerson.Lastname }}

We create a variable, and assign it a value before the for loop. We then assign a new value to the variable inside the for loop. After the for loop, the value of the variable should be what we assigned it in the for loop, but it is actually the value we assigned before we entered the loop.

Possibly caused by this PR: #48.

Add FluidTemplate.Parse

...which will throw an exception with parse errors if the input template is invalid.
This is useful when the templates are static and are never expected to have parsing errors.

Liquid within comment executes

Should Liquid within a comment still execute or should a comment prevent execution? For Shopify's implementation, code will not execute:

comment
Allows you to leave un-rendered code inside a Liquid template. Any text within the opening and closing comment blocks will not be output, and any Liquid code within will not be executed. https://help.shopify.com/themes/liquid/tags/theme-tags#comment

Fluid example:

{% comment %}
I don't get printed but this code is executed. That is, `programs` is assigned a value, and all that.
{% assign programs = '[ {"id": 1, "iconUrl": "http://lorempixel.com/output/animals-q-c-640-480-3.jpg", "title": "One" } ]' | parse %}
{% endcomment %}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.