<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Spike's Blog]]></title><description><![CDATA[My passion is building ethical software that makes the world a better place. I support open source and believe privacy is important in the digital age.]]></description><link>https://blog.spike.codes</link><generator>RSS for Node</generator><lastBuildDate>Thu, 08 Oct 2026 09:44:43 GMT</lastBuildDate><atom:link href="https://blog.spike.codes/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a simple template engine in <100 lines of Rust code]]></title><description><![CDATA[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 ...]]></description><link>https://blog.spike.codes/build-a-template-engine</link><guid isPermaLink="true">https://blog.spike.codes/build-a-template-engine</guid><category><![CDATA[Rust]]></category><category><![CDATA[HTML5]]></category><category><![CDATA[template]]></category><category><![CDATA[Regex]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Spike]]></dc:creator><pubDate>Fri, 17 Jun 2022 18:40:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1655490648648/Iiww93D_G.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>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.</p>
<p>A common example is sending the titles of posts from your back-end to be listed in a <code>&lt;ul&gt;</code> on your static HTML5 blog page, each as their own <code>&lt;li&gt;</code>.</p>
<p>This tutorial is aimed at beginner Rustaceans but you should be familiar with programming and working on the terminal.</p>
<h1 id="heading-why-rust">Why Rust?</h1>
<p><a target="_blank" href="http://rust-lang.org/">Rust</a> is the modern, memory-safe programming language (with <a target="_blank" href="https://www.arewewebyet.org/">a fantastic web ecosystem</a>!) that I'll be using in this tutorial. Packages in Rust are called "crates" and are typically published to <a target="_blank" href="https://crates.io">crates.io</a>.</p>
<p>If you'd like to learn more about the <a target="_blank" href="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">most loved language on StackOverflow</a>, I can't recommend the <a target="_blank" href="https://doc.rust-lang.org/book/ch00-00-introduction.html">Rust Book</a> and <a target="_blank" href="https://doc.rust-lang.org/rust-by-example">Rust by Example</a> enough!</p>
<p>In line with the notoriously-idiomatic Rust community, the file structure for this example crate will be:</p>
<pre><code>template<span class="hljs-operator">-</span>engine<span class="hljs-operator">/</span>
├─ dist<span class="hljs-operator">/</span>
│  ├─ index.html
├─ src<span class="hljs-operator">/</span>
│  ├─ lib.rs
├─ Cargo.toml
</code></pre><blockquote>
<p>🦀 The primary code file for Rust libraries is <code>src/lib.rs</code>; for binaries, <code>src/main.rs</code></p>
</blockquote>
<p><code>src/lib.rs</code> will house the crate's code, while the dependencies and other metadata will be kept neatly tucked away in <code>Cargo.toml</code>.</p>
<h1 id="heading-quest-ce-que-jinja">Qu'est-ce que Jinja ?</h1>
<p>Jinja is a web template engine for Python that uses a simple set of rules for <a target="_blank" href="https://documentation.bloomreach.com/engagement/docs/jinja-syntax">its syntax</a>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Delimiter:</strong></td><td><strong>It is used for:</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>{{..}}</code><br /> Example: <code>{{ customer }}</code>.</td><td>Print the value of the variable between the braces.</td></tr>
<tr>
<td><code>{%..%}</code><br /> Example: <code>{% set x = 2 %}</code>.</td><td>Statements that do not have an output.</td></tr>
<tr>
<td><code>{#..#}</code><br /> Example: <code>{# Blog: #}</code>.</td><td>Comments to explain the code.</td></tr>
</tbody>
</table>
</div><p>This will provide us with a simple, standardized set of methods to implement in our toy template engine.</p>
<h1 id="heading-the-plan">The Plan</h1>
<p>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 <em>slightly</em> better names for your variables—it's useful to brainstorm as much as possible before you begin scripting.</p>
<h2 id="heading-features">Features</h2>
<p>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:</p>
<ol>
<li>Printing variables:<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>Hello, my name is {{ name }}!<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
</li>
<li>If statements (only supports boolean expressions currently):<pre><code class="lang-html">{% if allowed %}
 <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to the internet!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
{% else %}
 <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No trespassing.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
{% endif %}
</code></pre>
</li>
<li>Repeat statements:<pre><code class="lang-html">{% repeat 3 times %}
<span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>{{ date }}<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
{% endrepeat %}
</code></pre>
</li>
<li>Comments:<pre><code class="lang-html">{# List of blog posts #}
</code></pre>
</li>
</ol>
<h2 id="heading-process">Process</h2>
<p>The <code>render()</code> function will be defined as:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">render</span></span>(<span class="hljs-keyword">mut</span> template: <span class="hljs-built_in">String</span>, <span class="hljs-keyword">mut</span> data: HashMap&lt;&amp;<span class="hljs-built_in">str</span>, Data&gt;) -&gt; <span class="hljs-built_in">String</span> {
</code></pre>
<p>and will take a <code>template</code> as an input such as:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>Hello {{ name }}!<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<p>along with a <a target="_blank" href="https://en.wikipedia.org/wiki/Hash_table">HashMap</a> (key-value list) of data:</p>
<pre><code class="lang-rs">HashMap::from([
    (<span class="hljs-string">"name"</span>, Data::Text(<span class="hljs-string">"world"</span>.to_string())),
])
</code></pre>
<p>and output an HTML5-compatible String with the correctly replaced data:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>Hello world!<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<h1 id="heading-lets-begin">Let's begin!</h1>
<h2 id="heading-setup">Setup</h2>
<p>Use Rust's package manager, <code>cargo</code>, to initialize the crate's (<strong>lib</strong>rary) directory structure:</p>
<pre><code class="lang-bash">cargo init template-engine --lib
</code></pre>
<p>This will create a new folder, <code>template-engine/</code>, with the file structure listed above (minus <code>templates/</code> which we'll add later).</p>
<p>Start by filling up <code>Cargo.toml</code> with the metadata of the crate and its dependencies:</p>
<pre><code class="lang-toml"><span class="hljs-section">[package]</span>
<span class="hljs-attr">name</span> = <span class="hljs-string">"template-engine"</span>
<span class="hljs-attr">version</span> = <span class="hljs-string">"0.1.0"</span>
<span class="hljs-attr">edition</span> = <span class="hljs-string">"2021"</span>

<span class="hljs-section">[dependencies]</span>
<span class="hljs-attr">regex</span> = <span class="hljs-string">"1"</span>
</code></pre>
<p>We'll be using good ol' <a target="_blank" href="https://en.wikipedia.org/wiki/Regular_expression">RegEx</a> to parse the delimiters.</p>
<h2 id="heading-imports">Imports</h2>
<p>Rendering will require both importing the <a target="_blank" href="https://github.com/rust-lang/regex"><code>regex</code></a> crate as well as loading the standard library's HashMap implementation. Additionally, the <code>fmt</code> module of the standard library should be loaded to support converting <code>Data</code> elements to strings.</p>
<pre><code class="lang-rs"><span class="hljs-keyword">use</span> regex::{Regex, Captures};
<span class="hljs-keyword">use</span> std::{fmt, collections::HashMap};
</code></pre>
<h2 id="heading-data">Data</h2>
<p>You may have noticed in the <a class="post-section-overview" href="#heading-process">Process</a> section that the 'value' type of the <code>HashMap&lt;&amp;str, Data&gt;</code> is a custom type called <code>Data</code>. These wildcard types can be defined in Rust with the <a target="_blank" href="https://doc.rust-lang.org/rust-by-example/custom_types/enum.html"><code>enum</code></a> keyword and allow the creation of a type that may be one of a few different variants.</p>
<p>The <code>Data</code> 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).</p>
<pre><code class="lang-rs"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Data</span></span> {
    Number(<span class="hljs-built_in">i32</span>),
    Boolean(<span class="hljs-built_in">bool</span>),
    Text(<span class="hljs-built_in">String</span>)
}
</code></pre>
<p>Rust is strictly-typed and therefore, conversions must be defined for your custom type so that the template engine can render the inputted <code>Data</code> types as strings.</p>
<p>This can be done with the following code which unwraps the inner type (either <code>i32</code>, <code>bool</code>, or <code>String</code>) of each <code>Data</code> type:</p>
<pre><code class="lang-rs"><span class="hljs-keyword">impl</span> fmt::Display <span class="hljs-keyword">for</span> Data {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">fmt</span></span>(&amp;<span class="hljs-keyword">self</span>, f: &amp;<span class="hljs-keyword">mut</span> fmt::Formatter) -&gt; fmt::<span class="hljs-built_in">Result</span> {
        <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span> {
            Self::Text(x) =&gt; <span class="hljs-built_in">write!</span>(f, <span class="hljs-string">"{}"</span>, x),
            Self::Number(x) =&gt; <span class="hljs-built_in">write!</span>(f, <span class="hljs-string">"{}"</span>, x),
            Self::Boolean(x) =&gt; <span class="hljs-built_in">write!</span>(f, <span class="hljs-string">"{}"</span>, x)
        }
    }
}
</code></pre>
<h1 id="heading-rendering">Rendering</h1>
<h2 id="heading-print-delimiters">Print delimiters</h2>
<p>The core feature of a template engine is printing the value of the inputted data based on what key is typed between <code>{{</code> and <code>}}</code> in the HTML file.</p>
<p>To match the instances where text is surrounded by double braces, we'll be using a Regular Expression — the industry standardized search pattern language.</p>
<blockquote>
<p>💡 I recommend <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet">MDN's cheat sheet</a> and <a target="_blank" href="https://regexr.com/">RegExr</a> for testing your regular expressions</p>
</blockquote>
<p>For the braces to be properly read by Rust's <code>regex</code> crate, you need to escape the characters by placing backslashes before them.</p>
<pre><code class="lang-rs"><span class="hljs-keyword">let</span> print_regex = Regex::new(<span class="hljs-string">r"\{\{(.*?)\}\}"</span>).unwrap();
</code></pre>
<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>


<p>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 <a class="post-section-overview" href="#heading-process">above example</a>).</p>
<pre><code class="lang-rust">template = print_regex.replace_all(&amp;template, |caps: &amp;Captures| {
    ...
    <span class="hljs-comment">// Find the corresponding key in the Data and return the value</span>
    data[key].to_string()
}).to_string();
</code></pre>
<blockquote>
<p>What should replace the <code>...</code> to extract the key?</p>
</blockquote>
<p>To extract the key, you must traverse through the capture groups and identify only the key.</p>
<p>The <code>regex</code> crate for Rust always inserts the entire match as the first element of captures. Thus, the indices and elements of <code>caps</code> are as follows:</p>
<pre><code><span class="hljs-number">0</span><span class="hljs-string">.</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ hello }}</span>"</span>
<span class="hljs-number">1</span><span class="hljs-string">.</span> <span class="hljs-string">" hello "</span>
</code></pre><p>To ensure the <code>Data</code> HashMap can locate the same key as specified in the template, use Rust's <a target="_blank" href="https://doc.rust-lang.org/stable/std/primitive.str.html#method.trim"><code>.trim()</code></a> method to remove the whitespace at the start and end of capture group 1.</p>
<pre><code class="lang-rs"><span class="hljs-comment">// Extract the text in between the {{ and the }} in the template</span>
<span class="hljs-keyword">let</span> key = caps.get(<span class="hljs-number">1</span>).unwrap().as_str().trim();
</code></pre>
<h2 id="heading-repeat-statements">Repeat statements</h2>
<p>Once again, through lots of StackOverflow reading, a new regular expression is born—this time, to match the repeat statements as defined above:</p>
<pre><code class="lang-rs"><span class="hljs-keyword">let</span> repeat_regex = Regex::new(<span class="hljs-string">r"\{% repeat (\d*?) times %\}((.|\n)*?)\{% endrepeat %\}"</span>).unwrap();
</code></pre>
<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>

