Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Liquid templating

liquid templating

What is a template engine?

  • We would like to create several texts (e.g. web pages, content of email, some report) that look exactly the same but will have different data.

  • We design the page, but instead of values we use vairables (or placeholders, if you prefer that word) that look like this: {{ ttitle }}.

  • The template engine then can replace those placeholder variables by the appropriate value.

  • Besides replacing individual values, a template system usually has more complex syntax as well. Loops, conditionals, includes etc.

Install

cargo add liquid

This will update the Cargo.toml to include:

[dependencies]
liquid = "0.26.4"

Liquid use-cases

Liquid Hello World

  • parse

  • build

  • object!

  • render

  • Depened on the liquid crate

[package]
name = "liquid-hello-world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
liquid = "0.26"
  • Start with a template that is part of the Rust source code.
  • We use the parse and build methods to create the template object.
  • We use unwrap here which is probably not ideal, but it simlifies the examples.
  • Using the liquid::object! macro we create an object from the data we would like to pass to the template.
  • Using the render method we combine the data with the template and generate (render) the resuling text.
fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("Welcome to {{name}}")
        .unwrap();

    let globals = liquid::object!({
        "name": "Liquid"
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
}
Welcome to Liquid

Liquid Hello World with variable

  • Using the same template as earlier we see how we can reuse the template in 3 different ways:

  • The value of "name" is hard-coded in the call to object!

  • The value of "name" is hard-coded in a variable as str.

  • The value of "name" is a String that could come from the outside world (e.g. from a file).

Welcome to Liquid
Welcome to Liquid - 2
Welcome to Liquid - 3
fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("Welcome to {{name}}")
        .unwrap();

    let globals = liquid::object!({
        "name": "Liquid"
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);

    // 2nd
    let name = "Liquid - 2";
    let globals = liquid::object!({
        "name": name
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);

    // 3rd
    let name = String::from("Liquid - 3");
    let globals = liquid::object!({
        "name": name
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

Liquid Hello World read template from file

  • parse_file

  • Templates are usually much biggger than what we had in the first example.

  • We usually prefer to keep the templates as an external files.

  • Instead of parse we can use parse_file to load the template from an external file.

  • This happens at run-time so you will need to supply the templates outside of the binary or the user will need to create the templates.

Hello Liquid!

fn main() {
    let filename = "template.txt";
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse_file(filename)
        .unwrap();

    let name = String::from("Liquid");
    let globals = liquid::object!({
        "name": name
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

{% embed include file="src/examples/liquid/liquid-hello-world-from-file/template.txt)

Liquid Hello World embed template file

  • parse

  • include_str!

  • If you would like to supply the temlates, probably the easiest is to embed them in the binary.

  • Using include_str! we can embed a text-file in the compiled binary of our Rust code.

  • In the source repository we have the templates as external files, but during build they are embedded in the code.

fn main() {
    let template = include_str!("../template.txt");
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let name = String::from("Liquid");
    let globals = liquid::object!({
        "name": name
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}
Hello {{name}}!

Liquid flow control: if - else

  • if

  • else

  • endif

  • Liquid has simple conditionals: if that we end with endif and the optional else.

fn main() {
    let template = "
        {% if at_home %}
           {{name}} is at home
        {% else %}
           {{name}} is NOT at home
        {% endif %}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    // 1st
    let globals = liquid::object!({
        "name": "Foo Bar",
        "at_home": true,
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);

    // 2nd
    let globals = liquid::object!({
        "name": "Peti Bar",
        "at_home": false,
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

        
           Foo Bar is at home
        
    

        
           Peti Bar is NOT at home
        
    

Liquid flow control: else if written as elsif

  • elsif
fn main() {
    let template = r#"
        {% if color == "blue" %}
            blue
        {% elsif color == "green" %}
            green
        {% else %}
            Unrecognized color
        {% endif %}
    "#;

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    // 1st
    let globals = liquid::object!({
        "color": "blue",
    });
    let output = template.render(&globals).unwrap();
    println!("{output}");
    assert_eq!(output.trim(), "blue");

    // 2nd
    let globals = liquid::object!({
        "color": "green",
    });
    let output = template.render(&globals).unwrap();
    println!("{output}");
    assert_eq!(output.trim(), "green");

    // 3rd
    let globals = liquid::object!({
        "color": "red",
    });
    let output = template.render(&globals).unwrap();
    println!("{output}");
    assert_eq!(output.trim(), "Unrecognized color");
}

        
            blue
        
    

        
            green
        
    

        
            Unrecognized color
        
    

Liquid flow control: case/when

  • case

  • when

  • endcase

  • the case statement ends with endcase.

fn main() {
    let template = r#"
      {% case color %}
        {% when "blue" %}
            blue
        {% when "green" %}
            green
        {% else %}
            Unrecognized color
      {% endcase %}
    "#;

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    // 1st
    let globals = liquid::object!({
        "color": "blue",
    });
    let output = template.render(&globals).unwrap();
    println!("{output}");
    assert_eq!(output.trim(), "blue");

    // 2nd
    let globals = liquid::object!({
        "color": "green",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
    assert_eq!(output.trim(), "green");

    // 3rd
    let globals = liquid::object!({
        "color": "red",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
    assert_eq!(output.trim(), "Unrecognized color");
}

      
            blue
        
    

      
            green
        
    

      
            Unrecognized color
      
    

Liquid passing more complex data

fn main() {
    let template = "
        {{animal.name}}
        {{animal.height}}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let globals = liquid::object!({
        "animal": {
            "name": "elephant",
            "height": 320,
        }
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

        elephant
        320
    

Liquid for loop passing a vector or an array

  • for

  • endfor

  • We are probably more interested in passing values from variables.

  • In this example we pass a vector of strings.

fn main() {
    let template = "
        {% for color in colors %}
           {{color}}
        {% endfor %}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    //let colors: [&str; 3] = ["red", "green", "blue"];
    let colors = vec!["red", "green", "blue"];

    let globals = liquid::object!({
        "colors": colors,
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

        
           red
        
           green
        
           blue
        
    

Liquid vector of tuples

  • Another example passing in a vector, but this time a vector of tuples.
  • We use the square-brackets [] and indexes to access the elements of a tuple.
fn main() {
    let template = "
        {% for color in colors %}
           {{ color[0] }} - {{ color[1] }}
        {% endfor %}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let colors = vec![("red", 23), ("green", 17), ("blue", 42)];

    let globals = liquid::object!({
        "colors": colors,
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

        
           red - 23
        
           green - 17
        
           blue - 42
        
    

Liquid HashMap

  • HashMap

  • We can pass in a HashMap and inside we can iterate over the key-value pairs as tuples.

  • So here too we use the [] with index 0 and 1 to access the key and the value.

use std::collections::HashMap;

fn main() {
    let template = "
        {% for color in colors %}
           {{ color[0] }} - {{ color[1] }}
        {% endfor %}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let colors: HashMap<&str, u32> = HashMap::from([("red", 23), ("green", 17), ("blue", 42)]);
    println!("{colors:#?}");

    let globals = liquid::object!({
        "colors": colors,
    });
    let output = template.render(&globals).unwrap();
    println!("{output}");
    assert!(output.contains("blue - 42"));
    assert!(output.contains("red - 23"));
    assert!(output.contains("green - 17"));
}
{
    "blue": 42,
    "red": 23,
    "green": 17,
}

        
           green - 17
        
           blue - 42
        
           red - 23
        
    

Liquid for loop with if conditions

  • for
  • endfor
  • if
  • endif
fn main() {
    let template = "
       {% for animal in animals %}
            {% if animal.real %}
                A real {{animal.name}}
            {% else %}
                A fake {{animal.name}}
            {% endif %}
        {% endfor %}
    ";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let globals = liquid::object!({
        "animals": [
            {
                "name": "mouse",
                "real": true,
            },
            {
                "name": "snake",
                "real": true,
            },
            {
                "name": "oliphant",
                "real": false,
            },
        ],
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

       
            
                A real mouse
            
        
            
                A real snake
            
        
            
                A fake oliphant
            
        
    

Liquid with struct

  • serde
[package]
name = "liquid-hello-world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
liquid = "0.26"
serde = { version = "1.0", features = ["derive"] }

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Car {
    manufacturer: String,
    electric: bool,
    gears: i8,
    names: Vec<String>,
}

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(
            "
            Car manufacturer: {{car.manufacturer}}
            {% if car.electric %}
                electric
            {% endif %}
            Gears: {{car.gears}}
            {% for name in car.names %}
               {{name}}
            {% endfor %}
        ",
        )
        .unwrap();

    let car = Car {
        manufacturer: String::from("Ford"),
        electric: false,
        gears: 5,
        names: vec![String::from("Mustang"), String::from("Anglia")],
    };
    //println!("manufacturer: {}", car.manufacturer);
    //println!("electric: {}", car.electric);

    let globals = liquid::object!(
    {
        "car": car, //liquid::to_object(&car),
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
}

            Car manufacturer: Ford
            
            Gears: 5
            
               Mustang
            
               Anglia
            
        

Liquid with Option in a struct

  • Option
[package]
name = "liquid-with-option"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
liquid = "0.26"
serde = { version = "1.0", features = ["derive"] }
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Details {
    weight: u32,
    length: u32,
}

#[derive(Serialize, Deserialize)]
struct Car {
    manufacturer: String,
    color: Option<String>,
    details: Option<Details>,
}

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(
            "
            Car manufacturer: {{car.manufacturer}}
            Always color: {{ car.color }}
            {% if car.color %}
                Color: {{ car.color }}
            {% else %}
                no color
            {% endif %}

            {% if car.details %}
                Weight: {{ car.details.weight }}
            {% endif %}
        ",
        )
        .unwrap();

    let car = Car {
        manufacturer: String::from("Ford"),
        color: Some(String::from("blue")),
        details: Some(Details {
            weight: 1000,
            length: 400,
        }),
    };

    let globals = liquid::object!(
    {
        "car": car,
    });
    let output = template.render(&globals).unwrap();

    println!("{output}");

    let car = Car {
        manufacturer: String::from("Ford"),
        color: None,
        details: None,
    };

    let globals = liquid::object!(
    {
        "car": car,
    });
    let output = template.render(&globals).unwrap();

    println!("{output}");
}

            Car manufacturer: Ford
            Always color: blue
            
                Color: blue
            

            
                Weight: 1000
            
        

            Car manufacturer: Ford
            Always color: 
            
                no color
            

            
        

Liquid include

  • include
  • partials
  • read_to_string
{% include 'templates/incl/header.txt' %}

title in page template: {{title}}
name in page template: {{name}}

title in header: {{title}}
value in header: {{value}}
use liquid::partials::{EagerCompiler, InMemorySource};
use std::fs::read_to_string;

pub type Partials = EagerCompiler<InMemorySource>;

fn main() {
    let mut partials = Partials::empty();
    let filename = "templates/incl/header.txt";
    let template = read_to_string(filename).unwrap();
    partials.add(filename, template);

    let template = liquid::ParserBuilder::with_stdlib()
        .partials(partials)
        .build()
        .unwrap()
        .parse_file("templates/page.txt")
        .unwrap();

    let globals = liquid::object!({
        "title": "Liquid",
        "name": "Foo Bar",
        "value": "some value",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}
title in header: Liquid
value in header: some value


title in page template: Liquid
name in page template: Foo Bar


Liquid include header and footer

  • include
title in header: Liquid
value in header: some value for the header


title in page template: Liquid
name in page template: Foo Bar

value in footer: some value for the footer


use std::fs::File;
use std::io::Read;

pub type Partials = liquid::partials::EagerCompiler<liquid::partials::InMemorySource>;

fn main() {
    let mut partials = Partials::empty();
    let filename = "templates/incl/header.txt";
    partials.add(filename, read_file(filename));
    let filename = "templates/incl/footer.txt";
    partials.add(filename, read_file(filename));

    let template = liquid::ParserBuilder::with_stdlib()
        .partials(partials)
        .build()
        .unwrap()
        .parse_file("templates/page.txt")
        .unwrap();

    let globals = liquid::object!({
        "title": "Liquid",
        "name": "Foo Bar",
        "header_value": "some value for the header",
        "footer_value": "some value for the footer",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

fn read_file(template_file: &str) -> String {
    let mut template = String::new();
    match File::open(template_file) {
        Ok(mut file) => {
            file.read_to_string(&mut template).unwrap();
        }
        Err(error) => {
            println!("Error opening file {}: {}", template_file, error);
        }
    }
    template
}
value in footer: {{footer_value}}
title in header: {{title}}
value in header: {{header_value}}
{% include 'templates/incl/header.txt' %}

title in page template: {{title}}
name in page template: {{name}}

{% include 'templates/incl/footer.txt' %}

Liquid layout (include and capture)

  • include
  • capture


title in header: Liquid
value in header: some value for the header


title in page template: Liquid
name in page template: Foo Bar


value in footer: some value for the footer


use std::fs::File;
use std::io::Read;

pub type Partials = liquid::partials::EagerCompiler<liquid::partials::InMemorySource>;

fn main() {
    let mut partials = Partials::empty();
    let filename = "templates/layout.txt";
    partials.add(filename, read_file(filename));

    let template = liquid::ParserBuilder::with_stdlib()
        .partials(partials)
        .build()
        .unwrap()
        .parse_file("templates/page.txt")
        .unwrap();

    let globals = liquid::object!({
        "title": "Liquid",
        "name": "Foo Bar",
        "header_value": "some value for the header",
        "footer_value": "some value for the footer",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}

fn read_file(template_file: &str) -> String {
    let mut template = String::new();
    match File::open(template_file) {
        Ok(mut file) => {
            file.read_to_string(&mut template).unwrap();
        }
        Err(error) => {
            println!("Error opening file {}: {}", template_file, error);
        }
    }
    template
}
title in header: {{title}}
value in header: {{header_value}}

{{content}}

value in footer: {{footer_value}}
{% capture content %}
title in page template: {{title}}
name in page template: {{name}}
{% endcapture %}

{% include 'templates/layout.txt' %}

Liquid assign to variable in template

  • assign
  • set
  • let
use liquid::partials::{EagerCompiler, InMemorySource};
use std::fs::read_to_string;

pub type Partials = EagerCompiler<InMemorySource>;

fn main() {
    let mut partials = Partials::empty();
    let filename = "templates/incl/header.txt";
    let template = read_to_string(filename).unwrap();
    partials.add(filename, template);

    let template = liquid::ParserBuilder::with_stdlib()
        .partials(partials)
        .build()
        .unwrap()
        .parse_file("templates/page.txt")
        .unwrap();

    let globals = liquid::object!({
        "title": "Liquid",
        "name": "Foo Bar",
        "value": "some value",
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
}
{% assign title = "Other title" %}
{% include 'templates/incl/header.txt' %}

title in page template: {{title}}
name in page template: {{name}}

title in header: {{title}}
value in header: {{value}}

title in header: Other title
value in header: some value


title in page template: Other title
name in page template: Foo Bar


Liquid filters on strings: upcase, downcase, capitalize

  • upcase
  • downcase
  • capitalize
fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(
            "
           plain: {{text}}
           upcase: {{text | upcase}}
           downcase: {{text | downcase}}
           capitalize: {{text | capitalize}}
        ",
        )
        .unwrap();

    let text = "this is Some tExt";

    let globals = liquid::object!({
        "text": text,
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
}

           plain: this is Some tExt
           upcase: THIS IS SOME TEXT
           downcase: this is some text
           capitalize: This is Some tExt
        

Liquid filters on numbers: plus, minus

  • plus

  • minus

  • Some filters can have parameters as well.

  • Increment or decerement the number by the given number.

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(
            "
           plain: {{whole}}
           plus 2: {{whole | plus : 2}}
           minus 2 {{whole | minus : 2}}

           plain: {{float}}
           plus 2: {{float | plus : 2}}
           minus 2 {{float | minus : 2}}
        ",
        )
        .unwrap();

    let whole = 42;
    let float = 4.2;

    let globals = liquid::object!({
        "whole": whole,
        "float": float,
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
}

           plain: 42
           plus 2: 44
           minus 2 40

           plain: 4.2
           plus 2: 6.2
           minus 2 2.2
        

Liquid filters: first, last

  • first
  • last

first or last

  • character in a string
  • element in an array, a vector, or a tuple
fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(
            "
           plain: {{text}}
           first: {{text | first}}
           last: {{text | last}}

           plain: {{words}}
           first: {{words | first}}
           last: {{words | last}}

           plain: {{numbers}}
           first: {{numbers | first}}
           last: {{numbers | last}}

           plain: {{tpl}}
           first: {{tpl | first}}
           last: {{tpl | last}}
       ",
        )
        .unwrap();

    let text = "This is some text";
    let words = ["These", "are", "words", "in", "an", "array"];
    let numbers = vec![7, 3, 19, 4];
    let tpl = ("foo", 42, "bar", 3.4);

    let globals = liquid::object!({
        "text": text,
        "words": words,
        "numbers": numbers,
        "tpl": tpl,
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
}

           plain: This is some text
           first: T
           last: t

           plain: Thesearewordsinanarray
           first: These
           last: array

           plain: 73194
           first: 7
           last: 4

           plain: foo42bar3.4
           first: foo
           last: 3.4
       

Liquid filter reverse array

fn main() {
    let result = render("direct: {{ items }}");
    println!("{}", result);
}

fn render(tmpl: &str) -> String {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    let globals = liquid::object!({
        "items": vec![2, 3, 4],
    });
    template.render(&globals).unwrap()
}

#[test]
pub fn test_reverse() {
    let result = render("direct: {{ items }}");
    assert_eq!(result, "direct: 234");

    let result = render("reversed: {{ items | reverse }}");
    assert_eq!(result, "reversed: 432");

    let result = render("direct: {% for item in items %}{{item}} {% endfor %}");
    assert_eq!(result, "direct: 2 3 4 ");

    let result = render("reversed: {% assign ritems = items | reverse %}{% for item in ritems %}{{item}} {% endfor %}");
    assert_eq!(result, "reversed: 4 3 2 ");

    let result = render("reversed: {% for item in items reversed %}{{item}} {% endfor %}");
    assert_eq!(result, "reversed: 4 3 2 ");
}

Liquid for loop: limit, offset, reversed

  • limit
  • offset
  • reversed
fn main() {
    let result = render("direct: {% for item in items %}{{item}} {% endfor %}");
    println!("{}", result);
}

fn render(tmpl: &str) -> String {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    let globals = liquid::object!({
        "items": vec![2, 3, 4, 5, 6, 7, 8],
    });
    template.render(&globals).unwrap()
}

#[test]
pub fn test_reverse() {
    let result = render("direct: {% for item in items %}{{item}} {% endfor %}");
    assert_eq!(result, "direct: 2 3 4 5 6 7 8 ");

    let result = render("limit: {% for item in items limit: 4 %}{{item}} {% endfor %}");
    assert_eq!(result, "limit: 2 3 4 5 ");

    let result = render("offset: {% for item in items offset: 2 %}{{item}} {% endfor %}");
    assert_eq!(result, "offset: 4 5 6 7 8 ");

    let result =
        render("offset and limit: {% for item in items offset: 2 limit: 4 %}{{item}} {% endfor %}");
    assert_eq!(result, "offset and limit: 4 5 6 7 ");

    // https://github.com/cobalt-org/liquid-rust/issues/274
    // let result = render("continue: {% for item in items limit: 3 %}{{item}} {% endfor %}new: {% for item in items offset:continue limit: 3 %}{{item}} {% endfor %}");
    // assert_eq!(result, "continue: 2 3 4 new: 5 6 7 ");

    let result = render("reversed: {% for item in items reversed %}{{item}} {% endfor %}");
    assert_eq!(result, "reversed: 8 7 6 5 4 3 2 ");
}
direct: 2 3 4 5 6 7 8 

Liquid comma between every two elements (forloop.last)

  • length
  • index (numbers start from 1)
  • index0 (numbers start from 0)
  • rindex
  • rindex0
  • first
  • last
fn main() {
    let result = render("direct: {% for item in items %}{{item}}, {% endfor %}");
    println!("{}", result);
}

fn render(tmpl: &str) -> String {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    let globals = liquid::object!({
        "items": vec![2, 3, 4, 5, 6, 7, 8],
    });
    template.render(&globals).unwrap()
}

#[test]
pub fn test_reverse() {
    let result = render("direct: {% for item in items %}{{item}}, {% endfor %}");
    assert_eq!(result, "direct: 2, 3, 4, 5, 6, 7, 8, ");

    let result = render("direct: {% for item in items %}{{item}}{% if forloop.last %}{% else %}, {% endif %}{% endfor %}");
    assert_eq!(result, "direct: 2, 3, 4, 5, 6, 7, 8");
}
direct: 2, 3, 4, 5, 6, 7, 8, 

Liquid: create your own filter: reverse a string

This is using the liquid-filter-reverse-string. Look at its source code

[package]
name = "liquid-filter-reverse-string-use"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
liquid = "0.26"
liquid-filter-reverse-string = "0.1"
use liquid_filter_reverse_string::ReverseStr;

fn main() {
    let template = "reversed: {{text | reversestr}}";
    let text = "Hello World!";

    let result = render(template, text);
    println!("{}", result);
    assert_eq!(result, "reversed: !dlroW olleH");
}

fn render(tmpl: &str, text: &str) -> String {
    let globals = liquid::object!({
        "text": text,
    });

    let template = liquid::ParserBuilder::with_stdlib()
        .filter(ReverseStr)
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    template.render(&globals).unwrap()
}

Liquid: create your own filter: commafy

[package]
name = "liquid-filter-commafy-use"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
liquid = "0.26"
liquid-filter-commafy = "0.1"
use liquid_filter_commafy::Commafy;

fn main() {
    assert_eq!(
        "2,345",
        render("{{value | commafy}}", liquid::object!({ "value": 2345 }))
    );
    assert_eq!(
        "123,456",
        render("{{value | commafy}}", liquid::object!({ "value": 123456 }))
    );
    assert_eq!(
        "123,456",
        render(
            "{{value | commafy}}",
            liquid::object!({ "value": "123456" })
        )
    );
}

fn render(tmpl: &str, glob: liquid::Object) -> String {
    let template = liquid::ParserBuilder::with_stdlib()
        .filter(Commafy)
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    template.render(&glob).unwrap()
}

Liquid: length of string, size of vector

  • len

  • size

  • Sometimes we would like to display or compare the length of a string or the number of elements in a vector.

  • We can do that using the size attribute.

fn main() {
    let text = "Some text";
    let animals = vec!["cat", "dog", "snake"];
    println!("{}", text.len());
    println!("{}", animals.len());
    assert_eq!(text.len(), 9);
    assert_eq!(animals.len(), 3);

    let template = "text: {{ text.size }} animals: {{ animals.size }}. The string is {% if text.size > 10 %}long{% else %}short{% endif %}.";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(template)
        .unwrap();

    let globals = liquid::object!({
        "text": text,
        "animals": animals,
    });
    let output = template.render(&globals).unwrap();

    println!("{}", output);
    assert_eq!(output, "text: 9 animals: 3. The string is short.");
}
9
3
text: 9 animals: 3. The string is short.

Liquid: Embed HTML - escape HTML

By default liquid inserts values as they are.

This means if a value we use in a template contains any HTML special character, that will be included in the resulting HTML. This can break the HTML and can open your site to HTML injection attack.

We can use the escape filter on each field where we would like to avoid this.

fn main() {
    plain_text();
    embed_html();
    escape_html();
}

fn plain_text() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("<h1>Welcome to {{field}}</h1>")
        .unwrap();

    let globals = liquid::object!({
        "field": "Liquid"
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
    assert_eq!(output, "<h1>Welcome to Liquid</h1>");
}

fn embed_html() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("<h1>Welcome to {{field}}</h1>")
        .unwrap();

    let globals = liquid::object!({
        "field": "<>"
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
    assert_eq!(output, "<h1>Welcome to <></h1>");
}

fn escape_html() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse("<h1>Welcome to {{field | escape}}</h1>")
        .unwrap();

    let globals = liquid::object!({
        "field": "<>"
    });
    let output = template.render(&globals).unwrap();
    println!("{}", output);
    assert_eq!(output, "<h1>Welcome to &lt;&gt;</h1>");
}
<h1>Welcome to Liquid</h1>
<h1>Welcome to <></h1>
<h1>Welcome to &lt;&gt;</h1>

Liquid tags

  • {% something %} is called a tag in Liquid.

  • There are several built-in tags in Liquid: if, else, endif, for, assign etc.

  • You can also define your own tags. In the next few examples we are going to do that.

  • {% tagname %} just a tagname.

  • {% tagname value %} tag with a single value.

  • {% tagname number number %} tag with two numbers.

  • {% youtube apple banana peach %} tag with one or more values.

  • {% tagname key="value" %} or {% tagname key=42 %} tag with single key-value pair.

  • {% youtube id="K6EvVvYnjrY" filename="some_name.mp4" %} tag with two key-value pairs. The second one optional.

  • {% include file="example/code.py" %} override the built-in include tag.

  • Use scalar values passed to the render function.

  • Use vector passed to the render function.

  • {% latest limit=5 %} tag with key-value where the value must be a u8.

  • {% latest limit=3 tag="programming" %} tag with key-value (where the value is a u8) and an optional key-value pair.

  • TODO:

  • {% tagname value value ... %} tag with multiple values

  • {% tagname key=value key=value %} tag with multiple key-value pairs

Liquid create your own tag without parameters

  • expect_nothing

This is probably the simplest example of extending the Liquid syntax by new tags. I am not sure how usefule is this in the real world as I think the same could be done with the include tag, but this might help understanding how to create more complex tags.

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
  • We need to add the struct implementing our tag (single_tag::SingleTag) to our parser using the tag method.
  • Then we can use the {% single %} tag in our template.
mod single_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(single_tag::SingleTag)
        .build()
        .unwrap()
        .parse("Liquid: {% single %}")
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(
        output,
        "Liquid: Single replaced by this string.".to_string()
    );
}
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct SingleTag;

impl TagReflection for SingleTag {
    fn tag(&self) -> &'static str {
        "single"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for SingleTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        arguments.expect_nothing()?;
        Ok(Box::new(Single))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct Single;

impl Renderable for Single {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        write!(writer, "Single replaced by this string.").replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("single".to_string(), SingleTag.into());
        options
    }

    #[test]
    fn simple() {
        let options = options();
        let template = parser::parse("{% single %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, "Single replaced by this string.");
    }
}
}

Liquid create your own tag with a single parameter

  • expect_next

  • expect_identifier

  • Convert this {% youtube K6EvVvYnjrY %} into a link to the video or maybe to an html section that embeds the video in the page.

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
mod youtube_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(youtube_tag::YouTubeTag)
        .build()
        .unwrap()
        .parse("Video: {% youtube K6EvVvYnjrY %}")
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(
        output,
        r#"Video: <a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a>"#.to_string()
    );
}
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct YouTubeTag;

impl TagReflection for YouTubeTag {
    fn tag(&self) -> &'static str {
        "youtube"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for YouTubeTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        let id = arguments
            .expect_next("Identifier expected.")?
            .expect_identifier()
            .into_result()?
            .to_string();

        // no more arguments should be supplied, trying to supply them is an error
        arguments.expect_nothing()?;

        Ok(Box::new(YouTube { id }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct YouTube {
    id: String,
}

impl Renderable for YouTube {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        write!(
            writer,
            r#"<a href="https://www.youtube.com/watch?v={}">video</a>"#,
            self.id
        )
        .replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("youtube".to_string(), YouTubeTag.into());
        options
    }

    #[test]
    fn youtube() {
        let options = options();
        let template = parser::parse("{% youtube   K6EvVvYnjrY %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a>"#
        );
    }
}
}

Liquid create your own tag with many values

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
mod youtube_tag;

fn main() {
    one();
    two();
}

fn one() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(youtube_tag::YouTubeTag)
        .build()
        .unwrap()
        .parse("Video: {% youtube K6EvVvYnjrY %}")
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(
        output,
        r#"Video: <a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a>"#.to_string()
    );
}

fn two() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(youtube_tag::YouTubeTag)
        .build()
        .unwrap()
        .parse("Video: {% youtube   R2_D2    K6EvVvYnjrY %}")
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(
        output,
        r#"Video: <a href="https://www.youtube.com/watch?v=R2_D2">video</a><a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a>"#.to_string()
    );
}
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::Error;
use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct YouTubeTag;

impl TagReflection for YouTubeTag {
    fn tag(&self) -> &'static str {
        "youtube"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for YouTubeTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        let mut ids = vec![];
        loop {
            let item = arguments.next();
            match item {
                None => break,
                Some(item) => {
                    let id = item.expect_identifier().into_result()?.to_string();

                    ids.push(id);
                }
            }
        }

        if ids.is_empty() {
            return Err(Error::with_msg("No video id provided"));
        }

        arguments.expect_nothing()?;

        Ok(Box::new(YouTube { ids }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct YouTube {
    ids: Vec<String>,
}

impl Renderable for YouTube {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        for id in &self.ids {
            write!(
                writer,
                r#"<a href="https://www.youtube.com/watch?v={}">video</a>"#,
                id
            )
            .replace("Failed to render")?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("youtube".to_string(), YouTubeTag.into());
        options
    }

    #[test]
    fn youtube() {
        let options = options();
        let template = parser::parse("{% youtube   K6EvVvYnjrY %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a>"#
        );
    }

    #[test]
    fn youtube_2() {
        let options = options();
        let template = parser::parse("{% youtube   K6EvVvYnjrY   R2_D2 %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a><a href="https://www.youtube.com/watch?v=R2_D2">video</a>"#
        );
    }

    #[test]
    fn youtube_3() {
        let options = options();
        let template = parser::parse("{% youtube  qqrq K6EvVvYnjrY   R2_D2 %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<a href="https://www.youtube.com/watch?v=qqrq">video</a><a href="https://www.youtube.com/watch?v=K6EvVvYnjrY">video</a><a href="https://www.youtube.com/watch?v=R2_D2">video</a>"#
        );
    }

    #[test]
    fn youtube_missing() {
        let options = options();
        let err = parser::parse("{% youtube %}", &options)
            .map(runtime::Template::new)
            .err()
            .unwrap();
        let err = err.to_string();
        assert_eq!(err, "liquid: No video id provided\n");
    }
}
}

Liquid create your own tag accepting two numbers

  • expect_literal

  • After the tag we have two values that are expected to be literal values that ar i32 numbers: {% add 19 23 %}

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::parser::TryMatchToken;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::ValueView;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct AddNumbersTag;

impl TagReflection for AddNumbersTag {
    fn tag(&self) -> &'static str {
        "add"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for AddNumbersTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        let literal = arguments
            .expect_next("Identifier expected.")?
            .expect_literal();

        let a = match literal {
            TryMatchToken::Matches(name) => name.to_kstr().into_string().parse::<i32>().unwrap(),
            TryMatchToken::Fails(name) => return name.raise_error().into_err(),
        };

        let literal = arguments
            .expect_next("Identifier expected.")?
            .expect_literal();

        let b = match literal {
            TryMatchToken::Matches(name) => name.to_kstr().into_string().parse::<i32>().unwrap(),
            TryMatchToken::Fails(name) => return name.raise_error().into_err(),
        };

        // no more arguments should be supplied, trying to supply them is an error
        arguments.expect_nothing()?;

        Ok(Box::new(AddNumbers { a, b }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct AddNumbers {
    a: i32,
    b: i32,
}

impl Renderable for AddNumbers {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        write!(writer, "{} + {} = {}", self.a, self.b, self.a + self.b)
            .replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("add".to_string(), AddNumbersTag.into());
        options
    }

    #[test]
    fn add_numbers() {
        let options = options();
        let template = parser::parse("{% add   19   23 %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, "19 + 23 = 42".to_string());
    }

    #[test]
    fn add_negative_numbers() {
        let options = options();
        let template = parser::parse("{% add   -19   -23 %}", &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, "-19 + -23 = -42".to_string());
    }
}
}
mod add_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(add_tag::AddNumbersTag)
        .build()
        .unwrap()
        .parse("{% add 2 4 %}")
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(output, "2 + 4 = 6");
}

Liquid create your own tag with attribute as key=value pair

  • {% youtube id = "R2_D2" %}
[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
mod youtube_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(youtube_tag::YouTubeTag)
        .build()
        .unwrap()
        .parse(r#"Video: {% youtube id="hello" %}"#)
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(
        output,
        r#"Video: <a href="https://youtube.com/watch?v=hello">video</a>"#
    );
}
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::parser::TryMatchToken;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::ValueView;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct YouTubeTag;

impl TagReflection for YouTubeTag {
    fn tag(&self) -> &'static str {
        "youtube"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for YouTubeTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        let field = arguments
            .expect_next("Identifier expected.")?
            .expect_identifier()
            .into_result()?
            .to_string();
        if field != "id" {
            return Err(liquid_core::error::Error::with_msg("Expected id"));
        }

        arguments
            .expect_next("Assignment operator \"=\" expected.")?
            .expect_str("=")
            .into_result_custom_msg("Assignment operator \"=\" expected.")?;

        let token = arguments.expect_next("Identifier or value expected")?;
        let value = match token.expect_literal() {
            TryMatchToken::Matches(name) => name.to_kstr().into_string(),
            TryMatchToken::Fails(name) => return name.raise_error().into_err(),
        };

        let id = value;

        // no more arguments should be supplied, trying to supply them is an error
        arguments.expect_nothing()?;

        Ok(Box::new(YouTube { id }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct YouTube {
    id: String,
}

impl Renderable for YouTube {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        write!(
            writer,
            r#"<a href="https://youtube.com/watch?v={}">video</a>"#,
            self.id
        )
        .replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("youtube".to_string(), YouTubeTag.into());
        options
    }

    #[test]
    fn youtube() {
        let options = options();
        let template = parser::parse(r#"{% youtube id = "R2_D2" %}"#, &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<a href="https://youtube.com/watch?v=R2_D2">video</a>"#
        );
    }
}
}

Liquid create tag that uses scalar values passed to the render function

  • How to get the values passed by the caller to the render function inside the tag definition?
[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
mod show_tag;

fn main() {
    show_one();
    show_two();
}

fn show_one() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(show_tag::ShowTag)
        .build()
        .unwrap()
        .parse(r#"{% show name %}"#)
        .unwrap();

    let globals = liquid::object!({"name": "Sancho Panza"});

    let output = template.render(&globals).unwrap();
    assert_eq!(output, "Sancho Panza");
}

fn show_two() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(show_tag::ShowTag)
        .build()
        .unwrap()
        .parse(r#"{% show name %} {% show number %}"#)
        .unwrap();

    let globals = liquid::object!({"name": "Sancho Panza", "number": 42});

    let output = template.render(&globals).unwrap();
    assert_eq!(output, "Sancho Panza 42");
}
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::model::Scalar;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::ValueView;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct ShowTag;

impl TagReflection for ShowTag {
    fn tag(&self) -> &'static str {
        "show"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for ShowTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        let field = arguments
            .expect_next("Identifier expected.")?
            .expect_identifier()
            .into_result()?
            .to_string();

        arguments.expect_nothing()?;

        Ok(Box::new(Show { field }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct Show {
    field: String,
}

impl Renderable for Show {
    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
        let field = self.field.clone();
        let value = match runtime.get(&[Scalar::new(field)]) {
            Ok(value) => value,
            Err(_) => {
                return Err(liquid_core::error::Error::with_msg(format!(
                    "No value called '{}' was passed to the render function.",
                    self.field
                )))
            }
        };

        write!(writer, "{}", value.to_kstr()).replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;
    use liquid_core::Value;

    fn options() -> Language {
        let mut options = Language::default();
        options.tags.register("show".to_string(), ShowTag.into());
        options
    }

    #[test]
    fn one() {
        let options = options();
        let template = parser::parse(r#"{% show name %}"#, &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();
        runtime.set_global("name".into(), Value::scalar("John Snow"));

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, r#"John Snow"#);
    }

    #[test]
    fn three() {
        let options = options();
        let template = parser::parse(
            r#"{% show name %} - {% show color %} - {% show answer %}"#,
            &options,
        )
        .map(runtime::Template::new)
        .unwrap();

        let runtime = RuntimeBuilder::new().build();
        runtime.set_global("name".into(), Value::scalar("John Snow"));
        runtime.set_global("color".into(), Value::scalar("blue"));
        runtime.set_global("answer".into(), Value::scalar(42));

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, "John Snow - blue - 42");
    }
}
}

Liquid create tag that uses array of values passed to the render function (latest, limit, tag)

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
serde = { version = "1.0.215", features = ["derive"] }
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::model::Scalar;
use liquid_core::parser::TryMatchToken;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::ValueView;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};
use serde::Serialize;

#[derive(Copy, Clone, Debug, Default)]
pub struct LatestTag;

impl TagReflection for LatestTag {
    fn tag(&self) -> &'static str {
        "latest"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for LatestTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        arguments
            .expect_next("limit expected")?
            .expect_str("limit")
            .into_result_custom_msg("limit expected.")?;

        arguments
            .expect_next("Assignment operator \"=\" expected.")?
            .expect_str("=")
            .into_result_custom_msg("Assignment operator \"=\" expected.")?;

        let token = arguments.expect_next("Identifier or value expected")?;
        let value = match token.expect_literal() {
            TryMatchToken::Matches(name) => name.to_kstr().to_string(),
            TryMatchToken::Fails(name) => return name.raise_error().into_err(),
        };
        let limit = match value.parse::<u8>() {
            Ok(value) => value,
            Err(_) => return Err(liquid_core::error::Error::with_msg("Expected number")),
        };

        let key = arguments.next();

        let tag = match key {
            Some(token) => {
                token
                    .expect_str("tag")
                    .into_result_custom_msg("expected tag")?;
                arguments
                    .expect_next("Assignment operator \"=\" expected.")?
                    .expect_str("=")
                    .into_result_custom_msg("Assignment operator \"=\" expected.")?;

                let literal = arguments.expect_next("value of tag")?.expect_literal();

                let value = match literal {
                    TryMatchToken::Matches(name) => name.to_kstr().to_string(),
                    TryMatchToken::Fails(name) => return name.raise_error().into_err(),
                };
                Some(value)
            }
            None => None,
        };

        arguments.expect_nothing()?;

        Ok(Box::new(Latest { limit, tag }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct Latest {
    limit: u8,
    tag: Option<String>,
}

impl Renderable for Latest {
    fn render_to(&self, writer: &mut dyn Write, runtime: &dyn Runtime) -> Result<()> {
        let mut count = 0;

        let selected_tag = self.tag.clone().unwrap_or_default();

        match runtime.get(&[Scalar::new("items")]) {
            // Ok(values) => values.as_array().unwrap().values().collect::<Vec<_>>(),
            Ok(values) => {
                for val in values.as_array().unwrap().values() {
                    let obj = val.as_object().unwrap();
                    let text = obj.get("text").unwrap().to_kstr().to_string();
                    let tag = obj.get("tag").unwrap().to_kstr().to_string();
                    if self.tag.is_some() && tag != selected_tag {
                        continue;
                    }

                    write!(writer, "<li>{} ({})</li>", text, tag).replace("Failed to render")?;
                    count += 1;
                    if count >= self.limit {
                        break;
                    }
                }
            }
            Err(_) => return Err(liquid_core::error::Error::with_msg("Expected number")),
        };

        //println!("{:?}", things[0]);
        //println!("text: {:?}", things[0].as_object().unwrap().get("text"));
        Ok(())
    }
}

#[derive(Debug, Serialize)]
pub struct Item<'a> {
    text: &'a str,
    id: u32,
    tag: &'a str,
}

impl<'a> Item<'a> {
    pub fn new(text: &'a str, id: u32, tag: &'a str) -> Self {
        Self { text, id, tag }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    fn get_items() -> &'static [Item<'static>] {
        let items = &[
            Item {
                text: "one",
                id: 1,
                tag: "web",
            },
            Item {
                text: "two",
                id: 2,
                tag: "programming",
            },
            Item {
                text: "three",
                id: 3,
                tag: "web",
            },
            Item {
                text: "four",
                id: 4,
                tag: "programming",
            },
            Item {
                text: "five",
                id: 5,
                tag: "web",
            },
            Item {
                text: "six",
                id: 6,
                tag: "programming",
            },
            Item {
                text: "seven",
                id: 7,
                tag: "web",
            },
            Item {
                text: "eight",
                id: 8,
                tag: "programming",
            },
            Item {
                text: "nine",
                id: 9,
                tag: "web",
            },
            Item {
                text: "ten",
                id: 10,
                tag: "web",
            },
        ];
        items
    }

    use liquid_core::object;
    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;
    use liquid_core::Value;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("latest".to_string(), LatestTag.into());
        options
    }

    #[test]
    fn latest_5() {
        let options = options();
        let template = parser::parse(r#"{% latest limit=5 %}"#, &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let objects = get_items()
            .iter()
            .map(|item| {
                let obj = object!({"text": item.text, "id": item.id, "tag": item.tag});
                Value::Object(obj)
            })
            .collect::<Vec<_>>();

        runtime.set_global("items".into(), Value::Array(objects));

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<li>one (web)</li><li>two (programming)</li><li>three (web)</li><li>four (programming)</li><li>five (web)</li>"#
        );
    }

    #[test]
    fn latest_5_web() {
        let options = options();
        let template = parser::parse(r#"{% latest limit=5   tag="web" %}"#, &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let objects = get_items()
            .iter()
            .map(|item| {
                let obj = object!({"text": item.text, "id": item.id, "tag": item.tag});
                Value::Object(obj)
            })
            .collect::<Vec<_>>();

        runtime.set_global("items".into(), Value::Array(objects));

        let output = template.render(&runtime).unwrap();
        assert_eq!(
            output,
            r#"<li>one (web)</li><li>three (web)</li><li>five (web)</li><li>seven (web)</li><li>nine (web)</li>"#
        );
    }
}
}
mod latest_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(latest_tag::LatestTag)
        .build()
        .unwrap()
        .parse(r#"{% latest limit=5 %}"#)
        .unwrap();

    let items = &[latest_tag::Item::new("one", 1, "web")];

    let globals = liquid::object!({"items": items});

    let output = template.render(&globals).unwrap();
    assert_eq!(output, r#"<li>one (web)</li>"#);
}

Liquid create include tag that overrides existing include tag

[package]
name = "parse"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26.9"
liquid-core = "0.26.9"
This is the hello file.
#![allow(unused)]
fn main() {
use std::io::Write;

use liquid_core::error::ResultLiquidReplaceExt;
use liquid_core::parser::TryMatchToken;
use liquid_core::Language;
use liquid_core::Renderable;
use liquid_core::Result;
use liquid_core::Runtime;
use liquid_core::ValueView;
use liquid_core::{ParseTag, TagReflection, TagTokenIter};

#[derive(Copy, Clone, Debug, Default)]
pub struct IncludeTag;

impl TagReflection for IncludeTag {
    fn tag(&self) -> &'static str {
        "include"
    }

    fn description(&self) -> &'static str {
        ""
    }
}

impl ParseTag for IncludeTag {
    fn parse(
        &self,
        mut arguments: TagTokenIter<'_>,
        _options: &Language,
    ) -> Result<Box<dyn Renderable>> {
        arguments
            .expect_next("\"file\" expected.")?
            .expect_str("file")
            .into_result_custom_msg("\"file\" expected.")?;

        arguments
            .expect_next("Assignment operator \"=\" expected.")?
            .expect_str("=")
            .into_result_custom_msg("Assignment operator \"=\" expected.")?;

        let token = arguments.expect_next("Identifier or value expected")?;
        let file = match token.expect_literal() {
            TryMatchToken::Matches(name) => name.to_kstr().into_string(),
            TryMatchToken::Fails(name) => return name.raise_error().into_err(),
        };

        arguments.expect_nothing()?;

        Ok(Box::new(Include { file }))
    }

    fn reflection(&self) -> &dyn TagReflection {
        self
    }
}

#[derive(Debug)]
struct Include {
    file: String,
}

impl Renderable for Include {
    fn render_to(&self, writer: &mut dyn Write, _runtime: &dyn Runtime) -> Result<()> {
        let content = std::fs::read_to_string(&self.file).replace("Failed to render")?;
        write!(writer, r#"{content}"#).replace("Failed to render")?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use liquid_core::parser;
    use liquid_core::runtime;
    use liquid_core::runtime::RuntimeBuilder;

    fn options() -> Language {
        let mut options = Language::default();
        options
            .tags
            .register("include".to_string(), IncludeTag.into());
        options
    }

    #[test]
    fn include_file() {
        let options = options();
        let template = parser::parse(r#"{% include file = "files/hello.txt" %}"#, &options)
            .map(runtime::Template::new)
            .unwrap();

        let runtime = RuntimeBuilder::new().build();

        let output = template.render(&runtime).unwrap();
        assert_eq!(output, "This is the hello file.\n");
    }
}
}
mod include_tag;

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .tag(include_tag::IncludeTag)
        .build()
        .unwrap()
        .parse(r#"{% include file="files/hello.txt" %}"#)
        .unwrap();

    let globals = liquid::object!({});

    let output = template.render(&globals).unwrap();
    assert_eq!(output, "This is the hello file.\n");
}

Liquid sort array or vector

  • sort
  • assign
[package]
name = "sorted-array"
version = "0.1.0"
edition = "2021"

[dependencies]
liquid = "0.26"
fn main() {
    let result = render("direct: {% for item in items %}{{item}} {% endfor %}");
    println!("{}", result);

    let result = render("sorted: {% assign sorted = items | sort %}{% for item in sorted %}{{item}} {% endfor %}");
    println!("{}", result);
}

fn render(tmpl: &str) -> String {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(tmpl)
        .unwrap();

    let globals = liquid::object!({
        "items": vec![2, 8, 4, 6, 3, 5, 7],
    });
    template.render(&globals).unwrap()
}

#[test]
pub fn test_reverse() {
    let result = render("direct: {% for item in items %}{{item}} {% endfor %}");
    assert_eq!(result, "direct: 2 8 4 6 3 5 7 ");

    let result = render("sorted: {% assign sorted = items | sort %}{% for item in sorted %}{{item}} {% endfor %}");
    assert_eq!(result, "sorted: 2 3 4 5 6 7 8 ");
}
direct: 2 8 4 6 3 5 7 
sorted: 2 3 4 5 6 7 8 

Liquid TODO

  • pass integer, string, bool, vector, tuple, struct
  • Control structures (if, else)
  • Loops (for)
  • Filters
  • Include template
  • Layout template (embed template)