Search code examples
javascriptarrayshandlebars.js

Retrieve all embedded Handlebar variables from a given text


I'm working with the Handlebars library and developing on a function to retrieve all embedded variables. The function works with simple, non-logic, text. However, when there is if-statement the function only returns a subset of the varibles.

import Handlebars from "handlebars";

function getHandlebarsVariables(input){
    const ast = Handlebars.parseWithoutProcessing(input);
    return ast.body
      .filter(({ type }) => type === 'MustacheStatement')
      .map((statement) => statement.params[0]?.original || statement.path?.original);
};

const text1 = '{{name}} {{age}} {{#if isAdult}} {{ Adult }} {{/if}}';

const variables = getHandlebarsVariables(text1);

console.log(variables); // currently returns ['name', 'age'], I want to return ['name', 'age', 'Adult']

What I expect the function to return: ['name', 'age', 'Adult'] Currently I get: ['name', 'age']


Solution

  • I think there is more to a compiler than you are giving it credit for. Your filter and map logic would be sufficient if the parsed Handlebars template were a simple, flat Array; but such a Syntax Tree is not that - it has many levels which must be parsed using recursion.

    I am offering the code below, not as a fully-functional, shippable program, but just as an example of how you could recursively traverse the Syntax Tree. This example works for the text1 input in your post, but I would not count on it to work for all Handlebars templates.

    import Handlebars from "handlebars";
    
    function getVariablesFromStatementsRecursive(statements) {
      return statements.reduce((acc, statement) => {
        const { type } = statement;
    
        if ("BlockStatement" === type) {
          const { inverse, program } = statement;
    
          if (program?.body) {
            acc = acc.concat(getVariablesFromStatementsRecursive(program.body));
          }
    
          if (inverse?.body) {
            acc = acc.concat(getVariablesFromStatementsRecursive(inverse.body));
          }
        } else if ("MustacheStatement" === type) {
          const { path } = statement;
    
          if (path?.original) {
            acc.push(path.original);
          }
        }
    
        return acc;
      }, []);
    }
    
    function getHandlebarsVariables(input) {
      const ast = Handlebars.parseWithoutProcessing(input);
    
      return getVariablesFromStatementsRecursive(ast.body);
    }
    
    const text1 = "{{name}} {{age}} {{#if isAdult}} {{ Adult }} {{/if}}";
    
    const variables = getHandlebarsVariables(text1);
    
    console.log(variables); // [ 'name', 'age', 'Adult' ]