Slice of Dev Logo

Template Engine in JavaScript

Cover Image for Template Engine in JavaScript
Author's Profile Pic
Rajkumar Gaur

Template engines allow the creation of dynamic output by combining data with pre-written templates. These engines use placeholders, or variables, in the templates that are filled in with data at runtime.

For example, suppose you have the following HTML template and the parameters object, then the output would be calculated using the parameters object.

Input Template:
<div>
  {{ for products }}
  <div>
    <h3>{ product.title }</h3>
    <p>{ product.price }</p>
    <p>{ product.rating }</p>
  </div>
  {{ end for }}
</div>

Input parameters:
{
  products: [
    { title: "Bread", price: "$5", rating: 5 },
    { title: "Butter", price: "$3", rating: 4.5}
  ]
}

Output by combining the template and params
<div>
  <div>
    <h3>Bread</h3>
    <p>$5</p>
    <p>5</p>
  </div>
  <div>
    <h3>Butter</h3>
    <p>$3</p>
    <p>4.5</p>
  </div>
</div>

As you can see above, template engines help to reuse the same code or string for different use cases by providing the parameters.

There are tons of good template engines already available in the JavaScript ecosystem like Moustache, Handlebars, Pug, etc. And there is honestly no reason to create your own and reinvent the wheel.

But, why not! Just for fun, we will be creating our own template system inspired by the template system used in the Solidity compiler called Whiskers.

Whiskers is implemented in C++, but let’s create our own JavaScript implementation for it in less than 100 lines of code.

Template Structure

Whiskers will support three types of parameters, plain variables, lists/arrays, and conditionals.

Plain parameters

These would be plain parameters that can be substituted from the input parameters.

Whiskers(`
<head>
  <<title>>
</head>`, {
  title: "WhiskersJS"
})

// output
<head>
  WhiskersJS
</head>

Lists

Lists should be enclosed between <<#param>>…<</param>> tags. Nested lists are also supported!

Whiskers(`
<ul>
  <<#food>>
    <li><<name>></li>
  <</food>>
</ul>`, {
  food: [
    { name: "Bread" },
    { name: "Butter" },
  ]
})

// output
<ul>
    <li>Bread</li>
    <li>Butter</li>
</ul>

Conditionals

Conditionals can be used like normal if-else statements. The conditional statements should be enclosed between <<?param>>param is truthy...<<!param>>param is falsy...<</param>> .

The else part can be omitted, so we can have just an if statement, <<?param>>Only execute if truthy<</param>>

Whiskers(`
<<?isKid>>You are a kid<<!isKid>>You are an adult<</isKid>>`, {
  isKid: true
})

// output
You are a kid

You can play around with it in the following CodePen

CodePen for WhiskersJS

You can check out the full code on GitHub if you want to jump to the code directly.

Implementing Plain Parameters

We will be relying on regular expressions to help us find the parameters in the template.

So, let’s look at the regular expression for detecting the plain parameters <<param>> .

// valid characters for parameter names
// + at the end for matching one or more characters
const paramChars = "[a-zA-Z0-9_-]+";

// regular expression object
const regex = new RegExp(
  `<<(?<param>${paramChars})>>`
);

