import { dict, unreachable, Stack, DictSet } from '@glimmer/util';
import { Ops, isModifier, 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 (isModifier(statement)) {
throw new Error('Compile Error: Element modifiers are not allowed in components');
} else 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) {
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]);
}
for (let i = 0; i < action.modifiers.length; i++) {
this.modifier([action.modifiers[i]]);
}
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 {
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 };