<p>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).</p>
<p>From there, you can apply the same syntax as before but this time utilizing Rust's <a target="_blank" href="https://doc.rust-lang.org/stable/std/primitive.str.html#method.repeat"><code>.repeat()</code></a> method to repeat the code block a certain  number of times:</p>
<pre><code class="lang-rs">template = repeat_regex.replace_all(&amp;template, |caps: &amp;Captures| {
    <span class="hljs-comment">// Extract the number of times to repeat the code</span>
    <span class="hljs-keyword">let</span> times = caps.get(<span class="hljs-number">1</span>).unwrap().as_str().trim();

    <span class="hljs-comment">// Parse the code block to be repeated</span>
    <span class="hljs-keyword">let</span> code = caps.get(<span class="hljs-number">2</span>).unwrap().as_str().trim();

    <span class="hljs-comment">// Repeat the code `times` number of times</span>
    code.repeat(times.parse::&lt;<span class="hljs-built_in">usize</span>&gt;().unwrap())
}).to_string();
</code></pre>
<blockquote>
<p>Note how after each statement, the template variable is reassigned to the rendered version.</p>
</blockquote>
<h2 id="heading-ifelse-statements">If/else statements</h2>
<p>This regular expression will match <code>{% if KEY %}...{% else %}...{% endif %}</code>:</p>
<pre><code class="lang-rs"><span class="hljs-keyword">let</span> if_else_regex = Regex::new(<span class="hljs-string">r"\{% if (.*?) %\}((.|\n)*?)(\{% else %\}((.|\n)*?)\{% endif %\}|\{% endif %\})"</span>).unwrap();
</code></pre>
<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>

