0
\$\begingroup\$

I am building a template engine for js and I need some help refactoring the code for fast and more efficient performance. If you can help modify or suggest some updates I'll be grateful.

var TEMPLATE_CONTEXT_REGISTER = new(function ContextRegister() {})();
var TEMPLATE_HELPERS_REGISTER = new(function HelpersRegister() {})();
var TEMPLATE_COMMENT_TYPE = Symbol.for("COMMENT");
var TEMPLATE_INIRIAL_TYPE = Symbol.for("INIRIAL");
var TEMPLATE_SEGMENT_TYPE = Symbol.for("SEGMENT");
var TEMPLATE_EXCLUDE_CHARS = "@";
var TEMPLATE_COMMENT_CHARS = ["(#", "#)"];
var TEMPLATE_CONTENT_CHARS = ["{{", "}}"];
var TEMPLATE_SEGMENT_CHARS = ["($", "$)"];
var TEMPLATE_REGEX_QUOTE = "(?=(?:[^'\"`]|[\"'`][^'\"`]*[\"'`])*$)";
var TEMPLATE_REGEX_START = TEMPLATE_SEGMENT_CHARS[0].replace(/(\(|\$)/g, "\\$1");
var TEMPLATE_REGEX_CLOSE = TEMPLATE_SEGMENT_CHARS[1].replace(/(\$|\))/g, "\\$1");
var TEMPLATE_REGEX_HELPER = [new RegExp("(@)" + TEMPLATE_REGEX_QUOTE, "g"), "$TEMPLATEHELPERS."];
var TEMPLATE_REGEX_COURSE = [new RegExp("(@loop)" + TEMPLATE_REGEX_QUOTE, "g"), "$TEMPLATELOOP"];
var TEMPLATE_REGEX_FACTOR = [new RegExp("(@this)" + TEMPLATE_REGEX_QUOTE, "g"), "$TEMPLATETHIS"];
var TEMPLATE_REGEX_SCRIPT = [
    new RegExp(TEMPLATE_REGEX_START + "script" + TEMPLATE_REGEX_CLOSE + "([\\s\\S]*?)" + TEMPLATE_REGEX_START + "/script" + TEMPLATE_REGEX_CLOSE, "gmi"),
    function(_, code) {
        return TEMPLATE_SEGMENT_CHARS[0] + "script " + code.replace(/\s\s+|\n/g, "") + TEMPLATE_SEGMENT_CHARS[1];
    },
];
var TEMPLATE_REGEX_ESCAPE = [
    new RegExp("s*(</?[ws=\"/.':;#-/?]+>)s*", "gi"),
    function(_, i) {
        return i.trim();
    },
];
var TEMPLATE_REGEX_OBJECT = [
    new RegExp("([.]+[\\w\\d-_]+)" + TEMPLATE_REGEX_QUOTE, "g"),
    function(obj) {
        return "['" + obj.substring(1) + "']";
    },
];

function _tpl_include_(partial, context, txtarr, jsxarr, index) {
    var template = new Template(partial, context);
    var jsx = _build_({ origins: template.origins, context: template.context, helpers: template.helpers, partial: template.partial }),
        txt = jsx.shift();
    txtarr = [].concat(txtarr, [txtarr.pop() + txt.shift()], txt);
    jsxarr = [].concat(jsxarr, jsx);
    return [txtarr, jsxarr, index + jsx.length];
}

function _tpl_length_(object) {
    return Array.isArray(object) ? object.length : typeof object === "object" ? Object.keys(object).length : null;
}

function _tpl_range_(from, to, inc, func) {
    to = inc ? (from < to ? to + 1 : to - 1) : to;
    for (var i = from; from < to ? i < to : i > to; from < to ? i++ : i--) {
        func(i, {
            index: i,
            round: i + 1,
            odd: i % 2 > 0,
            even: i % 2 === 0,
            first: i === from,
            last: i === from < to ? i - 1 : i + 1,
        });
    }
}

