9
\$\begingroup\$

To practice with regular expressions, I wrote this basic template engine in JavaScript in addition to a simple test case to make sure it works correctly.

I am wondering if there is any improvements I can do or if I am correctly following JavaScript coding style and conventions. Thanks in advance.

In short,

  1. {{= expression}} --> evaluates expression (context is optional)
  2. {{0}} --> evaluates based on given array index
  3. {{name}} --> evaluates based on given json values

Template code (does not require any dependency):

(function() {
    this.amirTemplateEngine = function() {
        var self = this;
        var _tokensToReplace, _values, _array;

        // evaluate in-line expressions
        self.evaluateString = function(string, options) {
            _tokensToReplace = string.match(new RegExp(/({{=[^}]*}})/g)) || [];

            _tokensToReplace.map(function(token) {
                string = string.replace(token, (function() {
                    return eval(token.replace("{{=", "").replace("}}", ""));
                }).call(options && options.context ? options.context : self));
            });

            return string;
        };

        // replace given object
        self.replaceGivenObject = function(string, options) {
            _values = options && options.values ? options.values : {};
            _tokensToReplace = string.match(new RegExp(/({{[ ]*[^}.]+[ ]*}})/g)) || [];

            _tokensToReplace.map(function(token) {
                string = string.replace(token, _values[token.replace(" ", "").replace("{{", "").replace("}}", "")]);
            });

            return string;
        };

        // replace given array index
        self.replaceGivenArray = function(string, options) {
            _array = options && options.array ? options.array : {};
            _tokensToReplace = string.match(new RegExp(/({{[ ]*[0-9]+[ ]*}})/g)) || [];

            _tokensToReplace.map(function(token) {
                string = string.replace(token, _array[parseInt(token.replace(" ", "").replace("{{", "").replace("}}", ""))]);
            });

            return string;
        };

        // helper function
        self.format = function(string, options) {
            // evaluate expressions
            string = self.evaluateString(string, options);

            // if options.array is defined, then evaluate template
            string = self.replaceGivenArray(string, options);

            // if options.values is defined, then evaluate template
            string = self.replaceGivenObject(string, options);

            return string;
        };

        // for chainability
        return self;
    };
})();

Testing the code (making sure output is correct):

var template = require('./amir-template-engine');

(function() {
    var self = this;
    something = "something else!";
    var template = "\
                    <h2 class='{{= (true).toString()}}'>{{0}} {{1}} {{2}}</h2>\
                    <p>{{= 4 > 0 ? 'good' : 'bad'}}</p>\
                    <p>{{= something }}</p>\
                    <p>{{name}}</p>\
                ";

    var result = "\
                    <h2 class='true'>seyed amir hossein</h2>\
                    <p>good</p>\
                    <p>something else!</p>\
                    <p>amir</p>\
                ";

    var templateObj = new amirTemplateEngine();

    testResult = templateObj.format(template, {
        array: ["seyed", "amir", "hossein"],
        values: {
            name: "amir"
        },
        context: self
    }) == result;

    console.log("Test is" + (testResult ? "" : " NOT") + " passing!");
})();

Link to GitHub repository

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks. I was able to use this to make an even simple template engine just for what I needed. Also, helped me understand how i could get the matches from a regex and perform the replacement. \$\endgroup\$
    – Raj Rao
    Commented Oct 24, 2018 at 21:13

1 Answer 1

14
+50
\$\begingroup\$

RegEx

[ ]* vs \s*

[ ]* which is same as *(space followed by a star) matches zero or more occurrences of space. This is not flexible as it only allows users to use spaces. If user use tabs, this regex will fail. Use \s*, which matches all the space characters.

Quoting from http://www.regular-expressions.info/shorthand.html

\s stands for "whitespace character". Again, which characters this actually includes, depends on the regex flavor. In all flavors discussed in this tutorial, it includes [ \t\r\n\f]. That is: \s matches a space, a tab, a line break, or a form feed. Most flavors also include the vertical tab

RegExp Constructor vs RegEx Literal

new RegExp() is used to create a new regex. This form of regex accepts a string as the first argument and returns new regex. This is used when creating regex from a variable. Since this uses string, the backslash is used to escape following character. This will produce unexpected results when regex itself contains a backslash. Example, new RegExp('\s') will return /s/. When using a backslash in this form, they need to be escaped new RegExp('\\s'). This is not necessarily difficult but requires to take care of escaping.

Passing regex literal syntax to the constructor is same as literal syntax to create regex. So, new RegExp(/({{=[^}]*}})/g) is same as /({{=[^}]*}})/g.

Mistake

The regex for object /{{\s*([^}.]*)\s*}}/g doesn't work for all cases. [^{.]* means to match anything which is not { AND .. If the key of object contains ., this regex will fail. Check this demo. Removing . solves this problem.

