UNPKG

ember-legacy-class-transform

Version:
298 lines (291 loc) 9.72 kB
import { dict, unreachable, expect } from '@glimmer/util'; export class SymbolTable { static top() { return new ProgramSymbolTable(); } child(locals) { let symbols = locals.map(name => this.allocate(name)); return new BlockSymbolTable(this, locals, symbols); } } export 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++; } } export 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 = this.parent.getLocalsMap(); this.symbols.forEach(symbol => dict[symbol] = this.get(symbol)); return dict; } 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}}<div>baz</div> * * 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, * * <div>{{#if}}foo{{else}}bar<b></b>{{/if}}</div> * * 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; } } export default 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 expect(this.getCurrentFrame(), "Expected a current frame"); } 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; }