The above regex will do the following

  • Match placeholders in the input string enclosed in << and >>.
  • The param group is created using the (?<param> syntax, which defines a named capturing group that captures the parameter name.

Next is using this regex to find the parameters and substitute them with the input object.

While we are at it, first let’s create the main Whiskers function that will be called for templating.

// takes the input template and params object as arguments
function Whiskers(inputStr = "", params = {}) {
  // finding the first occurrence of a <<param>>
  const match = inputStr.match(regex);

  // if there are no params in the input template, return the template as is
  if (!match) return inputStr;

  // will be implemented later
  // call the `handleVariable` function to substitute <<param>> with actual value
  return handleVariable(inputStr, params, match);
}

Let’s understand the return value of the .match method as we will heavily rely on it.

.match returns an array with some metadata properties, see the following.

[
  '<<title>>', // The entire match
  'title',     // The matched word enclosed in the first ()
  index: 23,   // The starting index of the match in the input string
  input: '<html>\n<head>\n  <title><<title>></title>\n</head>\n</html>', // The input string being searched
  groups: { // the object contains one named capturing group param with a value of 'title'
    param: 'title'
  }
]

Using this, let’s substitute the parameters with the actual value.

// to be called from the Whiskers function
function handleVariable(inputStr, params, match) {
  // Extract the part of the input string before the matched variable
  const beforeMatch = inputStr.substring(0, match.index);

  // Extract the name of the variable from the named capturing group
  const variableName = match.groups.param;

  // Look up the value of the variable in the params object
  const variableValue = params[variableName];

  // Extract the part of the input string after the matched variable
  const afterMatch = inputStr.substring(match.index + match[0].length);

  // Recursively call the Whiskers function on the remaining input string
  const remainingString = Whiskers(afterMatch, params);

  // Concatenate the parts of the input string with the variable value
  const outputStr = beforeMatch + variableValue + remainingString;

  // Return the updated input string
  return outputStr;
}

Note that we call the Whiskers function recursively to keep matching the next param and then add the result to the current output.

Also, the above code can be made less verbose.

function handleVariable(inputStr, params, match) {
  return (
    inputStr.substring(0, match.index) +
    params[match.groups.param] +
    Whiskers(inputStr.substring(match.index + match[0].length), params)
  );
}

Implementing Lists

Lists are identified using the <<#param>><</param>> tags. Let’s add the regular expression for it to our regex variable.

const regex = new RegExp(
  `<<(?<param>${paramChars})>>|` + // add an `|` OR condition
    `<<#(?<list>${paramChars})>>(?<listBody>(?:.|\\r|\\n)*?)<</\\k<list>>>` // regex for lists
);

The above regex will do the following

  • <<#: Matches the opening tag of a list variable.
  • (?<list>${paramChars}): Captures the name of the list variable as a named group.
  • >>: Matches the closing tag of the list variable.
  • (?<listBody>(?:.|\\r|\\n)*?): Captures the body of the list variable as a named group. The (?:.|\\r|\\n) matches any character, including line breaks.
  • <</\\k<list>>>: Matches the closing tag of the list variable, which consists of the name of the list variable captured in the list named group.

Let’s add a check for lists in the Whiskers function.

function Whiskers(inputStr = "", params = {}) {
  const match = inputStr.match(regex);

  if (!match) return inputStr;

  // condition for handling lists
  if (match[0].startsWith("<<#")) {
    return handleList(inputStr, params, match);
  }

  return handleVariable(inputStr, params, match);
}

Let’s also implement the handleList function.

function handleList(inputStr, params, match) {
  // Create an empty string to hold the result
  let result = "";

  // Check if the list variable exists in the params object
  if (params[match.groups.list]) {
    // If it does, iterate over each item in the list using the map function
    result = params[match.groups.list]
      .map((item) => {
        // Merge the current item's properties into a new params object
        const newParams = { ...params, ...item };

        // Extract the current item's template from the list body
        const itemTemplate = match.groups.listBody;

        // Call Whiskers with the current item's template and the new params object
        const itemResult = Whiskers(itemTemplate, newParams);

        // Return the result of calling Whiskers for this item
        return itemResult;
      })
      .join(""); // Join the resulting array of template strings into a single string
  }

  // Extract the portion of the input string that comes before the list
  const beforeList = inputStr.substring(0, match.index);

  // Extract the portion of the input string that comes after the list
  const afterList = inputStr.substring(match.index + match[0].length);

  // Call Whiskers on the portion of the input string after the list
  const afterListResult = Whiskers(afterList, params);

  // Concatenate the beforeList, the result of the list, and the afterListResult
  const finalResult = beforeList + result + afterListResult;

  // Return the final concatenated string
  return finalResult;
}

Implementing Conditionals

Conditionals are identified using <<?param>>true body<<!param>>false body<</param>> . Let’s update our regex.

const regex = new RegExp(
  `<<(?<param>${paramChars})>>|` + // add an `|` OR condition
    `<<#(?<list>${paramChars})>>(?<listBody>(?:.|\\r|\\n)*?)<</\\k<list>>>|` + // add an `|` OR condition
    `<<\\?(?<condition>${paramChars})>>(?<trueBody>(?:.|\\r|\\n)*?)(<<!\\k<condition>>>(?<falseBody>(?:.|\\r|\\n)*?))?<</\\k<condition>>>` // regex for conditonals
);

The regex does the following

  • <<\\? matches the opening tag of a conditional statement, which consists of <<?.
  • (?<condition>${paramChars}) defines a named capture group condition that matches a parameter name.
  • (?<trueBody>(?:.|\\r|\\n)*?) defines a named capture group trueBody that matches any character (including newlines) until the first occurrence of the closing tag for the conditional statement.
  • (<<!\\k<condition>>>(?<falseBody>(?:.|\\r|\\n)*?))? is an optional non-capturing group that matches the closing tag for the conditional statement followed by an opening tag for the inverse of the conditional statement, which consists of <<! and the parameter name. \k<condition> is a backreference to the named capture group condition. If this optional group is present, it defines a named capture group falseBody that matches any character (including newlines) until the closing tag for the inverse of the conditional statement is found.
  • <</\\k<condition>>> matches the closing tag for the conditional statement.

Let’s update the Whiskers function and add the handleConditional function.

function Whiskers(inputStr = "", params = {}) {
  const match = inputStr.match(regex);

  if (!match) return inputStr;

  if (match[0].startsWith("<<#")) {
    return handleList(inputStr, params, match);
  }

  // check for conditionals
  if (match[0].startsWith("<<?")) {
    return handleConditional(inputStr, params, match);
  }

  return handleVariable(inputStr, params, match);
}

function handleConditional(inputStr, params, match) {
  let result = ""; // Initialize the result string

  // Check if the conditional expression is true based on the parameter value
  if (params[match.groups.condition]) {
    // If it's true and there's a true body, evaluate the true body
    if (match.groups.trueBody) {
      result = Whiskers(
        match.groups.trueBody, // Use the true body as the input string
        params // Pass in the parameters object
      );
    }
  } else {
    // If it's false and there's a false body, evaluate the false body
    if (match.groups.falseBody) {
      result = Whiskers(
        match.groups.falseBody, // Use the false body as the input string
        params // Pass in the parameters object
      );
    }
  }

  // Return the input string with the evaluated result string
  return (
    inputStr.substring(0, match.index) + // Add the characters before the match
    result + // Add the evaluated result string
    Whiskers(inputStr.substring(match.index + match[0].length), params) // Evaluate the rest of the input string after the match recursively
  );
}

We are done! Check out the full code on GitHub.

Conclusion

It was fun building this template engine in JavaScript. Although this was a good exercise for playing with regular expressions, this should not be used in production. Thanks for reading! See you at the next one.


Cover Image for React Interview Experience

React Interview Experience

This blog is about my recent React interview experiences and some interesting questions that were asked. These questions might help you prepare for your next interview. Guess The Output | useState vs useReducer | useCallback and useMemo | Redux Vs Context API | Manage The Focus Of Elements Using React | Why is useRef used | Coding Problem.

Author's Profile Pic
Rajkumar Gaur
Cover Image for Streams in NodeJS

Streams in NodeJS

Node.js Streams are an essential feature of the platform that provide an efficient way to handle data flows. They allow for processing of large volumes of data in a memory-efficient and scalable way, and can be used for a variety of purposes such as reading from and writing to files, transforming data, and combining multiple streams into a single pipeline

Author's Profile Pic
Rajkumar Gaur