function _tpl_each_(obj, func) {
    if (obj === null) {
        return obj;
    }
    var index = -1;
    if (Array.isArray(obj)) {
        var length = obj.length,
            count = 1;
        while (++index < length) {
            if (
                func(index, obj[index], {
                    index: index,
                    round: count,
                    odd: index % 2 > 0,
                    even: index % 2 === 0,
                    first: index === 0,
                    last: count === length,
                }) === false
            ) {
                break;
            }
            count++;
        }
    }
    var key = Object.keys(obj),
        length = key.length,
        count = 1;
    while (++index < length) {
        if (
            func(key[index], obj[key[index]], {
                index: index,
                round: count,
                odd: index % 2 > 0,
                even: index % 2 === 0,
                first: index === 0,
                last: count === length,
            }) === false
        ) {
            break;
        }
        count++;
    }
}

function _loops_args_(args) {
    var mth = /\[(.*),(.*)\]/g.exec(args[1]),
        arr = args[0].trim(),
        val = args[1].trim(),
        key = "_";
    return [arr, mth ? mth[1].trim() : key, mth ? mth[2].trim() : val];
}

function _range_args_(args) {
    var mth = /(.*)\s*(:?to|via)\s*(.*)/g.exec(args[1]),
        arr = args[0].trim();
    return [arr, mth[1].trim(), mth[3].trim(), mth[2].trim() === "via"];
}

function _replace_(line, ...regexArray) {
    for (var i = 0, len = regexArray.length; i < len; i++) {
        var regex = regexArray[i][0],
            replace = regexArray[i][1];
        line = line.replace(regex, replace);
    }
    return line;
}

function _token_(type, value, ln, cl) {
    return {
        type,
        value: type === TEMPLATE_SEGMENT_TYPE ? _logic_(value, ln, cl) : value,
        ln,
        cl,
    };
}

function _check_(source, niddle, index) {
    return niddle.split("").every((e, i) => source[index + i] === e);
}

function _logic_(source, ln, cl) {
    var args = _replace_(source, TEMPLATE_REGEX_COURSE, TEMPLATE_REGEX_FACTOR, TEMPLATE_REGEX_HELPER, TEMPLATE_REGEX_OBJECT).split(" "),
        curs = "$TEMPLATELINNO=" + ln + ";$TEMPLATECOLNO=" + cl + ";",
        line = args.slice(1).join(" "),
        type = args[0];
    switch (type) {
        case "log":
        case "info":
        case "warn":
        case "error":
        case "debug":
            return "console." + type + "(" + line + ");" + curs;
        case "echo":
            return "$TEMPLATEJSXOF(" + line + ");" + curs;
        case "raw":
            return "$TEMPLATETXTOF(" + line + ");" + curs;
        case "set":
            return "var " + line + ";" + curs;
        case "elif":
            return "}else if(" + line + "){" + curs;
        case "else":
            return "}else{" + curs;
        case "empty":
            return "});}else{" + curs;
        case "script":
            return line + curs;
        case "include":
            var [partial, context] = line.split("with");
            return (
                "[$TEMPLATETXTAR,$TEMPLATEJSXAR,$TEMPLATEINDEX]=$TEMPLATEINCLUDE(" +
                partial +
                "," +
                context +
                ",$TEMPLATETXTAR,$TEMPLATEJSXAR,$TEMPLATEINDEX);" +
                curs
            );
        case "each":
            var [arr, key, val] = _loops_args_(line.split("as"));
            return "$TEMPLATEEACH(" + arr + ",function(" + key + "," + val + ", $TEMPLATELOOP){" + curs;
        case "range":
            var [arr, from, to, is] = _range_args_(line.split("from"));
            return "$TEMPLATERANGE(" + from + "," + to + "," + is + ",function(" + arr + ", $TEMPLATELOOP){" + curs;
        case "/each":
        case "/range":
            return "});" + curs;
        case "if":
            return "if(" + line + "){" + curs;
        case "unless":
            return "if(!(" + line + ")){" + curs;
        case "until":
            return "while(!(" + line + ")){" + curs;
        case "while":
            return "while(" + line + "){" + curs;
        case "forelse":
            var [arr, key, val] = _loops_args_(line.split("as"));
            return "if($TEMPLATELENGTH(" + arr + ")){$TEMPLATEEACH(" + arr + ",function(" + key + "," + val + ", $TEMPLATELOOP){" + curs;
        case "/if":
        case "/unless":
        case "/until":
        case "/while":
        case "/forelse":
            return "}" + curs;
        default:
            return "";
    }
}

