UNPKG

arc-templates

Version:

Fully powered JavaScript template engine with halfway-decent syntax.

120 lines (106 loc) 4.48 kB
import Lexer from './Lexer'; import Context from './Context'; import tokens from './tokens'; import Promise from 'bluebird'; import { transform } from 'babel-core'; const MISSING_FILENAME = '<string>'; const globalEval = eval; function nameOrExpression(token, defaultIfEmpty) { let value = token.value.trim(); if (value === '') { if (defaultIfEmpty !== undefined) { value = defaultIfEmpty; } else { throw new Error(token.begin + ': ' + token.token + ' tag must contain a name or expression.'); } } return value.startsWith('(') ? value : JSON.stringify(value); } function compile(text, filename) { const lexer = new Lexer(text, filename); const buffer = []; for (let token of lexer.lex()) { switch (token.token) { case tokens.DOCUMENT: buffer.push('this._appendRaw(' + JSON.stringify(token.value) + ');'); break; case tokens.EXPRESSION: buffer.push('this._append(' + token.value + ');'); break; case tokens.JAVASCRIPT: buffer.push(token.value + ';'); break; case tokens.LAYOUT: buffer.push('this._layout = ' + nameOrExpression(token) + ';'); break; case tokens.BLOCK_REFERENCE: buffer.push('this._appendRaw(this.child[' + nameOrExpression(token, 'content') + '] || "");'); break; case tokens.BLOCK_NAME: buffer.push('this._currentBlock = ' + nameOrExpression(token) + ';'); break; case tokens.PARTIAL: buffer.push('this.partial = this._locals.partial = yield this._partial(' + nameOrExpression(token) + ');'); buffer.push('this._appendRaw(this._locals.partial.content);'); break; default: throw new Error("Internal error."); } } return buffer.join('\n'); } class Template { constructor(arc) { this.arc = arc; } get filename() { return this._filename || MISSING_FILENAME; } set filename(value) { this._filename = value; } load() { if (this.text !== undefined) { return Promise.resolve(this.text); } else { return this.arc.filesystem.readFile(this.filename).then(text => this.text = text); } } compile() { return this.load().then(text => { // As much as I dislike eval, GeneratorFunction.constructor isn't working yet for Node *or* Babel. // The awkward extra function wrapper for ES5 is required because Babel does not yet support 'with' statements within generator functions. const funcText = this.arc.supportES5 ? '(function () { with (this._locals) with (this.data) { return (function *() {\n' + compile(text, this.filename) + '\n}).bind(this); } })' : '(function *() { with (this._locals) with (this.data) {\n' + compile(text, this.filename) + '\n} })'; // We also Babel-transform for modern Node versions because it doesn't yet support block scoping (const, let, etc) outside of strict mode (as of 2015-10-07). // Ideally, for modern Node, this should just be: globalEval(funcText) const func = this.arc.supportES5 ? globalEval(transform(funcText, { blacklist: ['strict'] }).code) : globalEval(transform(funcText, { whitelist: ['es6.blockScoping'] }).code); const context = new Context(this, func, this.filename); return context._execute.bind(context); }); } evaluate(data, child) { return this.compile().then(execute => execute(data, child)); } joinedPath(path) { if (this.filename === MISSING_FILENAME) { return path; } return this.arc.path.join(this.arc.path.dirname(this.filename), path); } static fromFile(arc, filename) { const result = new Template(arc); result.filename = filename; return result; } static fromString(arc, text, filename) { const result = new Template(arc); result.text = text; result.filename = filename; return result; } } export default Template;