cb-template
Version:
A Node.js server-side template engine that supports multi-level template inheritance.
291 lines (222 loc) • 11.1 kB
JavaScript
import Layout from './lib/layout.js';
import helpers from './lib/helper.js';
import * as utils from './lib/utils.js';
const VERSION = '3.0.0';
const TEMPLATE_OUT = '__templateOut__';
const TEMPLATE_VAR_NAME = '__templateVarName__';
const TEMPLATE_SUB = '__templateSub__';
const TEMPLATE_OBJECT = '__templateObject__';
const TEMPLATE_NAME = '__templateName__';
const TEMPLATE_HELPER = '__templateHelper__';
const SUB_TEMPLATE = '__subTemplate__';
const FOREACH_INDEX = 'Index';
const core = {
// Mark current version
version: VERSION,
// Custom delimiters, can contain regex metacharacters, can be HTML comment tags <! !>
leftDelimiter: '<%',
rightDelimiter: '%>',
// Custom default escaping behavior, defaults to auto-escape
escape: true,
basePath: '',
cachePath: '',
defaultExtName: '.html',
// Compile template
compile(str) {
// Return template function
return this._buildTemplateFunction(this._parse(str));
},
// Render template function
render(str, data, subTemplate) {
// Return rendered content
return this.compile(str)(data, subTemplate);
},
// Compile template file with inheritance support
compileFile(filename, options = {}, callback) {
// Handle parameter overloading: check actual number of arguments
if (arguments.length === 2) {
callback = options;
options = {};
}
const instance = new Layout(this);
instance.make(filename, options, (err, content) => {
// Return template function
callback(err, this._buildTemplateFunction(content));
});
},
// Render template file with inheritance support
renderFile(filename, data, options = {}, callback) {
// Handle parameter overloading: check actual number of arguments
if (arguments.length === 3) {
callback = options;
options = {};
}
this.compileFile(filename, options, (err, func) => {
// Return rendered content
callback(err, func(data));
});
},
_buildTemplateFunction(str) {
let funcBody = `
if (${SUB_TEMPLATE}) {
${TEMPLATE_OBJECT} = { value: ${TEMPLATE_OBJECT} };
}
if (${TEMPLATE_HELPER}.isObject(${TEMPLATE_OBJECT})) {
let ${TEMPLATE_VAR_NAME} = '';
for (var ${TEMPLATE_NAME} in ${TEMPLATE_OBJECT}) {
${TEMPLATE_VAR_NAME} += 'var ' + ${TEMPLATE_NAME} + ' = ${TEMPLATE_OBJECT}["' + ${TEMPLATE_NAME} + '"];';
}
if (${TEMPLATE_VAR_NAME} !== '') {
eval(${TEMPLATE_VAR_NAME});
}
${TEMPLATE_VAR_NAME} = null;
}
let ${TEMPLATE_SUB} = {};
let ${TEMPLATE_OUT} /* init */ = '${str}';
return ${TEMPLATE_OUT};
`;
// Remove invalid directives
funcBody = funcBody.replace(new RegExp(`${TEMPLATE_OUT}\\s*\\+?=\\s*'';`, 'g'), '');
// console.log(funcBody.replace(/\\n/g, '\n'));
const func = new Function(TEMPLATE_HELPER, TEMPLATE_OBJECT, SUB_TEMPLATE, funcBody);
return (templateObject, subTemplate) => func(helpers, templateObject, subTemplate);
},
// Parse template string
_parse(str) {
// Get delimiters
const _left_ = this.leftDelimiter;
const _right_ = this.rightDelimiter;
// Escape delimiters, support regex metacharacters, can be HTML comments <! !>
const _left = utils.encodeReg(_left_);
const _right = utils.encodeReg(_right_);
str = String(str)
// Remove JS comments within delimiters
.replace(new RegExp("(" + _left + "[^" + _right + "]*)//.*\n", "g"), "$1")
// Default support for HTML comments, removing them because users might use <! !> as delimiters
//.replace(/<!--[\s\S]*?-->/g, '')
// Remove comment content <%* arbitrary comments here *%>
.replace(new RegExp(_left + '\\*[\\s\\S]*?\\*' + _right, 'gm'), '')
// Handle content outside delimiters containing backslashes \ and single quotes '
.replace(new RegExp(_left + "(?:(?!" + _right + ")[\\s\\S])*" + _right + "|((?:(?!" + _left + ")[\\s\\S])+)", "g"), (item, $1) => {
let str = '';
if ($1) {
// Escape backslashes and single quotes
str = $1.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
else {
str = item;
}
return str;
})
// Remove all line breaks \r carriage return \t tab \n newline
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n');
str = str
// Define variables, add semicolon if missing for error tolerance <%let val='test'%>
.replace(new RegExp("(" + _left + "\\s*?let\\s*?.+?\\s*?[^;])\\s*?" + _right, "g"),
`$1;${_right_}`)
// Handle trailing semicolons for variables (including escape modes like <%:h=value%>) <%=value;%> exclude function calls <%fun1();%> exclude variable definitions <%var val='test';%>
.replace(new RegExp("(" + _left + ":?[hvu]?\\s*?=\\s*?[^;|" + _right + "]*?);\\s*?" + _right, "g"),
`$1${_right_}`)
// foreach loop <% foreach (x in arr) %>
.replace(new RegExp(_left + "\\s*?foreach\\s*?\\((.+?)\\s+in\\s+(.+?)\\)\\s*?" + _right, "g"),
`${_left_}if/*-*/(typeof($2)!=='undefined'&&(Array.isArray($2)&&$2.length>0||${TEMPLATE_HELPER}.isObject($2)&&!${TEMPLATE_HELPER}.isEmptyObject($2))){${TEMPLATE_HELPER}.each($2,($1${FOREACH_INDEX},$1)=>{${_right_}`)
// foreachelse directive <% foreachelse %>
.replace(new RegExp(_left + "\\s*?foreachelse\\s*?" + _right, "g"),
`${_left_}})}else{${TEMPLATE_HELPER}.run(()=>{${_right_}`)
// foreachbreak directive <% foreachbreak %>
.replace(new RegExp(_left + "\\s*?foreachbreak\\s*?" + _right, "g"),
`${_left_}return false;${_right_}`)
// foreach loop end <% /foreach %>
.replace(new RegExp(_left + "\\s*?/foreach\\s*?" + _right, "g"),
`${_left_}})}${_right_}`)
// if directive <% if (x == 1) %>
.replace(new RegExp(_left + "\\s*?if\\s*?\\((.+?)\\)\\s*?" + _right, "g"),
`${_left_}if($1){${_right_}`)
// elseif directive <% elseif (x == 1) %>
.replace(new RegExp(_left + "\\s*?else\\s*?if\\s*?\\((.+?)\\)\\s*?" + _right, "g"),
`${_left_}}else if($1){${_right_}`)
// else directive <% else %>
.replace(new RegExp(_left + "\\s*?else\\s*?" + _right, "g"),
`${_left_}}else{${_right_}`)
// if directive end <% /if %>
.replace(new RegExp(_left + "\\s*?/if\\s*?" + _right, "g"),
`${_left_}}${_right_}`)
// Note: must compile other directives after native directives are compiled
// Define sub-template <% define value(param) %>
.replace(new RegExp(_left + "\\s*?define\\s+?([a-z0-9_$]+?)\\s*?\\((.*?)\\)\\s*?" + _right, "g"),
`${_left_}${TEMPLATE_SUB}['$1']=($2)=>{${_right_}`)
// Add direct sub-template invocation code at the end of the last sub-template!
.replace(new RegExp(_left + "\\s*?/define\\s*?(?![\\s\\S]*\\s*?/define\\s*?)" + _right, "g"),
`${_left_} /define ${_right_}${_left_}if(${SUB_TEMPLATE}){${TEMPLATE_OUT}='';if(${TEMPLATE_SUB}[${SUB_TEMPLATE}]){${TEMPLATE_SUB}[${SUB_TEMPLATE}](value)}${TEMPLATE_VAR_NAME}=null;return}${_right_}`)
// Define sub-template end <% /define %>
.replace(new RegExp(_left + "\\s*?/define\\s*?" + _right, "g"),
`${_left_}};${_right_}`)
// Call sub-template <% run value() %>
.replace(new RegExp(_left + "\\s*?run\\s+?([a-zA-Z0-9_$]+?)\\s*?\\((.*?)\\)\\s*?" + _right, "g"),
`${_left_}if(${TEMPLATE_SUB}['$1']){${TEMPLATE_SUB}['$1']($2)}${_right_}`)
// Split by <% into arrays, then join with \t, equivalent to replacing <% with \t
// Split template by <% into segments, add \t at the end of each segment, i.e., use \t to separate each template fragment
.split(_left_).join("\t");
// Support user configuration for default auto-escaping
if (this.escape) {
str = str
// Find \t=any character%> replace with ',any character,'
// Replace simple variables \t=data%> replace with ',data,'
// Default HTML escaping also supports HTML escape syntax <%:h=value%>
.replace(new RegExp("\\t=(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.encodeHTML($1))+'`);
}
else {
str = str
// Default no HTML escaping
.replace(new RegExp("\\t=(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':$1)+'`);
};
str = str
// Support HTML escape syntax <%:h=value%>
.replace(new RegExp("\\t:h=(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.encodeHTML($1))+'`)
// Support non-escape syntax <%:=value%> and <%-value%>
.replace(new RegExp("\\t(?::=|-)(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':$1)+'`)
// Support URL escaping <%:u=value%>
.replace(new RegExp("\\t:u=(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':encodeURIComponent($1))+'`)
// Support UI variables in HTML tag event function parameters like onclick <%:v=value%>
.replace(new RegExp("\\t:v=(.*?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.encodeEventHTML($1))+'`)
// Support array iteration <%:a=value|separator%>
.replace(new RegExp("\\t:a=(.+?)(?:\\|(.*?))?" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.forEachArray($1,'$2'))+'`)
// Support money formatting <%:m=value%>
.replace(new RegExp("\\t:m=(.+?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null)||isNaN($1))?'':Number(Math.round(($1)*100)/100).toFixed(2))+'`)
// String truncation with ellipsis <%:s=value|length%>
.replace(new RegExp("\\t:s=(.+?)\\|(\\d+?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.encodeHTML($1.length>$2?$1.substr(0,$2)+'...':$1))+'`)
// HTTP protocol auto-adaptation <%:p=value%>
.replace(new RegExp("\\t:p=(.+?)" + _right, "g"),
`'+((typeof($1)==='undefined'||(typeof($1)==='object'&&$1===null))?'':${TEMPLATE_HELPER}.encodeHTML(${TEMPLATE_HELPER}.replaceUrlProtocol($1)))+'`)
// <%:func=value%>
.replace(new RegExp("\\t:func=(.*?)" + _right, "g"),
`'+${TEMPLATE_HELPER}.encodeHTML($1)+'`)
// <%:func-value%>
.replace(new RegExp("\\t:func-(.*?)" + _right, "g"),
`'+($1)+'`)
// Split string by \t into arrays, then join with '; to replace trailing \t with ';'
.split("\t").join("';")
// Replace %> with ${TEMPLATE_OUT}+='
// Remove closing delimiter and generate string concatenation
.split(_right_).join(`${TEMPLATE_OUT}+='`);
// console.log(str);
return str;
}
};
export default {
...core,
getInstance() {
return { ...core };
}
};