function _parse_(source) {
    var ln = 1,
        cl = 1,
        dt = [],
        al = "",
        cu = "",
        op = false;

    for (let i = 0; i < source.length; i++) {
        cl++;
        if (source[i] === "\n")(cl = 1), ln++;
        if (_check_(source, TEMPLATE_EXCLUDE_CHARS + TEMPLATE_COMMENT_CHARS[0], i)) {
            if (cu !== "") {
                al += cu + TEMPLATE_COMMENT_CHARS[0];
                cu = "";
            }
            op = false;
            i = i + TEMPLATE_COMMENT_CHARS[0].length;
        } else if (_check_(source, TEMPLATE_EXCLUDE_CHARS + TEMPLATE_SEGMENT_CHARS[0], i)) {
            if (cu !== "") {
                al += cu + TEMPLATE_SEGMENT_CHARS[0];
                cu = "";
            }
            op = false;
            i = i + TEMPLATE_SEGMENT_CHARS[0].length;
        } else if (_check_(source, TEMPLATE_EXCLUDE_CHARS + TEMPLATE_CONTENT_CHARS[0], i)) {
            if (cu !== "") {
                al += cu + TEMPLATE_CONTENT_CHARS[0];
                cu = "";
            }
            op = false;
            i = i + TEMPLATE_CONTENT_CHARS[0].length;
        } else if (
            _check_(source, TEMPLATE_COMMENT_CHARS[0], i) ||
            _check_(source, TEMPLATE_SEGMENT_CHARS[0], i) ||
            _check_(source, TEMPLATE_CONTENT_CHARS[0], i)
        ) {
            var plus =
                (_check_(source, TEMPLATE_COMMENT_CHARS[0], i) ?
                    TEMPLATE_COMMENT_CHARS[0] :
                    _check_(source, TEMPLATE_SEGMENT_CHARS[0], i) ?
                    TEMPLATE_SEGMENT_CHARS[0] :
                    TEMPLATE_CONTENT_CHARS[0]
                ).length - 1;
            if (cu !== "") {
                if (_check_(source, TEMPLATE_COMMENT_CHARS[0], i)) al += cu;
                else {
                    dt.push(_token_(TEMPLATE_INIRIAL_TYPE, al + cu, ln, cl + plus));
                    al = "";
                }
                cu = "";
            }
            op = true;
            i = i + plus;
        } else if (_check_(source, TEMPLATE_COMMENT_CHARS[1], i)) {
            var plus = TEMPLATE_COMMENT_CHARS[1].length - 1;
            if (op) {
                dt.push(_token_(TEMPLATE_COMMENT_TYPE, cu.trim(), ln, cl + plus));
            } else {
                al += cu + TEMPLATE_COMMENT_CHARS[1];
            }
            cu = "";
            op = false;
            i = i + plus;
        } else if (_check_(source, TEMPLATE_SEGMENT_CHARS[1], i)) {
            var plus = TEMPLATE_SEGMENT_CHARS[1].length - 1;
            if (op) {
                dt.push(_token_(TEMPLATE_SEGMENT_TYPE, cu.trim(), ln, cl + plus));
            } else {
                al += cu + TEMPLATE_SEGMENT_CHARS[1];
            }
            cu = "";
            op = false;
            i = i + plus;
        } else if (_check_(source, TEMPLATE_CONTENT_CHARS[1], i)) {
            var plus = TEMPLATE_CONTENT_CHARS[1].length - 1;
            if (op) {
                dt.push(_token_(TEMPLATE_SEGMENT_TYPE, cu.trim()[0] === ">" ? "raw " + cu.trim().slice(1) : "echo " + cu.trim(), ln, cl + plus));
            } else {
                al += cu + TEMPLATE_CONTENT_CHARS[1];
            }
            cu = "";
            op = false;
            i = i + plus;
        } else {
            cu += source[i];
        }
    }
    dt.push(_token_(TEMPLATE_INIRIAL_TYPE, al + cu, ln + 1, cl + 1));
    return dt;
}

