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)+'}'; }