<p>From there, simply use the <code>regex</code> crate's <a target="_blank" href="https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace_all"><code>.replace_all()</code></a> method to swap the capture with either the parsed <code>if</code> code block or the parsed <code>else</code> code block, depending on the value of the boolean that matches <code>key</code>.</p>
<pre><code class="lang-rs">template = if_else_regex.replace_all(&amp;template, |caps: &amp;Captures| {
    <span class="hljs-comment">// Extract the name of the bool being tested</span>
    <span class="hljs-keyword">let</span> key = caps.get(<span class="hljs-number">1</span>).unwrap().as_str().trim();
    <span class="hljs-comment">// Parse the 'if' and (optional) 'else' code blocks</span>
    <span class="hljs-keyword">let</span> if_code = caps.get(<span class="hljs-number">2</span>).unwrap().as_str().trim();
    <span class="hljs-keyword">let</span> else_code = caps.get(<span class="hljs-number">5</span>).map_or(<span class="hljs-string">""</span>, |m| m.as_str()).trim();
    <span class="hljs-comment">// Find the corresponding key in the Data and return the value</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> Data::Boolean(exp) = data[key] {
        <span class="hljs-keyword">if</span> exp { if_code.to_string() }
        <span class="hljs-keyword">else</span> { else_code.to_string() }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-string">"ERROR PARSING KEY"</span>.to_string()
    }
}).to_string();
</code></pre>
<p><a target="_blank" href="https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap"><code>.unwrap()</code></a> is one of the many methods for error handling in Rust but it will throw an error if the unwrapped <code>Option</code> is <code>None</code> (Rust equivalent to null).</p>
<p>Using <a target="_blank" href="https://doc.rust-lang.org/std/option/enum.Option.html#method.map_or"><code>.map_or()</code></a> when handling the 5th capture group (line recreated below) allows us to store an empty string as the <code>else_code</code> if there is no else code block in the template.</p>
<pre><code class="lang-rs"><span class="hljs-keyword">let</span> else_code = caps.get(<span class="hljs-number">5</span>).map_or(<span class="hljs-string">""</span>, |m| m.as_str()).trim();
</code></pre>
<h2 id="heading-comments">Comments</h2>
<p>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.</p>
<p>Here, we don't need to match the key or code block surrounded by <code>{#</code> and <code>#}</code> so a simple string <a target="_blank" href="https://doc.rust-lang.org/std/primitive.str.html#method.replace"><code>.replace()</code></a> method will suffice:</p>
<pre><code class="lang-rs">template = template.replace(<span class="hljs-string">"{#"</span>, <span class="hljs-string">"&lt;!--"</span>).replace(<span class="hljs-string">"#}"</span>, <span class="hljs-string">"--&gt;"</span>);
</code></pre>
<h2 id="heading-put-it-all-together">Put it all together!</h2>
<p>Your finished render function should look something like this:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">render</span></span>(<span class="hljs-keyword">mut</span> template: <span class="hljs-built_in">String</span>, <span class="hljs-keyword">mut</span> data: HashMap&lt;&amp;<span class="hljs-built_in">str</span>, Data&gt;) -&gt; <span class="hljs-built_in">String</span> {
    <span class="hljs-comment">// Render variable printing</span>
    <span class="hljs-keyword">let</span> print_regex = Regex::new(<span class="hljs-string">r"\{\{(.*?)\}\}"</span>).unwrap();
    template = print_regex.replace_all(&amp;template, |caps: &amp;Captures| {
        <span class="hljs-comment">// Extract the text in between the {{ and the }} in the template</span>
        <span class="hljs-keyword">let</span> key = caps.get(<span class="hljs-number">1</span>).unwrap().as_str().trim();
        <span class="hljs-comment">// Find the corresponding key in the Data and return the value</span>
        data[key].to_string()
    }).to_string();

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

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

    <span class="hljs-comment">// Process comments</span>
    template = template.replace(<span class="hljs-string">"{#"</span>, <span class="hljs-string">"&lt;!--"</span>).replace(<span class="hljs-string">"#}"</span>, <span class="hljs-string">"--&gt;"</span>);

    <span class="hljs-comment">// Return output</span>
    template
}
</code></pre>
<h1 id="heading-testing-it-out">Testing it out</h1>
<h2 id="heading-writing-a-template">Writing a template</h2>
<p>Inside <code>dist/index.html</code>, we'll design a simple template to test documentation comments, code-disabling comments, variable printing, if/else statements, and repeat statements.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    {# This is a comment! #}
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>{{ hello }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

    {% if allowed %}
        {% repeat 3 times %}
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to the {{ hello }}!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        {% endrepeat %}
    {% else %}
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No trespassing.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    {% endif %}

    {#
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hidden from the {{ hello }}...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    #}
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<h2 id="heading-create-the-test">Create the test</h2>
<p>Unlike Rust binaries, libraries in Rust have no primary function to run which is why executing <code>cargo run</code> in the root directory won't work.</p>
<p>Instead, you can play around with your library code using Rust <a target="_blank" href="https://doc.rust-lang.org/book/ch11-01-writing-tests.html">tests</a>.</p>
<p>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:</p>
<pre><code class="lang-rs"><span class="hljs-meta">#[cfg(test)]</span>
<span class="hljs-keyword">mod</span> tests {
    <span class="hljs-keyword">use</span> crate::{render, Data};
    <span class="hljs-keyword">use</span> std::collections::HashMap;

    <span class="hljs-meta">#[test]</span>
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">basic_template</span></span>() {
        ...
    }
}
</code></pre>
<p>In our <code>basic_template()</code> test, we'll use the <code>fs</code> module of the Rust standard library to read the contents of our example template and save it as a string:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">let</span> input = std::fs::read_to_string(<span class="hljs-string">"dist/index.html"</span>).expect(<span class="hljs-string">"Something went wrong reading the file"</span>);
</code></pre>
<p>Next, we'll construct a HashMap of data to pass to the <code>render()</code> function:</p>
<pre><code class="lang-rs"><span class="hljs-keyword">let</span> data = HashMap::from([
    (<span class="hljs-string">"hello"</span>, Data::Text(<span class="hljs-string">"internet"</span>.to_string())),
    (<span class="hljs-string">"allowed"</span>, Data::Boolean(<span class="hljs-literal">true</span>))
]);
</code></pre>
<p>Finally, we'll call our <code>render()</code> function and print the output:</p>
<pre><code class="lang-rs"><span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, render(input, data));
</code></pre>
<h2 id="heading-run-the-test">Run the test</h2>
<p>To run a Rust test without hiding printed text, use:</p>
<pre><code class="lang-bash">cargo <span class="hljs-built_in">test</span> -- --nocapture
</code></pre>
<p>Which should successfully run and print to the terminal:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- This is a comment! --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>internet<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to the internet!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to the internet!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to the internet!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

    <span class="hljs-comment">&lt;!--
    &lt;p&gt;Hidden from the internet...&lt;/p&gt;
    --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<p><strong>Congrats on creating your own template engine! 🥳</strong></p>
<h1 id="heading-whats-next">What's next?</h1>
<p>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.</p>
<p>If you're willing to take on the strict typing of Rust, you could expand this project by:</p>
<ul>
<li>Supporting more operators in the If/Else Statement expression syntax (<code>&lt;</code>, <code>&gt;</code>, <code>==</code>, <code>!=</code>, etc.)</li>
<li>Allowing the editing of variables in the templates and rendering the template delimiters as ordered commands rather than simultaneous replacements</li>
<li>Implementing more Jinja features such as macros, importing of other templates, correct escaping, etc. (see <a target="_blank" href="https://documentation.bloomreach.com/engagement/docs/jinja-syntax">Bloomreach's documentation on Jinja syntax</a>)</li>
</ul>
<hr />
<p>The code for this project can be found here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/spikecodes/template-engine">https://github.com/spikecodes/template-engine</a></div>
<hr />
<p>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.</p>
]]></content:encoded></item></channel></rss>