function _compile_(source) {
    var exe = _parse_(source);
    var txt = exe.filter((e) => e.type === TEMPLATE_INIRIAL_TYPE);
    var jsx = exe.filter((e) => e.type === TEMPLATE_SEGMENT_TYPE);
    return Array.from({ length: Math.max(txt.length, jsx.length) }, function(_, i) {
        return i;
    }).reduce(function(acc, idx) {
        var jx = jsx[idx] || { value: "" },
            tx = txt[idx] || { value: "", ln: -1, cl: -1 };
        return (acc += "$TEMPLATETXTOF(`" + tx.value + "`);$TEMPLATELINNO=" + tx.ln + ";$TEMPLATECOLNO=" + tx.cl + ";" + jx.value);
    }, "");
}

function _bind_(object) {
    for (var name in TEMPLATE_HELPERS_REGISTER)
        if (
            typeof TEMPLATE_HELPERS_REGISTER[name] === "function" &&
            !/^(?:class\s+|function\s+(?:_class|_default|[A-Z]))/.test(TEMPLATE_HELPERS_REGISTER[name])
        )
            TEMPLATE_HELPERS_REGISTER[name] = TEMPLATE_HELPERS_REGISTER[name].bind(object);
        else delete TEMPLATE_HELPERS_REGISTER[name];
}

function _build_({ origins, context, helpers, partial }) {
    if (!variables._VCS_[partial]) variables._VCS_[partial] = _compile_(origins);
    var exe =
        "return function($TEMPLATETHIS,$TEMPLATEHELPERS,$TEMPLATEINCLUDE,$TEMPLATELENGTH,$TEMPLATERANGE,$TEMPLATEEACH,$TEMPLATEERROR){var $TEMPLATETXTAR=[],$TEMPLATEJSXAR=[],$TEMPLATEINDEX=0,$TEMPLATELINNO=0,$TEMPLATECOLNO=0;function $TEMPLATEESCAPE($LINE){var $MAP={'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;','\\'':'&#39;'};return typeof $LINE!=='string'?$LINE:$LINE.replace(/[&<>\"']/g,function($CHAR){return $MAP[$CHAR];});}function $TEMPLATETXTOF($LINE){if(!$TEMPLATETXTAR[$TEMPLATEINDEX]){$TEMPLATETXTAR[$TEMPLATEINDEX]='';}$TEMPLATETXTAR[$TEMPLATEINDEX]+=$LINE;}function $TEMPLATEJSXOF($LINE){$TEMPLATEJSXAR[$TEMPLATEINDEX]=$TEMPLATEESCAPE($LINE);$TEMPLATEINDEX++;}with($TEMPLATETHIS||{}){try{" +
        variables._VCS_[partial] +
        "}catch(e){throw new $TEMPLATEERROR(e.message+'\\nview: " +
        partial +
        "\\nline: '+$TEMPLATELINNO+'\\npos: '+$TEMPLATECOLNO);}}return [$TEMPLATETXTAR.map(function(txt){return txt.replace(/\\n+|\\n\\s+/g, '\\n')}),...$TEMPLATEJSXAR];}";
    return new Function("", exe)()(context, helpers, _tpl_include_, _tpl_length_, _tpl_range_, _tpl_each_, CurseError);
}