Not a mistake, but a validation. Using + quantifier instead of *. * says, match zero or more of the previous pattern. For example, {{=[^}]*}} will match {{=}} which you might not want to match and process.

Better use of capturing groups

The regex /({{=[^}]*}})/g will match the string starting with {{= to the }} literal and put the complete string in first captured group. Then inside replace, {{= and }} are again replaced

token.replace("{{=", "").replace("}}", "")

This can be improved by removing those characters from capture group.

/{{=([^}]*)}}/g

This regex will match only string contained in {{= and }}. Thus, removing the need of those two replace statements. The same thing can be used for the other two regexes.

With above improvements, the regex will be as below:

  1. /{{=([^}]+)}}/g
  2. /{{\s*([^}]+)\s*}}/g
  3. /{{\s*(\d+)\s*}}/g

If the expressions do not contain spaces in between them, [^}]+ can be replaced by \S+(one or more non-space characters).

JavaScript

match => map => n * replace

In the three functions, you're first using match to get the matches then iterating over it and using replace inside it. This is not at all necessary. Simple replace is sufficient in this case.

Rewritten evaluateString

// Note: I've removed `call()` here
return string.replace(/{{=([^}]*)}}/g, function(m, $1) {
    return eval($1);
});

$1 is the actual expression inside delimiters here. Similarly, other two methods can be written.

Other small improvements:

  1. Naming: something, string.
  2. Using _ prefix when variables are not local/private to the method: _tokensToReplace, _values, _array
  3. Missing strict mode: What does “use strict” do in JavaScript, and what is the reasoning behind it?
  4. Really need those comments? Guessing a number, but comments concerning
  5. Use === to compare variables. Which equals operator (== vs ===) should be used in JavaScript comparisons?
  6. For default values, || can be used instead of ternary.
  7. The default value for _array should be [] instead of {}.

Demo:

(function() {
  this.amirTemplateEngine = function() {
    var self = this;

    self.evaluateString = function(string, options) {
      return string.replace(/{{=([^}]*)}}/g, function(m, $1) {
        return eval($1);
      });
    };

    self.replaceGivenObject = function(string, options) {
      var _values = options && options.values || {};

      return string.replace(/{{\s*([^}]*)\s*}}/g, function(m, $1) {
        return _values[$1.trim()];
      });
    };

    self.replaceGivenArray = function(string, options) {
      var _array = options && options.array || [];

      return string.replace(/{{\s*(\d+)\s*}}/g, function(m, $1) {
        return _array[$1];
      });
    };

    self.format = function(string, options) {
      string = self.evaluateString(string, options);
      string = self.replaceGivenArray(string, options);
      string = self.replaceGivenObject(string, options);

      return string;
    };

    return self;
  };
})();

(function() {
  var self = this;
  something = "something else!";
  var template = "\
                    <h2 class='{{= (true).toString()}}'>{{0}} {{1}} {{2}}</h2>\
                    <p>{{= 4 > 0 ? 'good' : 'bad'}}</p>\
                    <p>{{= something }}</p>\
                    <p>{{name}}</p>\
                ";

  var result = "\
                    <h2 class='true'>seyed amir hossein</h2>\
                    <p>good</p>\
                    <p>something else!</p>\
                    <p>amir</p>\
                ";

  var templateObj = new amirTemplateEngine();

  testResult = templateObj.format(template, {
    array: ["seyed", "amir", "hossein"],
    values: {
      name: "amir"
    },
    context: self
  }) == result;

  console.log("Test is" + (testResult ? "" : " NOT") + " passing!");
})();

My Take I'd use constructor pattern with overridden toString method and single regex for replacement.

function MyTemplate(str, config) {
  this.str = str;
  this.config = config;
}
MyTemplate.prototype.toString = function() {
  var object = this.config && this.config.values || {};
  var array = this.config && this.config.array || [];

  return this.str
    .replace(/{{(=?.*?)}}/g, function(m, $1) {
      $1 = $1.trim();

      return $1.startsWith('=') ? eval($1.substr(1)) :
        /^\d+$/.test($1) ? array[$1] : object[$1];
    });
};

(function() {
  var self = this;
  something = "something else!";
  var template = `<h2 class='{{= (true).toString()}}'>{{0}} {{1}} {{2}}</h2>
                    <p>{{= 4 > 0 ? 'good' : 'bad'}}</p>
                    <p>{{= something }}</p>
                    <p>{{name}}</p>`;

  var result = `<h2 class='true'>seyed amir hossein</h2>
                    <p>good</p>
                    <p>something else!</p>
                    <p>amir</p>`;

  var templateObj = new MyTemplate(template, {
    array: ["seyed", "amir", "hossein"],
    values: {
      name: "amir"
    },
    context: self
  });

  var testResult = templateObj.toString() === result;
  console.log("Test is" + (testResult ? "" : " NOT") + " passing!");
})();

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.