# Build a simple template engine in <100 lines of Rust code

If you've ever built a full-stack web app, you've probably run into a templating engine like the one provided by Django or Flask. These neat utility packages parse your HTML files and "fill-in-the-blanks" (so to speak) with dynamic content.

A common example is sending the titles of posts from your back-end to be listed in a `<ul>` on your static HTML5 blog page, each as their own `<li>`.

This tutorial is aimed at beginner Rustaceans but you should be familiar with programming and working on the terminal.

# Why Rust?

[Rust](http://rust-lang.org/) is the modern, memory-safe programming language (with [a fantastic web ecosystem](https://www.arewewebyet.org/)!) that I'll be using in this tutorial. Packages in Rust are called "crates" and are typically published to [crates.io](https://crates.io).

If you'd like to learn more about the [most loved language on StackOverflow](https://developers.slashdot.org/story/21/08/03/1756241/rust-is-the-most-loved-language-for-the-6th-year-in-a-row-in-stack-overflow-study), I can't recommend the [Rust Book](https://doc.rust-lang.org/book/ch00-00-introduction.html) and [Rust by Example](https://doc.rust-lang.org/rust-by-example) enough!

In line with the notoriously-idiomatic Rust community, the file structure for this example crate will be:

```
template-engine/
├─ dist/
│  ├─ index.html
├─ src/
│  ├─ lib.rs
├─ Cargo.toml
```

> 🦀 The primary code file for Rust libraries is `src/lib.rs`; for binaries, `src/main.rs`

`src/lib.rs` will house the crate's code, while the dependencies and other metadata will be kept neatly tucked away in `Cargo.toml`.

# Qu'est-ce que Jinja ?

Jinja is a web template engine for Python that uses a simple set of rules for [its syntax](https://documentation.bloomreach.com/engagement/docs/jinja-syntax):

| **Delimiter:**                                            | **It is used for:**                                                      |
|-----------------------------------------------------------|--------------------------------------------------------------------------|
| `{{..}}`<br> Example: `{{ customer }}`. |  Print the value of the variable between the braces. |
| `{%..%}`<br> Example: `{% set x = 2 %}`.             | Statements that do not have an output.             |
| `{#..#}`<br> Example: `{# Blog: #}`.    | Comments to explain the code.   |

This will provide us with a simple, standardized set of methods to implement in our toy template engine.

# The Plan

The only thing programmers hate more than working with legacy software is having to do a boring, repetitive process. To avoid having to rewrite your code as much—while you come up with *slightly* better names for your variables—it's useful to brainstorm as much as possible before you begin scripting.

## Features

By the end of this tutorial, you should have a basic, safe (no Rust code is ever evaluated), and minimal template engine. It will support the following features:

1. Printing variables:
```html
<body>Hello, my name is {{ name }}!</body>
```
2. If statements (only supports boolean expressions currently):
```html
{% if allowed %}
    <p>Welcome to the internet!</p>
{% else %}
    <p>No trespassing.</p>
{% endif %}
```
3. Repeat statements:
```html
{% repeat 3 times %}
<li>{{ date }}</li>
{% endrepeat %}
```
4. Comments:
```html
{# List of blog posts #}
```

## Process

The `render()` function will be defined as:

```rust
fn render(mut template: String, mut data: HashMap<&str, Data>) -> String {
```

and will take a `template` as an input such as:

```html
<body>Hello {{ name }}!</body>
```

along with a [HashMap](https://en.wikipedia.org/wiki/Hash_table) (key-value list) of data:

```rs
HashMap::from([
    ("name", Data::Text("world".to_string())),
])
```

and output an HTML5-compatible String with the correctly replaced data:

```html
<body>Hello world!</body>
```

# Let's begin!

## Setup

Use Rust's package manager, `cargo`, to initialize the crate's (**lib**rary) directory structure:

```bash
cargo init template-engine --lib
```

This will create a new folder, `template-engine/`, with the file structure listed above (minus `templates/` which we'll add later).

Start by filling up `Cargo.toml` with the metadata of the crate and its dependencies:

```toml
[package]
name = "template-engine"
version = "0.1.0"
edition = "2021"

[dependencies]
regex = "1"
```

We'll be using good ol' [RegEx](https://en.wikipedia.org/wiki/Regular_expression) to parse the delimiters.

## Imports

Rendering will require both importing the [`regex`](https://github.com/rust-lang/regex) crate as well as loading the standard library's HashMap implementation. Additionally, the `fmt` module of the standard library should be loaded to support converting `Data` elements to strings.

```rs
use regex::{Regex, Captures};
use std::{fmt, collections::HashMap};
```

## Data

You may have noticed in the [Process](#heading-process) section that the 'value' type of the `HashMap<&str, Data>` is a custom type called `Data`. These wildcard types can be defined in Rust with the [`enum`](https://doc.rust-lang.org/rust-by-example/custom_types/enum.html) keyword and allow the creation of a type that may be one of a few different variants.

The `Data` type is necessary for the template engine to support more than just string values and to properly convert back and forth between integers/booleans (to be processed on the Rust side) and strings (to be rendered in the HTML).

```rs
enum Data {
    Number(i32),
    Boolean(bool),
    Text(String)
}
```

Rust is strictly-typed and therefore, conversions must be defined for your custom type so that the template engine can render the inputted `Data` types as strings.

This can be done with the following code which unwraps the inner type (either `i32`, `bool`, or `String`) of each `Data` type:

```rs
impl fmt::Display for Data {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Text(x) => write!(f, "{}", x),
            Self::Number(x) => write!(f, "{}", x),
            Self::Boolean(x) => write!(f, "{}", x)
        }
    }
}
```

# Rendering

## Print delimiters

The core feature of a template engine is printing the value of the inputted data based on what key is typed between `{{` and `}}` in the HTML file.

To match the instances where text is surrounded by double braces, we'll be using a Regular Expression — the industry standardized search pattern language.

> 💡 I recommend [MDN's cheat sheet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet) and [RegExr](https://regexr.com/) for testing your regular expressions

For the braces to be properly read by Rust's `regex` crate, you need to escape the characters by placing backslashes before them.

```rs
let print_regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
```

<details>
  <summary>RegEx Breakdown</summary>
  
  <ul>
  <li><code>{{</code> — Match opening double braces</li>
  <li><code>(.\*?)</code> — Match any text <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a></li>
  <li><code>{{</code> — Match closing double braces</li>
  </ul>
</details>


Next, iterate through each instance and replace it with the value that corresponds to the entered key (e.g. "world" corresponds to "hello" in the [above example](#heading-process)).

```rust
template = print_regex.replace_all(&template, |caps: &Captures| {
    ...
    // Find the corresponding key in the Data and return the value
    data[key].to_string()
}).to_string();
```

> What should replace the `...` to extract the key?

To extract the key, you must traverse through the capture groups and identify only the key.

The `regex` crate for Rust always inserts the entire match as the first element of captures. Thus, the indices and elements of `caps` are as follows:

```
0. "{{ hello }}"
1. " hello "
```

To ensure the `Data` HashMap can locate the same key as specified in the template, use Rust's [`.trim()`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.trim) method to remove the whitespace at the start and end of capture group 1.

```rs
// Extract the text in between the {{ and the }} in the template
let key = caps.get(1).unwrap().as_str().trim();
```

## Repeat statements

Once again, through lots of StackOverflow reading, a new regular expression is born—this time, to match the repeat statements as defined above:

```rs
let repeat_regex = Regex::new(r"\{% repeat (\d*?) times %\}((.|\n)*?)\{% endrepeat %\}").unwrap();
```

<details>
  <summary>RegEx Breakdown</summary>
  
  <ul>
  <li><code>{% repeat </code> — Match first part of opening tag</li>
  <li><code>(\\d\*?)</code> — Match digits(s) <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a> as capture group</li>
  <li><code> times %}</code> — Match end part of opening tag</li>
  <li><code>((.|\\n)\*?)</code> — Match multi-line string of text <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a></li>
  <li><code>{% endrepeat %}</code> — Match closing tag</li>
  </ul>
</details>

This beaut will capture both the number of times to repeat the code and the code block itself (capture group indexes #3 and #6 respectively).

From there, you can apply the same syntax as before but this time utilizing Rust's [`.repeat()`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.repeat) method to repeat the code block a certain  number of times:

```rs
template = repeat_regex.replace_all(&template, |caps: &Captures| {
	// Extract the number of times to repeat the code
	let times = caps.get(1).unwrap().as_str().trim();

	// Parse the code block to be repeated
	let code = caps.get(2).unwrap().as_str().trim();

	// Repeat the code `times` number of times
	code.repeat(times.parse::<usize>().unwrap())
}).to_string();
```

> Note how after each statement, the template variable is reassigned to the rendered version.

## If/else statements

This regular expression will match `{% if KEY %}...{% else %}...{% endif %}`:

```rs
let if_else_regex = Regex::new(r"\{% if (.*?) %\}((.|\n)*?)(\{% else %\}((.|\n)*?)\{% endif %\}|\{% endif %\})").unwrap();
```

<details>
  <summary>RegEx Breakdown</summary>
  
  <ul>
  <li><code>{% if </code> — Match first part of opening tag</li>
  <li><code>(.\*?)</code> — Match any text <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a></li>
  <li><code> %}</code> — Match end part of opening tag</li>
  <li><code>((.|\\n)\*?)</code> — Match multi-line string of text <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a></li>
  <li><code>{% else %}</code> — Match else tag</li>
  <li><code>((.|\\n)\*?)</code> — Match multi-line string of text <a target="_blank" href="https://javascript.info/regexp-greedy-and-lazy#lazy-mode">lazily</a></li>
  <li><code>{% endif %}</code> — Match closing tag</li>
  </ul>
</details>

From there, simply use the `regex` crate's [`.replace_all()`](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace_all) method to swap the capture with either the parsed `if` code block or the parsed `else` code block, depending on the value of the boolean that matches `key`.

```rs
template = if_else_regex.replace_all(&template, |caps: &Captures| {
	// Extract the name of the bool being tested
	let key = caps.get(1).unwrap().as_str().trim();
	// Parse the 'if' and (optional) 'else' code blocks
	let if_code = caps.get(2).unwrap().as_str().trim();
	let else_code = caps.get(5).map_or("", |m| m.as_str()).trim();
	// Find the corresponding key in the Data and return the value
	if let Data::Boolean(exp) = data[key] {
		if exp { if_code.to_string() }
		else { else_code.to_string() }
	} else {
		"ERROR PARSING KEY".to_string()
	}
}).to_string();
```

[`.unwrap()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap) is one of the many methods for error handling in Rust but it will throw an error if the unwrapped `Option` is `None` (Rust equivalent to null).

Using [`.map_or()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or) when handling the 5th capture group (line recreated below) allows us to store an empty string as the `else_code` if there is no else code block in the template.

```rs
let else_code = caps.get(5).map_or("", |m| m.as_str()).trim();
```

## Comments

Lastly, Jinja-style comments should be converted to HTML comments so that they can still be read in the rendered code. Comments are useful to disable parts of the template for debugging or provide documentation to your template code.

Here, we don't need to match the key or code block surrounded by `{#` and `#}` so a simple string [`.replace()`](https://doc.rust-lang.org/std/primitive.str.html#method.replace) method will suffice:

```rs
template = template.replace("{#", "<!--").replace("#}", "-->");
```

## Put it all together!

Your finished render function should look something like this:

```rust
fn render(mut template: String, mut data: HashMap<&str, Data>) -> String {
	// Render variable printing
	let print_regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
	template = print_regex.replace_all(&template, |caps: &Captures| {
		// Extract the text in between the {{ and the }} in the template
		let key = caps.get(1).unwrap().as_str().trim();
		// Find the corresponding key in the Data and return the value
		data[key].to_string()
	}).to_string();

	// Render repeat statements
	let repeat_regex = Regex::new(r"\{% repeat (\d*?) times %\}((.|\n)*?)\{% endrepeat %\}").unwrap();
	template = repeat_regex.replace_all(&template, |caps: &Captures| {
		// Extract the number of times to repeat the code
		let times = caps.get(1).unwrap().as_str().trim();
		// Parse the code block to be repeated
		let code = caps.get(2).unwrap().as_str().trim();
		// Repeat the code `times` number of times
		code.repeat(times.parse::<usize>().unwrap())
	}).to_string();

	// Render for statements
	let if_else_regex = Regex::new(r"\{% if (.*?) %\}((.|\n)*?)(\{% else %\}((.|\n)*?)\{% endif %\}|\{% endif %\})").unwrap();
	template = if_else_regex.replace_all(&template, |caps: &Captures| {
		// Extract the name of the bool being tested
		let key = caps.get(1).unwrap().as_str().trim();
		// Parse the 'if' and (optional) 'else' code blocks
		let if_code = caps.get(2).unwrap().as_str().trim();
		let else_code = caps.get(5).map_or("", |m| m.as_str()).trim();
		// Find the corresponding key in the Data and return the value
		if let Data::Boolean(exp) = data[key] {
			if exp { if_code.to_string() }
			else { else_code.to_string() }
		} else {
			"ERROR PARSING KEY".to_string()
		}
	}).to_string();

	// Process comments
	template = template.replace("{#", "<!--").replace("#}", "-->");

	// Return output
	template
}
```

# Testing it out

## Writing a template

Inside `dist/index.html`, we'll design a simple template to test documentation comments, code-disabling comments, variable printing, if/else statements, and repeat statements.

```html
<body>
	{# This is a comment! #}
	<h1>{{ hello }}</h1>

	{% if allowed %}
		{% repeat 3 times %}
			<p>Welcome to the {{ hello }}!</p>
		{% endrepeat %}
	{% else %}
		<p>No trespassing.</p>
	{% endif %}

	{#
	<p>Hidden from the {{ hello }}...</p>
	#}
</body>
```

## Create the test

Unlike Rust binaries, libraries in Rust have no primary function to run which is why executing `cargo run` in the root directory won't work.

Instead, you can play around with your library code using Rust [tests](https://doc.rust-lang.org/book/ch11-01-writing-tests.html).

These tests are run independently from your library code meaning you'll need to import the elements of your crate to be used in your test:

```rs
#[cfg(test)]
mod tests {
	use crate::{render, Data};
	use std::collections::HashMap;

	#[test]
	fn basic_template() {
		...
	}
}
```

In our `basic_template()` test, we'll use the `fs` module of the Rust standard library to read the contents of our example template and save it as a string:

```rust
let input = std::fs::read_to_string("dist/index.html").expect("Something went wrong reading the file");
```

Next, we'll construct a HashMap of data to pass to the `render()` function:

```rs
let data = HashMap::from([
	("hello", Data::Text("internet".to_string())),
	("allowed", Data::Boolean(true))
]);
```

Finally, we'll call our `render()` function and print the output:

```rs
println!("{}", render(input, data));
```

## Run the test

To run a Rust test without hiding printed text, use:

```bash
cargo test -- --nocapture
```

Which should successfully run and print to the terminal:

```html
<body>
	<!-- This is a comment! -->
	<h1>internet</h1>

	<p>Welcome to the internet!</p><p>Welcome to the internet!</p><p>Welcome to the internet!</p>

	<!--
	<p>Hidden from the internet...</p>
	-->
</body>
```

**Congrats on creating your own template engine! 🥳**

# What's next?

In comparison to most template engines, the toy we built today is pretty bare-bones. Currently, no variables can be set in the template code, nesting only partially(?) works, and few data types are supported.

If you're willing to take on the strict typing of Rust, you could expand this project by:

 - Supporting more operators in the If/Else Statement expression syntax (`<`, `>`, `==`, `!=`, etc.)
 - Allowing the editing of variables in the templates and rendering the template delimiters as ordered commands rather than simultaneous replacements
 - Implementing more Jinja features such as macros, importing of other templates, correct escaping, etc. (see [Bloomreach's documentation on Jinja syntax](https://documentation.bloomreach.com/engagement/docs/jinja-syntax))

------

The code for this project can be found here:

%[https://github.com/spikecodes/template-engine]

------

Please leave a like if you enjoyed this post! Let me know in the comments if you have any suggestions or clarifications for making this article accurate, easy to understand, and up-to-date.
