ember-material-icons
Version:
Google Material icons for your ember-cli app
258 lines (213 loc) • 7.52 kB
text/typescript
let push = Array.prototype.push;
class Frame {
public parentNode: Object = null;
public children: Object = null;
public childIndex: number = null;
public childCount: number = null;
public childTemplateCount = 0;
public mustacheCount = 0;
public actions: any[] = [];
public blankChildTextNodes: number[] = null;
public symbols: SymbolTable = null;
}
export class SymbolTable {
constructor(
private symbols: string[],
private parent: SymbolTable = null
) {}
hasLocalVariable(name: string): boolean {
let { symbols, parent } = this;
return symbols.indexOf(name) >= 0 || (parent && parent.hasLocalVariable(name));
}
}
/**
* 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
*/
function TemplateVisitor() {
this.frameStack = [];
this.actions = [];
this.programDepth = -1;
}
// Traversal methods
TemplateVisitor.prototype.visit = function(node) {
this[node.type](node);
};
TemplateVisitor.prototype.Program = function(program) {
this.programDepth++;
let parentFrame = this.getCurrentFrame();
let programFrame = this.pushFrame();
if (parentFrame) {
program.symbols = new SymbolTable(program.blockParams, parentFrame.symbols);
} else {
program.symbols = new SymbolTable(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++; }
push.apply(this.actions, programFrame.actions.reverse());
};
TemplateVisitor.prototype.ElementNode = function(element) {
let parentFrame = this.getCurrentFrame();
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 = parentFrame.symbols;
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]);
}
elementFrame.actions.push(['openElement', actionArgs.concat([
elementFrame.mustacheCount, elementFrame.blankChildTextNodes.reverse() ])]);
this.popFrame();
// Propagate the element's frame state to the parent frame
if (elementFrame.mustacheCount > 0) { parentFrame.mustacheCount++; }
parentFrame.childTemplateCount += elementFrame.childTemplateCount;
push.apply(parentFrame.actions, elementFrame.actions);
};
TemplateVisitor.prototype.AttrNode = function(attr) {
if (attr.value.type !== 'TextNode') {
this.getCurrentFrame().mustacheCount++;
}
};
TemplateVisitor.prototype.TextNode = function(text) {
let frame = this.getCurrentFrame();
if (text.chars === '') {
frame.blankChildTextNodes.push(domIndexOf(frame.children, text));
}
frame.actions.push(['text', [text, frame.childIndex, frame.childCount]]);
};
TemplateVisitor.prototype.BlockStatement = function(node) {
let frame = this.getCurrentFrame();
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); }
};
TemplateVisitor.prototype.PartialStatement = function(node) {
let frame = this.getCurrentFrame();
frame.mustacheCount++;
frame.actions.push(['mustache', [node, frame.childIndex, frame.childCount]]);
};
TemplateVisitor.prototype.CommentStatement = function(text) {
let frame = this.getCurrentFrame();
frame.actions.push(['comment', [text, frame.childIndex, frame.childCount]]);
};
TemplateVisitor.prototype.MustacheCommentStatement = function() {
// Intentional empty: Handlebars comments should not affect output.
};
TemplateVisitor.prototype.MustacheStatement = function(mustache) {
let frame = this.getCurrentFrame();
frame.mustacheCount++;
frame.actions.push(['mustache', [mustache, frame.childIndex, frame.childCount]]);
};
// Frame helpers
TemplateVisitor.prototype.getCurrentFrame = function() {
return this.frameStack[this.frameStack.length - 1];
};
TemplateVisitor.prototype.pushFrame = function() {
let frame = new Frame();
this.frameStack.push(frame);
return frame;
};
TemplateVisitor.prototype.popFrame = function() {
return this.frameStack.pop();
};
export default TemplateVisitor;
// 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;
}