function Template(partial, context) {
    if (!(partial in variables._VMS_)) throw new CurseError('view "' + partial + '" not found');
    this.origins = _replace_(variables._VMS_[partial], TEMPLATE_REGEX_SCRIPT, TEMPLATE_REGEX_ESCAPE, [/ {2,}/g, " "]);
    this.context = {...TEMPLATE_CONTEXT_REGISTER, ...(context || {}) };
    this.helpers = _bind_(this.context);
    this.partial = partial;
}

Template.prototype.exec = function() {
    var template = _build_({ origins: this.origins, context: this.context, helpers: this.helpers, partial: this.partial });
    return new Parser(...template).exec();
};

Template.prototype.string = function() {
    var template = _build_({ origins: this.origins, context: this.context, helpers: this.helpers, partial: this.partial });
    return template.shift().reduce(function(acc, part, i) {
        return acc + part + (["string", "number", "boolean"].includes(typeof template[i]) ? template[i] : "");
    }, "");
};

function TemplateRegister() {}

TemplateRegister.prototype.helper = function(name, callback) {
    if (name in TEMPLATE_HELPERS_REGISTER) throw new CurseError("Helper " + name + " already exists.");
    TEMPLATE_HELPERS_REGISTER[name] = callback;
};

TemplateRegister.prototype.context = function(name, value) {
    if (name in TEMPLATE_CONTEXT_REGISTER) throw new CurseError("Context " + name + " already exists.");
    TEMPLATE_CONTEXT_REGISTER[name] = value;
};

Template.register = new TemplateRegister();

\$\endgroup\$
1
  • 4
    \$\begingroup\$ It would benefit reviewers to have a bit more information about the code in the description. From the help center page How to ask: "You will get more insightful reviews if you not only provide your code, but also give an explanation of what it does. The more detail, the better." Obviously it is a template engine but perhaps sample usage would be helpful. \$\endgroup\$ Aug 22, 2023 at 6:11

1 Answer 1

3
\$\begingroup\$

Style

