tempura
Version:
A light, crispy, and delicious template engine
144 lines (127 loc) • 4.14 kB
JavaScript
const ESCAPE = /[&"<]/g, CHARS = {
'"': '"',
'&': '&',
'<': '<',
};
const ENDLINES = /[\r\n]+$/g;
const CURLY = /{{{?\s*([\s\S]*?)\s*}}}?/g;
const VAR = /(?:^|[-*+^|%/&=\s])([a-zA-Z$_][\w$]*)(?:(?=$|[-*+^|%/&=\s]))/g;
const ARGS = /([a-zA-Z$_][^\s=]*)\s*=\s*((["`'])(?:(?=(\\?))\4.)*?\3|{[^}]*}|\[[^\]]*]|\S+)/g;
// $$1 = escape()
// $$2 = extra blocks
// $$3 = template values
function gen(input, options) {
options = options || {};
let char, num, action, tmp;
let last = CURLY.lastIndex = 0;
let wip='', txt='', match, inner;
let extra=options.blocks||{}, stack=[];
let initials = new Set(options.props||[]);
function close() {
if (wip.length > 0) {
txt += (txt ? 'x+=' : '=') + '`' + wip + '`;';
} else if (txt.length === 0) {
txt = '="";'
}
wip = '';
}
while (match = CURLY.exec(input)) {
wip += input.substring(last, match.index).replace(ENDLINES, '');
last = match.index + match[0].length;
inner = match[1].trim();
char = inner.charAt(0);
if (char === '!') {
// comment, continue
} else if (char === '#') {
close();
[, action, inner] = /^#\s*(\w[\w\d]+)\s*([^]*)/.exec(inner);
if (action === 'expect') {
inner.split(/[\n\r\s\t]*,[\n\r\s\t]*/g).forEach(key => {
initials.add(key);
});
} else if (action === 'var') {
num = inner.indexOf('=');
tmp = inner.substring(0, num++).trim();
inner = inner.substring(num).trim().replace(/[;]$/, '');
txt += `var ${tmp}=${inner};`;
} else if (action === 'each') {
num = inner.indexOf(' as ');
stack.push(action);
if (!~num) {
txt += `for(var i=0,$$a=${inner};i<$$a.length;i++){`;
} else {
tmp = inner.substring(0, num).trim();
inner = inner.substring(num + 4).trim();
let [item, idx='i'] = inner.replace(/[()\s]/g, '').split(','); // (item, idx?)
txt += `for(var ${idx}=0,${item},$$a=${tmp};${idx}<$$a.length;${idx}++){${item}=$$a[${idx}];`;
}
} else if (action === 'if') {
txt += `if(${inner}){`;
stack.push(action);
} else if (action === 'elif') {
txt += `}else if(${inner}){`;
} else if (action === 'else') {
txt += `}else{`;
} else if (action in extra) {
if (inner) {
tmp = [];
// parse arguments, `defer=true` -> `{ defer: true }`
while (match = ARGS.exec(inner)) tmp.push(match[1] + ':' + match[2]);
inner = tmp.length ? '{' + tmp.join() + '}' : '';
}
inner = inner || '{}';
tmp = options.async ? 'await ' : '';
wip += '${' + tmp + '$$2.' + action + '(' + inner + ',$$2)}';
} else {
throw new Error(`Unknown "${action}" block`);
}
} else if (char === '/') {
action = inner.substring(1);
inner = stack.pop();
close();
if (action === inner) txt += '}';
else throw new Error(`Expected to close "${inner}" block; closed "${action}" instead`);
} else {
if (match[0].charAt(2) === '{') wip += '${' + inner + '}'; // {{{ raw }}}
else wip += '${$$1(' + inner + ')}';
if (options.loose) {
while (tmp = VAR.exec(inner)) {
initials.add(tmp[1]);
}
}
}
}
if (stack.length > 0) {
throw new Error(`Unterminated "${stack.pop()}" block`);
}
if (last < input.length) {
wip += input.substring(last).replace(ENDLINES, '');
}
close();
tmp = initials.size ? `{${ [...initials].join() }}=$$3,x` : ' x';
return `var${tmp + txt}return x`;
}
export function esc(value) {
value = (value == null) ? '' : '' + value;
let last=ESCAPE.lastIndex=0, tmp=0, out='';
while (ESCAPE.test(value)) {
tmp = ESCAPE.lastIndex - 1;
out += value.substring(last, tmp) + CHARS[value[tmp]];
last = tmp + 1;
}
return out + value.substring(last);
}
export function compile(input, options={}) {
return new (options.async ? (async()=>{}).constructor : Function)(
'$$1', '$$2', '$$3', gen(input, options)
).bind(0, options.escape || esc, options.blocks);
}
export function transform(input, options={}) {
return (
options.format === 'cjs'
? 'var $$1=require("tempura").esc;module.exports='
: 'import{esc as $$1}from"tempura";export default '
) + (
options.async ? 'async ' : ''
) + 'function($$3,$$2){'+gen(input, options)+'}';
}