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={'&':'&','<':'<','>':'>','\"':'"','\\'':'''};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();
-
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\$Sᴀᴍ Onᴇᴌᴀ– Sᴀᴍ Onᴇᴌᴀ ♦2023年08月22日 06:11:50 +00:00Commented Aug 22, 2023 at 6:11
1 Answer 1
Style
Some style points.
JS does not use
snake_case
naming, try to use the JS stylecamelCase
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 beconst 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 invar 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 haveif (_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
loopfor (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