import { dict, unreachable, Stack, DictSet } from '@glimmer/util'; import { Ops, isFlushElement, isArgument, isAttribute, isAttrSplat } from '@glimmer/wire-format'; import { isLiteral, SyntaxError, preprocess } from '@glimmer/syntax'; class SymbolTable { static top() { return new ProgramSymbolTable(); } child(locals) { let symbols = locals.map(name => this.allocate(name)); return new BlockSymbolTable(this, locals, symbols); } } class ProgramSymbolTable extends SymbolTable { constructor() { super(...arguments); this.symbols = []; this.size = 1; this.named = dict(); this.blocks = dict(); } has(_name) { return false; } get(_name) { throw unreachable(); } getLocalsMap() { return {}; } getEvalInfo() { return []; } allocateNamed(name) { let named = this.named[name]; if (!named) { named = this.named[name] = this.allocate(name); } return named; } allocateBlock(name) { let block = this.blocks[name]; if (!block) { block = this.blocks[name] = this.allocate(`&${name}`); } return block; } allocate(identifier) { this.symbols.push(identifier); return this.size++; } } class BlockSymbolTable extends SymbolTable { constructor(parent, symbols, slots) { super(); this.parent = parent; this.symbols = symbols; this.slots = slots; } has(name) { return this.symbols.indexOf(name) !== -1 || this.parent.has(name); } get(name) { let slot = this.symbols.indexOf(name); return slot === -1 ? this.parent.get(name) : this.slots[slot]; } getLocalsMap() { let dict$$1 = this.parent.getLocalsMap(); this.symbols.forEach(symbol => dict$$1[symbol] = this.get(symbol)); return dict$$1; } getEvalInfo() { let locals = this.getLocalsMap(); return Object.keys(locals).map(symbol => locals[symbol]); } allocateNamed(name) { return this.parent.allocateNamed(name); } allocateBlock(name) { return this.parent.allocateBlock(name); } allocate(identifier) { return this.parent.allocate(identifier); } } /** * Takes in an AST and outputs a list of actions to be consumed * by a compiler. For example, the template * * foo{{bar}}
baz
* * produces the actions * * [['startProgram', [programNode, 0]], * ['text', [textNode, 0, 3]], * ['mustache', [mustacheNode, 1, 3]], * ['openElement', [elementNode, 2, 3, 0]], * ['text', [textNode, 0, 1]], * ['closeElement', [elementNode, 2, 3], * ['endProgram', [programNode]]] * * This visitor walks the AST depth first and backwards. As * a result the bottom-most child template will appear at the * top of the actions list whereas the root template will appear * at the bottom of the list. For example, * *
{{#if}}foo{{else}}bar{{/if}}
* * produces the actions * * [['startProgram', [programNode, 0]], * ['text', [textNode, 0, 2, 0]], * ['openElement', [elementNode, 1, 2, 0]], * ['closeElement', [elementNode, 1, 2]], * ['endProgram', [programNode]], * ['startProgram', [programNode, 0]], * ['text', [textNode, 0, 1]], * ['endProgram', [programNode]], * ['startProgram', [programNode, 2]], * ['openElement', [elementNode, 0, 1, 1]], * ['block', [blockNode, 0, 1]], * ['closeElement', [elementNode, 0, 1]], * ['endProgram', [programNode]]] * * The state of the traversal is maintained by a stack of frames. * Whenever a node with children is entered (either a ProgramNode * or an ElementNode) a frame is pushed onto the stack. The frame * contains information about the state of the traversal of that * node. For example, * * - index of the current child node being visited * - the number of mustaches contained within its child nodes * - the list of actions generated by its child nodes */ class Frame { constructor() { this.parentNode = null; this.children = null; this.childIndex = null; this.childCount = null; this.childTemplateCount = 0; this.mustacheCount = 0; this.actions = []; this.blankChildTextNodes = null; this.symbols = null; } } class TemplateVisitor { constructor() { this.frameStack = []; this.actions = []; this.programDepth = -1; } visit(node) { this[node.type](node); } // Traversal methods Program(program) { this.programDepth++; let parentFrame = this.getCurrentFrame(); let programFrame = this.pushFrame(); if (!parentFrame) { program['symbols'] = SymbolTable.top(); } else { program['symbols'] = parentFrame.symbols.child(program.blockParams); } let startType, endType; if (this.programDepth === 0) { startType = 'startProgram'; endType = 'endProgram'; } else { startType = 'startBlock'; endType = 'endBlock'; } programFrame.parentNode = program; programFrame.children = program.body; programFrame.childCount = program.body.length; programFrame.blankChildTextNodes = []; programFrame.actions.push([endType, [program, this.programDepth]]); programFrame.symbols = program['symbols']; for (let i = program.body.length - 1; i >= 0; i--) { programFrame.childIndex = i; this.visit(program.body[i]); } programFrame.actions.push([startType, [program, programFrame.childTemplateCount, programFrame.blankChildTextNodes.reverse()]]); this.popFrame(); this.programDepth--; // Push the completed template into the global actions list if (parentFrame) { parentFrame.childTemplateCount++; } this.actions.push(...programFrame.actions.reverse()); } ElementNode(element) { let parentFrame = this.currentFrame; let elementFrame = this.pushFrame(); elementFrame.parentNode = element; elementFrame.children = element.children; elementFrame.childCount = element.children.length; elementFrame.mustacheCount += element.modifiers.length; elementFrame.blankChildTextNodes = []; elementFrame.symbols = element['symbols'] = parentFrame.symbols.child(element.blockParams); let actionArgs = [element, parentFrame.childIndex, parentFrame.childCount]; elementFrame.actions.push(['closeElement', actionArgs]); for (let i = element.attributes.length - 1; i >= 0; i--) { this.visit(element.attributes[i]); } for (let i = element.children.length - 1; i >= 0; i--) { elementFrame.childIndex = i; this.visit(element.children[i]); } let open = ['openElement', [...actionArgs, elementFrame.mustacheCount, elementFrame.blankChildTextNodes.reverse()]]; elementFrame.actions.push(open); this.popFrame(); // Propagate the element's frame state to the parent frame if (elementFrame.mustacheCount > 0) { parentFrame.mustacheCount++; } parentFrame.childTemplateCount += elementFrame.childTemplateCount; parentFrame.actions.push(...elementFrame.actions); } AttrNode(attr) { if (attr.value.type !== 'TextNode') { this.currentFrame.mustacheCount++; } } TextNode(text) { let frame = this.currentFrame; if (text.chars === '') { frame.blankChildTextNodes.push(domIndexOf(frame.children, text)); } frame.actions.push(['text', [text, frame.childIndex, frame.childCount]]); } BlockStatement(node) { let frame = this.currentFrame; frame.mustacheCount++; frame.actions.push(['block', [node, frame.childIndex, frame.childCount]]); if (node.inverse) { this.visit(node.inverse); } if (node.program) { this.visit(node.program); } } PartialStatement(node) { let frame = this.currentFrame; frame.mustacheCount++; frame.actions.push(['mustache', [node, frame.childIndex, frame.childCount]]); } CommentStatement(text) { let frame = this.currentFrame; frame.actions.push(['comment', [text, frame.childIndex, frame.childCount]]); } MustacheCommentStatement() { // Intentional empty: Handlebars comments should not affect output. } MustacheStatement(mustache) { let frame = this.currentFrame; frame.mustacheCount++; frame.actions.push(['mustache', [mustache, frame.childIndex, frame.childCount]]); } // Frame helpers get currentFrame() { return this.getCurrentFrame(); } getCurrentFrame() { return this.frameStack[this.frameStack.length - 1]; } pushFrame() { let frame = new Frame(); this.frameStack.push(frame); return frame; } popFrame() { return this.frameStack.pop(); } } // Returns the index of `domNode` in the `nodes` array, skipping // over any nodes which do not represent DOM nodes. function domIndexOf(nodes, domNode) { let index = -1; for (let i = 0; i < nodes.length; i++) { let node = nodes[i]; if (node.type !== 'TextNode' && node.type !== 'ElementNode') { continue; } else { index++; } if (node === domNode) { return index; } } return -1; } class Block { constructor() { this.statements = []; } push(statement) { this.statements.push(statement); } } class InlineBlock extends Block { constructor(table) { super(); this.table = table; } toJSON() { return { statements: this.statements, parameters: this.table.slots }; } } class TemplateBlock extends Block { constructor(symbolTable) { super(); this.symbolTable = symbolTable; this.type = 'template'; this.yields = new DictSet(); this.named = new DictSet(); this.blocks = []; this.hasEval = false; } push(statement) { this.statements.push(statement); } toJSON() { return { symbols: this.symbolTable.symbols, statements: this.statements, hasEval: this.hasEval }; } } class ComponentBlock extends Block { constructor(tag, table, selfClosing) { super(); this.tag = tag; this.table = table; this.selfClosing = selfClosing; this.attributes = []; this.arguments = []; this.inParams = true; this.positionals = []; } push(statement) { if (this.inParams) { if (isFlushElement(statement)) { this.inParams = false; } else if (isArgument(statement)) { this.arguments.push(statement); } else if (isAttribute(statement)) { this.attributes.push(statement); } else if (isAttrSplat(statement)) { this.attributes.push(statement); } else { throw new Error('Compile Error: only parameters allowed before flush-element'); } } else { this.statements.push(statement); } } toJSON() { let args = this.arguments; let keys = args.map(arg => arg[1]); let values = args.map(arg => arg[2]); let block = this.selfClosing ? null : { statements: this.statements, parameters: this.table.slots }; return [this.tag, this.attributes, [keys, values], block]; } } class Template { constructor(symbols) { this.block = new TemplateBlock(symbols); } toJSON() { return this.block.toJSON(); } } class JavaScriptCompiler { constructor(opcodes, symbols, options) { this.blocks = new Stack(); this.values = []; this.opcodes = opcodes; this.template = new Template(symbols); this.options = options; } static process(opcodes, symbols, options) { let compiler = new JavaScriptCompiler(opcodes, symbols, options); return compiler.process(); } get currentBlock() { return this.blocks.current; } process() { this.opcodes.forEach(op => { let opcode = op[0]; let arg = op[1]; if (!this[opcode]) { throw new Error(`unimplemented ${opcode} on JavaScriptCompiler`); } this[opcode](arg); }); return this.template; } /// Nesting startBlock(program) { let block = new InlineBlock(program['symbols']); this.blocks.push(block); } endBlock() { let { template, blocks } = this; let block = blocks.pop(); template.block.blocks.push(block.toJSON()); } startProgram() { this.blocks.push(this.template.block); } endProgram() {} /// Statements text(content) { this.push([Ops.Text, content]); } append(trusted) { this.push([Ops.Append, this.popValue(), trusted]); } comment(value) { this.push([Ops.Comment, value]); } modifier(name) { let params = this.popValue(); let hash = this.popValue(); this.push([Ops.Modifier, name, params, hash]); } block([name, template, inverse]) { let params = this.popValue(); let hash = this.popValue(); let blocks = this.template.block.blocks; this.push([Ops.Block, name, params, hash, blocks[template], blocks[inverse]]); } openComponent(element) { let tag = this.options && this.options.customizeComponentName ? this.options.customizeComponentName(element.tag) : element.tag; let component = new ComponentBlock(tag, element['symbols'], element.selfClosing); this.blocks.push(component); } openSplattedElement(element) { let tag = element.tag; if (element.blockParams.length > 0) { throw new Error(`Compile Error: <${element.tag}> is not a component and doesn't support block parameters`); } else { this.push([Ops.OpenSplattedElement, tag]); } } openElement(element) { let tag = element.tag; if (element.blockParams.length > 0) { throw new Error(`Compile Error: <${element.tag}> is not a component and doesn't support block parameters`); } else { this.push([Ops.OpenElement, tag]); } } flushElement() { this.push([Ops.FlushElement]); } closeComponent(_element) { if (_element.modifiers.length > 0) { throw new Error('Compile Error: Element modifiers are not allowed in components'); } let [tag, attrs, args, block] = this.endComponent(); this.push([Ops.Component, tag, attrs, args, block]); } closeDynamicComponent(_element) { let [, attrs, args, block] = this.endComponent(); this.push([Ops.DynamicComponent, this.popValue(), attrs, args, block]); } closeElement(_element) { this.push([Ops.CloseElement]); } staticAttr([name, namespace]) { let value = this.popValue(); this.push([Ops.StaticAttr, name, value, namespace]); } dynamicAttr([name, namespace]) { let value = this.popValue(); this.push([Ops.DynamicAttr, name, value, namespace]); } trustingAttr([name, namespace]) { let value = this.popValue(); this.push([Ops.TrustingAttr, name, value, namespace]); } staticArg(name) { let value = this.popValue(); this.push([Ops.StaticArg, name, value]); } dynamicArg(name) { let value = this.popValue(); this.push([Ops.DynamicArg, name, value]); } yield(to) { let params = this.popValue(); this.push([Ops.Yield, to, params]); } attrSplat(to) { this.push([Ops.AttrSplat, to]); } debugger(evalInfo) { this.push([Ops.Debugger, evalInfo]); this.template.block.hasEval = true; } hasBlock(name) { this.pushValue([Ops.HasBlock, name]); } hasBlockParams(name) { this.pushValue([Ops.HasBlockParams, name]); } partial(evalInfo) { let params = this.popValue(); this.push([Ops.Partial, params[0], evalInfo]); this.template.block.hasEval = true; } /// Expressions literal(value) { if (value === undefined) { this.pushValue([Ops.Undefined]); } else { this.pushValue(value); } } unknown(name) { this.pushValue([Ops.Unknown, name]); } get([head, path]) { this.pushValue([Ops.Get, head, path]); } maybeLocal(path) { this.pushValue([Ops.MaybeLocal, path]); } concat() { this.pushValue([Ops.Concat, this.popValue()]); } helper(name) { let params = this.popValue(); let hash = this.popValue(); this.pushValue([Ops.Helper, name, params, hash]); } /// Stack Management Opcodes prepareArray(size) { let values = []; for (let i = 0; i < size; i++) { values.push(this.popValue()); } this.pushValue(values); } prepareObject(size) { let keys = new Array(size); let values = new Array(size); for (let i = 0; i < size; i++) { keys[i] = this.popValue(); values[i] = this.popValue(); } this.pushValue([keys, values]); } /// Utilities endComponent() { let component = this.blocks.pop(); return component.toJSON(); } push(args) { while (args[args.length - 1] === null) { args.pop(); } this.currentBlock.push(args); } pushValue(val) { this.values.push(val); } popValue() { return this.values.pop(); } } // There is a small whitelist of namespaced attributes specially // enumerated in // https://www.w3.org/TR/html/syntax.html#attributes-0 // // > When a foreign element has one of the namespaced attributes given by // > the local name and namespace of the first and second cells of a row // > from the following table, it must be written using the name given by // > the third cell from the same row. // // In all other cases, colons are interpreted as a regular character // with no special meaning: // // > No other namespaced attribute can be expressed in the HTML syntax. const XLINK = 'http://www.w3.org/1999/xlink'; const XML = 'http://www.w3.org/XML/1998/namespace'; const XMLNS = 'http://www.w3.org/2000/xmlns/'; const WHITELIST = { 'xlink:actuate': XLINK, 'xlink:arcrole': XLINK, 'xlink:href': XLINK, 'xlink:role': XLINK, 'xlink:show': XLINK, 'xlink:title': XLINK, 'xlink:type': XLINK, 'xml:base': XML, 'xml:lang': XML, 'xml:space': XML, xmlns: XMLNS, 'xmlns:xlink': XMLNS }; function getAttrNamespace(attrName) { return WHITELIST[attrName] || null; } class SymbolAllocator { constructor(ops) { this.ops = ops; this.symbolStack = new Stack(); } process() { let out = []; let { ops } = this; for (let i = 0; i < ops.length; i++) { let op = ops[i]; let result = this.dispatch(op); if (result === undefined) { out.push(op); } else { out.push(result); } } return out; } dispatch(op) { let name = op[0]; let operand = op[1]; return this[name](operand); } get symbols() { return this.symbolStack.current; } startProgram(op) { this.symbolStack.push(op['symbols']); } endProgram(_op) { this.symbolStack.pop(); } startBlock(op) { this.symbolStack.push(op['symbols']); } endBlock(_op) { this.symbolStack.pop(); } flushElement(op) { this.symbolStack.push(op['symbols']); } closeElement(_op) { this.symbolStack.pop(); } closeComponent(_op) { this.symbolStack.pop(); } closeDynamicComponent(_op) { this.symbolStack.pop(); } attrSplat(_op) { return ['attrSplat', this.symbols.allocateBlock('attrs')]; } get(op) { let [name, rest] = op; if (name === 0) { return ['get', [0, rest]]; } if (isLocal(name, this.symbols)) { let head = this.symbols.get(name); return ['get', [head, rest]]; } else if (name[0] === '@') { let head = this.symbols.allocateNamed(name); return ['get', [head, rest]]; } else { return ['maybeLocal', [name, ...rest]]; } } maybeGet(op) { let [name, rest] = op; if (name === 0) { return ['get', [0, rest]]; } if (isLocal(name, this.symbols)) { let head = this.symbols.get(name); return ['get', [head, rest]]; } else if (name[0] === '@') { let head = this.symbols.allocateNamed(name); return ['get', [head, rest]]; } else if (rest.length === 0) { return ['unknown', name]; } else { return ['maybeLocal', [name, ...rest]]; } } yield(op) { if (op === 0) { throw new Error('Cannot yield to this'); } return ['yield', this.symbols.allocateBlock(op)]; } debugger(_op) { return ['debugger', this.symbols.getEvalInfo()]; } hasBlock(op) { if (op === 0) { throw new Error('Cannot hasBlock this'); } return ['hasBlock', this.symbols.allocateBlock(op)]; } hasBlockParams(op) { if (op === 0) { throw new Error('Cannot hasBlockParams this'); } return ['hasBlockParams', this.symbols.allocateBlock(op)]; } partial(_op) { return ['partial', this.symbols.getEvalInfo()]; } text(_op) {} comment(_op) {} openComponent(_op) {} openElement(_op) {} openSplattedElement(_op) {} staticArg(_op) {} dynamicArg(_op) {} staticAttr(_op) {} trustingAttr(_op) {} dynamicAttr(_op) {} modifier(_op) {} append(_op) {} block(_op) {} literal(_op) {} helper(_op) {} unknown(_op) {} maybeLocal(_op) {} prepareArray(_op) {} prepareObject(_op) {} concat(_op) {} } function isLocal(name, symbols) { return symbols && symbols.has(name); } function isTrustedValue(value) { return value.escaped !== undefined && !value.escaped; } class TemplateCompiler { constructor() { this.templateId = 0; this.templateIds = []; this.opcodes = []; this.includeMeta = false; } static compile(ast, options) { let templateVisitor = new TemplateVisitor(); templateVisitor.visit(ast); let compiler = new TemplateCompiler(); let opcodes = compiler.process(templateVisitor.actions); let symbols = new SymbolAllocator(opcodes).process(); return JavaScriptCompiler.process(symbols, ast['symbols'], options); } process(actions) { actions.forEach(([name, ...args]) => { if (!this[name]) { throw new Error(`Unimplemented ${name} on TemplateCompiler`); } this[name](...args); }); return this.opcodes; } startProgram([program]) { this.opcode(['startProgram', program], program); } endProgram() { this.opcode(['endProgram', null], null); } startBlock([program]) { this.templateId++; this.opcode(['startBlock', program], program); } endBlock() { this.templateIds.push(this.templateId - 1); this.opcode(['endBlock', null], null); } text([action]) { this.opcode(['text', action.chars], action); } comment([action]) { this.opcode(['comment', action.value], action); } openElement([action]) { let attributes = action.attributes; let hasSplat; for (let i = 0; i < attributes.length; i++) { let attr = attributes[i]; if (attr.name === '...attributes') { hasSplat = attr; break; } } if (isDynamicComponent(action)) { let head, rest; [head, ...rest] = action.tag.split('.'); if (head === 'this') { head = 0; } this.opcode(['get', [head, rest]]); this.opcode(['openComponent', action], action); } else if (isComponent(action)) { this.opcode(['openComponent', action], action); } else if (hasSplat) { this.opcode(['openSplattedElement', action], action); } else { this.opcode(['openElement', action], action); } let typeAttr = null; let attrs = action.attributes; for (let i = 0; i < attrs.length; i++) { if (attrs[i].name === 'type') { typeAttr = attrs[i]; continue; } this.attribute([attrs[i]]); } if (typeAttr) { this.attribute([typeAttr]); } this.opcode(['flushElement', action], null); } closeElement([action]) { if (isDynamicComponent(action)) { this.opcode(['closeDynamicComponent', action], action); } else if (isComponent(action)) { this.opcode(['closeComponent', action], action); } else if (action.modifiers.length > 0) { for (let i = 0; i < action.modifiers.length; i++) { this.modifier([action.modifiers[i]]); } this.opcode(['closeElement', action], action); } else { this.opcode(['closeElement', action], action); } } attribute([action]) { let { name, value } = action; let namespace = getAttrNamespace(name); let isStatic = this.prepareAttributeValue(value); if (name.charAt(0) === '@') { // Arguments if (isStatic) { this.opcode(['staticArg', name], action); } else if (action.value.type === 'MustacheStatement') { this.opcode(['dynamicArg', name], action); } else { this.opcode(['dynamicArg', name], action); } } else { let isTrusting = isTrustedValue(value); if (isStatic && name === '...attributes') { this.opcode(['attrSplat', null], action); } else if (isStatic) { this.opcode(['staticAttr', [name, namespace]], action); } else if (isTrusting) { this.opcode(['trustingAttr', [name, namespace]], action); } else if (action.value.type === 'MustacheStatement') { this.opcode(['dynamicAttr', [name, null]], action); } else { this.opcode(['dynamicAttr', [name, namespace]], action); } } } modifier([action]) { assertIsSimplePath(action.path, action.loc, 'modifier'); let { path: { parts } } = action; this.prepareHelper(action); this.opcode(['modifier', parts[0]], action); } mustache([action]) { let { path } = action; if (isLiteral(path)) { this.mustacheExpression(action); this.opcode(['append', !action.escaped], action); } else if (isYield(path)) { let to = assertValidYield(action); this.yield(to, action); } else if (isPartial(path)) { let params = assertValidPartial(action); this.partial(params, action); } else if (isDebugger(path)) { assertValidDebuggerUsage(action); this.debugger('debugger', action); } else { this.mustacheExpression(action); this.opcode(['append', !action.escaped], action); } } block([action /*, index, count*/]) { this.prepareHelper(action); let templateId = this.templateIds.pop(); let inverseId = action.inverse === null ? null : this.templateIds.pop(); this.opcode(['block', [action.path.parts[0], templateId, inverseId]], action); } /// Internal actions, not found in the original processed actions arg([path]) { let { parts: [head, ...rest] } = path; this.opcode(['get', [`@${head}`, rest]], path); } mustacheExpression(expr) { let { path } = expr; if (isLiteral(path)) { this.opcode(['literal', path.value], expr); } else if (isBuiltInHelper(path)) { this.builtInHelper(expr); } else if (isArg(path)) { this.arg([path]); } else if (isHelperInvocation(expr)) { this.prepareHelper(expr); this.opcode(['helper', path.parts[0]], expr); } else if (path.this) { this.opcode(['get', [0, path.parts]], expr); } else { let [head, ...parts] = path.parts; this.opcode(['maybeGet', [head, parts]], expr); } // } else if (isLocal(path, this.symbols)) { // let [head, ...parts] = path.parts; // this.opcode(['get', [head, parts]], expr); // } else if (isSimplePath(path)) { // this.opcode(['unknown', path.parts[0]], expr); // } else { // this.opcode(['maybeLocal', path.parts], expr); // } } /// Internal Syntax yield(to, action) { this.prepareParams(action.params); this.opcode(['yield', to], action); } debugger(_name, action) { this.opcode(['debugger', null], action); } hasBlock(name, action) { this.opcode(['hasBlock', name], action); } hasBlockParams(name, action) { this.opcode(['hasBlockParams', name], action); } partial(_params, action) { this.prepareParams(action.params); this.opcode(['partial', null], action); } builtInHelper(expr) { let { path } = expr; if (isHasBlock(path)) { let name = assertValidHasBlockUsage(expr.path.original, expr); this.hasBlock(name, expr); } else if (isHasBlockParams(path)) { let name = assertValidHasBlockUsage(expr.path.original, expr); this.hasBlockParams(name, expr); } } /// Expressions, invoked recursively from prepareParams and prepareHash SubExpression(expr) { if (isBuiltInHelper(expr.path)) { this.builtInHelper(expr); } else { this.prepareHelper(expr); this.opcode(['helper', expr.path.parts[0]], expr); } } PathExpression(expr) { if (expr.data) { this.arg([expr]); } else { let [head, ...rest] = expr.parts; if (expr.this) { this.opcode(['get', [0, expr.parts]], expr); } else { this.opcode(['get', [head, rest]], expr); } } } StringLiteral(action) { this.opcode(['literal', action.value], action); } BooleanLiteral(action) { this.opcode(['literal', action.value], action); } NumberLiteral(action) { this.opcode(['literal', action.value], action); } NullLiteral(action) { this.opcode(['literal', action.value], action); } UndefinedLiteral(action) { this.opcode(['literal', action.value], action); } /// Utilities opcode(opcode, action = null) { // TODO: This doesn't really work if (this.includeMeta && action) { opcode.push(this.meta(action)); } this.opcodes.push(opcode); } prepareHelper(expr) { assertIsSimplePath(expr.path, expr.loc, 'helper'); let { params, hash } = expr; this.prepareHash(hash); this.prepareParams(params); } prepareParams(params) { if (!params.length) { this.opcode(['literal', null], null); return; } for (let i = params.length - 1; i >= 0; i--) { let param = params[i]; this[param.type](param); } this.opcode(['prepareArray', params.length], null); } prepareHash(hash) { let pairs = hash.pairs; if (!pairs.length) { this.opcode(['literal', null], null); return; } for (let i = pairs.length - 1; i >= 0; i--) { let { key, value } = pairs[i]; this[value.type](value); this.opcode(['literal', key], null); } this.opcode(['prepareObject', pairs.length], null); } prepareAttributeValue(value) { // returns the static value if the value is static switch (value.type) { case 'TextNode': this.opcode(['literal', value.chars], value); return true; case 'MustacheStatement': this.attributeMustache([value]); return false; case 'ConcatStatement': this.prepareConcatParts(value.parts); this.opcode(['concat', null], value); return false; } } prepareConcatParts(parts) { for (let i = parts.length - 1; i >= 0; i--) { let part = parts[i]; if (part.type === 'MustacheStatement') { this.attributeMustache([part]); } else if (part.type === 'TextNode') { this.opcode(['literal', part.chars], null); } } this.opcode(['prepareArray', parts.length], null); } attributeMustache([action]) { this.mustacheExpression(action); } meta(node) { let loc = node.loc; if (!loc) { return []; } let { source, start, end } = loc; return ['loc', [source || null, [start.line, start.column], [end.line, end.column]]]; } } function isHelperInvocation(mustache) { return mustache.params && mustache.params.length > 0 || mustache.hash && mustache.hash.pairs.length > 0; } function isSimplePath({ parts }) { return parts.length === 1; } function isYield(path) { return path.original === 'yield'; } function isPartial(path) { return path.original === 'partial'; } function isDebugger(path) { return path.original === 'debugger'; } function isHasBlock(path) { return path.original === 'has-block'; } function isHasBlockParams(path) { return path.original === 'has-block-params'; } function isBuiltInHelper(path) { return isHasBlock(path) || isHasBlockParams(path); } function isArg(path) { return !!path['data']; } function isDynamicComponent(element) { let open = element.tag.charAt(0); let [maybeLocal] = element.tag.split('.'); let isNamedArgument = open === '@'; let isLocal = element['symbols'].has(maybeLocal); let isThisPath = element.tag.indexOf('this.') === 0; return isLocal || isNamedArgument || isThisPath; } function isComponent(element) { let open = element.tag.charAt(0); let isPath = element.tag.indexOf('.') > -1; let isUpperCase = open === open.toUpperCase() && open !== open.toLowerCase(); return isUpperCase && !isPath || isDynamicComponent(element); } function assertIsSimplePath(path, loc, context) { if (!isSimplePath(path)) { throw new SyntaxError(`\`${path.original}\` is not a valid name for a ${context} on line ${loc.start.line}.`, path.loc); } } function assertValidYield(statement) { let { pairs } = statement.hash; if (pairs.length === 1 && pairs[0].key !== 'to' || pairs.length > 1) { throw new SyntaxError(`yield only takes a single named argument: 'to'`, statement.loc); } else if (pairs.length === 1 && pairs[0].value.type !== 'StringLiteral') { throw new SyntaxError(`you can only yield to a literal value`, statement.loc); } else if (pairs.length === 0) { return 'default'; } else { return pairs[0].value.value; } } function assertValidPartial(statement) { let { params, hash, escaped, loc } = statement; if (params && params.length !== 1) { throw new SyntaxError(`Partial found with no arguments. You must specify a template name. (on line ${loc.start.line})`, statement.loc); } else if (hash && hash.pairs.length > 0) { throw new SyntaxError(`partial does not take any named arguments (on line ${loc.start.line})`, statement.loc); } else if (!escaped) { throw new SyntaxError(`{{{partial ...}}} is not supported, please use {{partial ...}} instead (on line ${loc.start.line})`, statement.loc); } return params; } function assertValidHasBlockUsage(type, call) { let { params, hash, loc } = call; if (hash && hash.pairs.length > 0) { throw new SyntaxError(`${type} does not take any named arguments`, call.loc); } if (params.length === 0) { return 'default'; } else if (params.length === 1) { let param = params[0]; if (param.type === 'StringLiteral') { return param.value; } else { throw new SyntaxError(`you can only yield to a literal value (on line ${loc.start.line})`, call.loc); } } else { throw new SyntaxError(`${type} only takes a single positional argument (on line ${loc.start.line})`, call.loc); } } function assertValidDebuggerUsage(statement) { let { params, hash } = statement; if (hash && hash.pairs.length > 0) { throw new SyntaxError(`debugger does not take any named arguments`, statement.loc); } if (params.length === 0) { return 'default'; } else { throw new SyntaxError(`debugger does not take any positional arguments`, statement.loc); } } const defaultId = (() => { if (typeof require === 'function') { try { /* tslint:disable:no-require-imports */ const crypto = require('crypto'); /* tslint:enable:no-require-imports */ let idFn = src => { let hash = crypto.createHash('sha1'); hash.update(src, 'utf8'); // trim to 6 bytes of data (2^48 - 1) return hash.digest('base64').substring(0, 8); }; idFn('test'); return idFn; } catch (e) {} } return function idFn() { return null; }; })(); const defaultOptions = { id: defaultId, meta: {} }; function precompile(string, options = defaultOptions) { let ast = preprocess(string, options); let { meta } = options; let { block } = TemplateCompiler.compile(ast, options); let idFn = options.id || defaultId; let blockJSON = JSON.stringify(block.toJSON()); let templateJSONObject = { id: idFn(JSON.stringify(meta) + blockJSON), block: blockJSON, meta: meta }; // JSON is javascript return JSON.stringify(templateJSONObject); } export { defaultId, precompile, TemplateCompiler, TemplateVisitor };