Some style points.

  • JS does not use snake_case naming, try to use the JS style camelCase naming. We only use underscore when naming constants in allcaps.

  • Why all the post and pre script underscores? eg function _token_( I can not see any need and thus its just noise.

  • Use constants for variables that do not change. Eg var TEMPLATE_COMMENT_CHARS = ["(#", "#)"]; should be const TEMPLATE_COMMENT_CHARS = ["(#", "#)"];

  • If you find that you are prefixing many variables with the same name, its a sure sign that you should encapsulate those variables in an object. Eg you have many names starting with TEMPLATE_ as in

      var TEMPLATE_CONTEXT_REGISTER = new(function ContextRegister() {})();
      var TEMPLATE_HELPERS_REGISTER = new(function HelpersRegister() {})();
      var TEMPLATE_COMMENT_TYPE = Symbol.for("COMMENT");
      var TEMPLATE_INIRIAL_TYPE = Symbol.for("INIRIAL");
      var TEMPLATE_SEGMENT_TYPE = Symbol.for("SEGMENT");
      ... // and so on
    

    Can be

    const TEMPLATE = Object.freeze({
       contextRegister: new(function ContextRegister() {})(),
       helpersRegister: new(function HelpersRegister() {})(),
       commentType: Symbol.for("COMMENT"),
       inirialType: Symbol.for("INIRIAL"),
       segmentType: Symbol.for("SEGMENT"),
       ... // and so on
    

    the Object.freeze( ensures that the objects' content can not be changed.

  • Always delimit code blocks with {} Example you have

    if (_check_(source, TEMPLATE_COMMENT_CHARS[0], i)) al += cu;
    else {
        dt.push(_token_(TEMPLATE_INIRIAL_TYPE, al + cu, ln, cl + plus));
        al = "";
    }
    

    much better as...

         if (check(source, TEMPLATE.commentChars[0], i)) { 
             al += cu;
         } else {
             dt.push(token(TEMPLATE.inirialType, al + cu, ln, cl + plus));
             al = "";
         }
    
  • Way too many short unreadable names

    Example

    var ln = 1,
        cl = 1,
        dt = [],
        al = "",
        cu = "",
        op = false;
    
  • Keep long complex clauses out of statements.

    Example you have

        if (
            func(key[index], obj[key[index]], {
                index: index,
                round: count,
                odd: index % 2 > 0,
                even: index % 2 === 0,
                first: index === 0,
                last: count === length,
            }) === false
        ) {
    

    Move the expression out of the statement and assign to a variable, test the variable. Only if the expression is more than a line.

         // I have no clue what this does, and thus the name "test"
         // Using a descriptive name would help the code a lot.
         const test = func(key[index], obj[key[index]], {
             index: index,
             round: count,
             odd: index % 2 > 0,
             even: index % 2 === 0,
             first: index === 0,
             last: count === length,
         });
         if (!test) {
    
  • Use modern syntax.

    Example...

    for (var i = 0, len = regexArray.length; i < len; i++) {
        var regex = regexArray[i][0],
            replace = regexArray[i][1];
        line = line.replace(regex, replace);
    }
    

    Can use a for of loop

     for (const regexp of regexArray) {
         const regex = regexp[0];
         const replace = regexp[1];
         line = line.replace(regex, replace);
     }
    

    Or no need for the single use variable declaration...

     for (const regexp of regexArray) {
         line = line.replace(regexp[0], regexp[1]);
     }
    

    Or use destructuring assignment to get the first two items of each sub array;;

     for (const [regex, replace] of regexArray) {
         line = line.replace(regex, replace);
     }
    

    See next section Design for some more usage of modern JS syntax.

    • BTW

      Modern JS engines optimise loops, thus capturing the length is now unneeded overhead, as in...

       for (var i = 0, len = regexArray.length; i < len; i++) {
      

      with the following running ever so slightly faster,

      for (var i = 0; i < regexArray.length; i++) {
      

Design

You have long complex switch statements.

JS does not optimise switch statements and thus the time complexity of switch statements is \$O(n)\$, where n is the number of cases.

Consider using an object to map the names you test in the switch statement. The map will hash the expression (type in switch(type)) and then find the correct case in the ideal \$O(1)\$ time.

Note: that there is an overhead for the hashing function. This design optimization is only worthwhile for switch statements with many cases.

Example from your code...

switch (type) {
    case "error":
    case "debug":
        return "console." + type + "(" + line + ");" + curs;
    case "echo":
        return "$TEMPLATEJSXOF(" + line + ");" + curs;
    case "raw":
        return "$TEMPLATETXTOF(" + line + ");" + curs;
    case "set":
        return "var " + line + ";" + curs;
    case "elif":
        return "}else if(" + line + "){" + curs;
    case "else":   
 ... // and so on
    default:
        return "";
}

can be written as...

const logsCommon = (type, line, curs) => `console.${type}(${ line });${ curs }`;
const PARSER = {
    error: logsCommon,
    debug: logsCommon,
    echo:     (type, line, curs) => `$\TEMPLATEJSXOF(${ line });${ curs }`,
    raw:      (type, line, curs) => `$\TEMPLATETXTOF(${ line });${ curs }`,
    ["set"]:  (type, line, curs) => `var ${ line };${ curs }`,
    elif:     (type, line, curs) => `}else if(${ line }){${ curs }`,
    ["else"]: (type, line, curs) => 
    ... // and so on

The switch statement thus becomes

  return PARSER[type]?.(type, line, curs) ?? "";

Note: that tokens and non-names can be added using bracket notation.

Note: the use of Template literals AKA (Template strings).

Note: use of arrow functions.

Note: use of optional chaining to call the function PARSER[type]?.

Note: use of nullish coalescing operator the ?? "" returns the default if the expression PARSER[type]?.(type, line, curs) evaluates to undefined or null

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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