jtdal
Version:
Small template engine based on Zope TAL, using data attributes
362 lines (361 loc) • 20.3 kB
JavaScript
;
export default class jTDAL {
static _keywords = ['condition', 'repeat', 'content', 'replace', 'attributes', 'omittag'];
static _regexpPatternPath = '(?:[\\w\\-\\/]*[\\w](?:[\\s]*\\|[\\s]*[\\w\\-\\/]*[\\w])*)';
static _regexpPatternString = 'STRING:(?:[^;](?:(?!<=;);)?)+';
static _regexpPatternMacro = 'MACRO:[a-zA-Z0-9-]+';
static _regexpPatternPathBoolean = '(?:(?:!)?[\\w\\-\\/]*[\\w](?:[\\s]*\\|[\\s]*[\\w\\-\\/]*[\\w])*)';
static _regexpPatternExpressionAllowedBoolean = '(?:' + jTDAL._regexpPatternString + '|(?:' + jTDAL._regexpPatternPathBoolean + ')(?:[\\s]*\\|[\\s]*' + jTDAL._regexpPatternString + ')?)';
static _regexpPatternExpressionAllowedBooleanMacro = '(?:' + jTDAL._regexpPatternMacro + '|' + jTDAL._regexpPatternString + '|' + jTDAL._regexpPatternPathBoolean + '(?:[\\s]*\\|[\\s]*' + jTDAL._regexpPatternString + ')?)';
static _regexpTagWithTDAL = new RegExp('<((?:\\w+:)?\\w+)(\\s+[^<>]+?)??\\s+\\bdata-tdal-(?:' + jTDAL._keywords.join('|') +
')\\b=([\'"])(.*?)\\3(\\s+[^<>]+?)??\\s*(\/)?>', 'i');
static _regexpTagAttributes = /\s((?:[\w-]+:)?[\w-]+)=(?:(['"])(.*?)\2|([^>\s'"]+))/gi;
static _regexpPathString = new RegExp('(?:{(' + jTDAL._regexpPatternPath + ')}|{\\?(' + jTDAL._regexpPatternPathBoolean + ')}(.*?){\\/\\2})');
static _regexpCondition = new RegExp('^[\\s]*(' + jTDAL._regexpPatternExpressionAllowedBoolean + ')[\\s]*$');
static _regexpRepeat = new RegExp('^[\\s]*([\\w\\-]+?)[\\s]+(' + jTDAL._regexpPatternPath + ')[\\s]*$');
static _regexpContent = new RegExp('^[\\s]*(?:(structure)[\\s]+)?(' + jTDAL._regexpPatternExpressionAllowedBooleanMacro + ')[\\s]*$');
static _regexpAttributes = new RegExp('[\\s]*(?:(?:([\\w\\-]+)(\\??)[\\s]+(' + jTDAL._regexpPatternExpressionAllowedBoolean + ')[\\s]*)(?:;;[\\s]*|$))', 'g');
static _regexpAttributesTDAL = /\s*(data-tdal-[\w-]+)=(?:(['"])(.*?)\2|([^>\s'"]+))/gi;
static _HTML5VoidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
_macros = {};
static _ParseString(stringExpression, macros = {}) {
let returnValue = '""';
let match;
while (null != (match = jTDAL._regexpPathString.exec(stringExpression))) {
if (0 < match['index']) {
returnValue += '+' + JSON.stringify(String(stringExpression.substring(0, match['index'])));
}
stringExpression = stringExpression.substring(match['index'] + match[0].length);
if (match[1]) {
returnValue += '+(a(' + jTDAL._ParsePath(match[1], false, macros) + ')&&("string"===typeof t[t[0]]||("number"===typeof t[t[0]]&&!isNaN(t[t[0]])))?t[t[0]]:"")';
}
else if (match[2]) {
const tmpValue = jTDAL._ParsePath(match[2], true, macros);
if ('true' === tmpValue) {
returnValue += '+' + this._ParseString(match[3], macros);
}
else if ('false' !== tmpValue) {
returnValue += '+(true===' + tmpValue + '?""+' + this._ParseString(match[3], macros) + ':"")';
}
}
}
if (0 < stringExpression.length) {
returnValue += '+' + JSON.stringify(String(stringExpression));
}
return returnValue;
}
static _ParsePath(pathExpression, getBoolean = false, macros = {}) {
let returnValue = '';
if (pathExpression) {
paths: {
const paths = pathExpression.split('|');
const cL1 = paths.length;
for (let iL1 = 0; iL1 < cL1; ++iL1) {
if (0 != iL1) {
returnValue += '||';
}
let currentPath = paths[iL1].replace(/^\s+/, '');
if (currentPath.startsWith('STRING:')) {
returnValue += '(' + this._ParseString(currentPath.substring(7)) + ')';
break paths;
}
else if (currentPath.startsWith('MACRO:')) {
if (undefined !== macros[currentPath.substring(6)]) {
returnValue += '("function"===typeof m["' + currentPath.substring(6) + '"]?m["' + currentPath.substring(6) + '"]():false)';
}
else {
returnValue += 'false';
}
break paths;
}
else {
currentPath = currentPath.replace(/\s+$/, '');
const not = ('!' === currentPath[0]);
const boolPath = getBoolean || not;
const path = (not ? currentPath.substring(1) : currentPath).split('/');
if ((0 < path.length) && (0 < path[0].length)) {
switch (path[0]) {
case 'FALSE': {
if (not) {
returnValue += 'true';
}
else {
returnValue += 'false';
}
break paths;
}
case 'TRUE': {
if (not) {
returnValue += 'false';
}
else {
returnValue += 'true';
}
break paths;
}
case 'REPEAT': {
if (3 == path.length) {
returnValue += (boolPath ? (not ? '!' : '') + 'b(' : '') + 'c(r,"' + path.join('/') + '")' + (boolPath ? ')' : '');
}
break;
}
case 'GLOBAL': {
if (1 < path.length && 0 < path[1].length) {
returnValue += (boolPath ? (not ? '!' : '') + 'b(' : '') + 'c(d,"' + path.slice(1).join('/') + '")' + (boolPath ? ')' : '');
}
break;
}
default: {
if (boolPath) {
returnValue += (not ? '!(' : '') + '(a(c(r,"' + path.join('/') + '"))&&b(t[t[0]]))||(a(c(d,"' + path.join('/') + '"))&&b(t[t[0]]))' + (not ? ')' : '');
}
else {
returnValue += '((a(c(r,"' + path.join('/') + '"))||a(c(d,"' + path.join('/') + '")))?t[t[0]]:false)';
}
}
}
}
}
}
}
}
else {
returnValue = 'false';
}
return returnValue;
}
_Parse(template) {
let returnValue = '';
let tmpTDALTags;
const attributesPrefix = 'data-tdal-';
while (null !== (tmpTDALTags = jTDAL._regexpTagWithTDAL.exec(template))) {
if (0 < tmpTDALTags['index']) {
returnValue += "+" + JSON.stringify(String(template.substring(0, tmpTDALTags['index'])));
}
template = template.substring(tmpTDALTags['index'] + tmpTDALTags[0].length);
let selfClosed = !!tmpTDALTags[6] || jTDAL._HTML5VoidElements.includes(tmpTDALTags[1].toLowerCase());
const current = ['', tmpTDALTags[0], '', '', '', '', '', ''];
const attributes = {};
const matches = tmpTDALTags[0].matchAll(jTDAL._regexpTagAttributes);
for (const match of matches) {
attributes[match[1]] = match;
}
current[1] = current[1].replaceAll(jTDAL._regexpAttributesTDAL, '');
if (!selfClosed) {
const endTag = new RegExp('<(\\/)?' + tmpTDALTags[1] + '[^<>]*(?<!\\/)>', 'gi');
let closingPosition = [];
let tags = 1;
let tmpMatch;
while ((undefined === closingPosition[0]) && (null !== (tmpMatch = endTag.exec(template)))) {
if (!tmpMatch[1]) {
tags++;
}
else {
tags--;
}
if (0 == tags) {
closingPosition = [tmpMatch['index'], tmpMatch[0].length];
}
}
if (undefined === closingPosition[0]) {
selfClosed = true;
}
else {
current[4] += this._Parse(template.substring(0, closingPosition[0]));
current[6] += template.substring(closingPosition[0], closingPosition[0] + closingPosition[1]);
template = template.substring(closingPosition[0] + closingPosition[1]);
}
}
tdal: {
let tmpMatch;
let tmpValue;
let attribute = attributesPrefix + jTDAL._keywords[0];
if (attributes[attribute] && (jTDAL._regexpCondition.exec(attributes[attribute][3]))) {
tmpValue = jTDAL._ParsePath(attributes[attribute][3], true, this._macros);
if ('false' === tmpValue) {
break tdal;
}
else if ('true' !== tmpValue) {
current[0] += '+(true===' + tmpValue + '?""';
current[7] = ':"")' + current[7];
}
}
attribute = attributesPrefix + jTDAL._keywords[1];
if (attributes[attribute] && (tmpMatch = jTDAL._regexpRepeat.exec(attributes[attribute][3]))) {
tmpValue = jTDAL._ParsePath(tmpMatch[2], false, this._macros);
if (('false' == tmpValue) || ('""' == tmpValue) || ('true' == tmpValue)) {
break tdal;
}
else {
current[0] += '+(';
current[0] += '((';
current[0] += 'a(' + tmpValue + ')&&';
current[0] += '(!Array.isArray(t[t[0]])||(t[t[0]]=Object.assign({},t[t[0]])))&&';
current[0] += '("object"===typeof t[t[0]]&&null!==t[t[0]]&&Object.keys(t[t[0]]).length)';
current[0] += ')?((t[++t[0]]=1)&&t[0]++):((t[0]+=2)&&false))';
current[0] += '?';
current[0] += 'Object.keys(t[t[0]-2]).reduce((o,e)=>{';
current[0] += 'r["' + tmpMatch[1] + '"]=t[t[0]-2][e];';
current[0] += 'const n=t[t[0]-1]++,l=Object.keys(t[t[0]-2]).length;';
current[0] += 'r["REPEAT"]["' + tmpMatch[1] + '"]={' +
'index:e,' +
'number:n,' +
'length:l,' +
'even:0==(n%2),' +
'odd:1==(n%2),' +
'first:1==n,' +
'last:l==n' +
'};';
current[0] += 'return o';
current[7] = ';},""):"")+((t[0]-=2)&&(delete r["REPEAT"]["' + tmpMatch[1] + '"])&&delete(r["' + tmpMatch[1] + '"])?"":"")' + current[7];
}
}
attribute = attributesPrefix + jTDAL._keywords[2];
if (attributes[attribute] && (tmpMatch = jTDAL._regexpContent.exec(attributes[attribute][3]))) {
tmpValue = jTDAL._ParsePath(tmpMatch[2], false, this._macros);
if ('false' == tmpValue) {
current[4] = '';
}
else if ('true' != tmpValue) {
let encoding = ['', ''];
if ('structure' != tmpMatch[1]) {
encoding[0] = 'String(';
encoding[1] = ').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)';
}
current[3] += '+(a(' + tmpValue + ')&&("string"===typeof t[t[0]]||("number"===typeof t[t[0]]&&!isNaN(t[t[0]])))?' + encoding[0] + 't[t[0]]' + encoding[1] + ':(true!==t[t[0]]?"":""';
current[5] += '))';
}
}
else {
attribute = attributesPrefix + jTDAL._keywords[3];
if (attributes[attribute] && (tmpMatch = jTDAL._regexpContent.exec(attributes[attribute][3]))) {
tmpValue = jTDAL._ParsePath(tmpMatch[2], false, this._macros);
if ('false' == tmpValue) {
current[1] = '';
current[4] = '';
current[6] = '';
}
else if ('true' != tmpValue) {
let encoding = ['', ''];
if ('structure' != tmpMatch[1]) {
encoding[0] = 'String(';
encoding[1] = ').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)';
}
current[0] += '+(a(' + tmpValue + ')&&("string"===typeof t[t[0]]||("number"===typeof t[t[0]]&&!isNaN(t[t[0]])))?' + encoding[0] + 't[t[0]]' + encoding[1] + ':(true!==t[t[0]]?"":""';
current[7] = '))' + current[7];
}
}
}
attribute = attributesPrefix + jTDAL._keywords[4];
if (attributes[attribute]) {
const matches = attributes[attribute][3].matchAll(jTDAL._regexpAttributes);
for (tmpMatch of matches) {
const isFlag = '?' === tmpMatch[2];
tmpValue = jTDAL._ParsePath(tmpMatch[3], isFlag, this._macros);
if ('false' === tmpValue) {
if (undefined !== attributes[tmpMatch[1]]) {
current[1] = current[1].replace(new RegExp('\\s*\\b' + tmpMatch[1] + '\\b(?:=([\'"]).*?\\1)?(?=\\s|\\/?>)'), '');
}
}
else if ('true' !== tmpValue) {
if (isFlag) {
current[2] += `+(${tmpValue}?" ${tmpMatch[1]}":""`;
}
else {
current[2] += `+(a(${tmpValue})&&( "string" === typeof t[t[0]] || ( "number" === typeof t[t[0]] && !isNaN(t[t[0]])))?" ${tmpMatch[1]}=\\""+t[t[0]]+"\\"":( true !== t[t[0]]?"":"${tmpMatch[1]}"`;
}
if (undefined !== attributes[tmpMatch[1]]) {
current[1] = current[1].replace(new RegExp('\\s*\\b' + tmpMatch[1] + '\\b(?:=([\'"]).*?\\1)?(?=\\s|\\/?>)'), '');
current[2] += (((undefined !== attributes[tmpMatch[1]][3]) && ('' != attributes[tmpMatch[1]][3])) ? '+"="+' + JSON.stringify(String(attributes[tmpMatch[1]][2] + attributes[tmpMatch[1]][3] + attributes[tmpMatch[1]][2])) : "");
}
current[2] += ')';
if (!isFlag) {
current[2] += ')';
}
}
}
}
attribute = attributesPrefix + jTDAL._keywords[5];
if (attributes[attribute] && (jTDAL._regexpCondition.exec(attributes[attribute][3]))) {
tmpValue = jTDAL._ParsePath(attributes[attribute][3], true, this._macros);
if ('true' == tmpValue) {
current[1] = '';
current[6] = '';
}
else if ('false' != tmpValue) {
current[0] += '+(' + tmpValue + '?"":""';
current[3] = ')' + current[3];
current[5] += '+(' + tmpValue + '?"":""';
current[7] = ')' + current[7];
}
}
}
current[1] = current[1].replace(/\s*\/?>$/, '');
if (selfClosed && (('' != current[4]) || ('' != current[3]) || ('' != current[5]))) {
current[6] = '</' + tmpTDALTags[1] + '>';
selfClosed = false;
}
returnValue += current[0] + '+' + JSON.stringify(String(current[1])) + current[2] +
(('' != current[1]) ? '+"' + (selfClosed ? '/' : '') + '>"' : '') + current[3] + current[4] + current[5] + '+' +
JSON.stringify(String(current[6])) + current[7];
}
returnValue += '+' + JSON.stringify(String(template));
return returnValue;
}
MacroAdd(macroName, template, trim = true, strip = true) {
let returnValue = false;
if (macroName.match(/^[a-zA-Z0-9-]*$/)) {
returnValue = true;
this._macros[macroName] = '""' + this._Parse(strip ? template.replace(/<!--.*?-->/sg, '') : template);
if (trim) {
this._macros[macroName] = '(' + this._macros[macroName] + ').trim()';
}
}
return returnValue;
}
constructor(macros = [], trim = true, strip = true) {
const cL1 = macros.length;
for (let iL1 = 0; iL1 < cL1; iL1++) {
this.MacroAdd(macros[iL1][0], macros[iL1][1], trim, strip);
}
}
_Compile(template, trim = true, strip = true) {
const tmpArray = ['', ''];
let tmpValue = this._Parse(strip ? template.replace(/<!--.*?-->/sg, '') : template);
let returnValue = 'const r={"REPEAT":{}}';
returnValue += ',t=[1]';
returnValue += ',m={' + Object.keys(this._macros).map((macro) => '"' + macro + '":()=>' + this._macros[macro]).join(',') + '}';
returnValue += ',a=(e)=>{';
returnValue += 't[t[0]]=e;';
returnValue += 'return false!==t[t[0]]';
returnValue += '}';
returnValue += ',c=(a,b)=>{';
returnValue += 'let z=!1,y=b.split("/"),x,w;';
returnValue += 'if(0<y.length&&0<y[0].length)';
returnValue += 'for(z=a,x=0;x<y.length&&1!==z;x++)';
returnValue += 'z="object"===typeof z&&null!==z&&void 0!==(w="function"===typeof z[y[x]]?z[y[x]](d,r):z[y[x]])&&w;';
returnValue += 'return z';
returnValue += '}';
returnValue += ',b=(v)=>{';
returnValue += 'return v&&("object"===typeof v?0<Object.keys(v).length:(Array.isArray(v)?0<v.length:true))';
returnValue += '}';
returnValue += ';';
if (trim) {
tmpArray[0] = '(';
tmpArray[1] = ').trim()';
}
returnValue += 'return ' + tmpArray[0] + '""' + tmpValue + tmpArray[1];
tmpValue = '';
do {
tmpValue = returnValue;
returnValue = returnValue.replace(/(?<!\\)"\+"/g, '').replace('(true!==t[t[0]]?"":"")', '""');
} while (tmpValue != returnValue);
return returnValue;
}
CompileToFunction(template, trim = true, strip = true) {
return new Function('d', this._Compile(template, trim, strip));
}
CompileToString(template, trim = true, strip = true) {
return 'function(d){' + this._Compile(template, trim, strip) + '}';
}
}