ember-legacy-class-transform
Version:
The default blueprint for ember-cli addons.
392 lines • 14.2 kB
JavaScript
import TemplateVisitor from "./template-visitor";
import JavaScriptCompiler from "./javascript-compiler";
import { Stack, getAttrNamespace } from "@glimmer/util";
import { assert, expect } from "@glimmer/util";
import { isLiteral, SyntaxError } from '@glimmer/syntax';
function isTrustedValue(value) {
return value.escaped !== undefined && !value.escaped;
}
export default class TemplateCompiler {
constructor(options) {
this.templateId = 0;
this.templateIds = [];
this.symbolStack = new Stack();
this.opcodes = [];
this.includeMeta = false;
this.options = options || {};
}
static compile(options, ast) {
let templateVisitor = new TemplateVisitor();
templateVisitor.visit(ast);
let compiler = new TemplateCompiler(options);
let opcodes = compiler.process(templateVisitor.actions);
return JavaScriptCompiler.process(opcodes, ast['symbols'], options.meta);
}
get symbols() {
return expect(this.symbolStack.current, 'Expected a symbol table on the stack');
}
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.symbolStack.push(program[0]['symbols']);
this.opcode('startProgram', program, program);
}
endProgram() {
this.symbolStack.pop();
this.opcode('endProgram', null);
}
startBlock(program) {
this.symbolStack.push(program[0]['symbols']);
this.templateId++;
this.opcode('startBlock', program, program);
}
endBlock() {
this.symbolStack.pop();
this.templateIds.push(this.templateId - 1);
this.opcode('endBlock', null);
}
text([action]) {
this.opcode('text', action, action.chars);
}
comment([action]) {
this.opcode('comment', action, action.value);
}
openElement([action]) {
this.opcode('openElement', action, action);
for (let i = 0; i < action.attributes.length; i++) {
this.attribute([action.attributes[i]]);
}
for (let i = 0; i < action.modifiers.length; i++) {
this.modifier([action.modifiers[i]]);
}
this.opcode('flushElement', null);
this.symbolStack.push(action['symbols']);
}
closeElement([action]) {
this.symbolStack.pop();
this.opcode('closeElement', null, 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', action, name);
} else if (action.value.type === 'MustacheStatement') {
this.opcode('dynamicArg', action, name);
} else {
this.opcode('dynamicArg', action, name);
}
} else {
let isTrusting = isTrustedValue(value);
if (isStatic) {
this.opcode('staticAttr', action, name, namespace);
} else if (isTrusting) {
this.opcode('trustingAttr', action, name, namespace);
} else if (action.value.type === 'MustacheStatement') {
this.opcode('dynamicAttr', action, name);
} else {
this.opcode('dynamicAttr', action, name, namespace);
}
}
}
modifier([action]) {
assertIsSimplePath(action.path, action.loc, 'modifier');
let { path: { parts } } = action;
this.prepareHelper(action);
this.opcode('modifier', action, parts[0]);
}
mustache([action]) {
let { path } = action;
if (isLiteral(path)) {
this.mustacheExpression(action);
this.opcode('append', action, !action.escaped);
} 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, !action.escaped);
}
}
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, action.path.parts[0], templateId, inverseId);
}
/// Internal actions, not found in the original processed actions
arg([path]) {
let { parts: [head, ...rest] } = path;
let symbol = this.symbols.allocateNamed(head);
this.opcode('get', path, symbol, rest);
}
mustacheExpression(expr) {
let { path } = expr;
if (isLiteral(path)) {
this.opcode('literal', expr, path.value);
} else if (isBuiltInHelper(path)) {
this.builtInHelper(expr);
} else if (isArg(path)) {
this.arg([path]);
} else if (isHelperInvocation(expr)) {
this.prepareHelper(expr);
this.opcode('helper', expr, path.parts[0]);
} else if (path.this) {
this.opcode('get', expr, 0, path.parts);
} else if (isLocal(path, this.symbols)) {
let [head, ...parts] = path.parts;
this.opcode('get', expr, this.symbols.get(head), parts);
} else if (isSimplePath(path)) {
this.opcode('unknown', expr, path.parts[0]);
} else {
this.opcode('maybeLocal', expr, path.parts);
}
}
/// Internal Syntax
yield(to, action) {
this.prepareParams(action.params);
this.opcode('yield', action, this.symbols.allocateBlock(to));
}
debugger(_name, action) {
this.opcode('debugger', action, this.symbols.getEvalInfo());
}
hasBlock(name, action) {
this.opcode('hasBlock', action, this.symbols.allocateBlock(name));
}
hasBlockParams(name, action) {
this.opcode('hasBlockParams', action, this.symbols.allocateBlock(name));
}
partial(_params, action) {
this.prepareParams(action.params);
this.opcode('partial', action, this.symbols.getEvalInfo());
}
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, expr.path.parts[0]);
}
}
PathExpression(expr) {
if (expr.data) {
this.arg([expr]);
} else {
let { symbols } = this;
let [head] = expr.parts;
if (expr.this) {
this.opcode('get', expr, 0, expr.parts);
} else if (symbols.has(head)) {
this.opcode('get', expr, symbols.get(head), expr.parts.slice(1));
} else {
this.opcode('get', expr, 0, expr.parts);
}
}
}
StringLiteral(action) {
this.opcode('literal', null, action.value);
}
BooleanLiteral(action) {
this.opcode('literal', null, action.value);
}
NumberLiteral(action) {
this.opcode('literal', null, action.value);
}
NullLiteral(action) {
this.opcode('literal', null, action.value);
}
UndefinedLiteral(action) {
this.opcode('literal', null, action.value);
}
/// Utilities
opcode(name, action, ...args) {
let opcode = [name, ...args];
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];
assert(this[param.type], `Unimplemented ${param.type} on TemplateCompiler`);
this[param.type](param);
}
this.opcode('prepareArray', null, params.length);
}
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];
assert(this[value.type], `Unimplemented ${value.type} on TemplateCompiler`);
this[value.type](value);
this.opcode('literal', null, key);
}
this.opcode('prepareObject', null, pairs.length);
}
prepareAttributeValue(value) {
// returns the static value if the value is static
switch (value.type) {
case 'TextNode':
this.opcode('literal', value, value.chars);
return true;
case 'MustacheStatement':
this.attributeMustache([value]);
return false;
case 'ConcatStatement':
this.prepareConcatParts(value.parts);
this.opcode('concat', 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', null, part.chars);
}
}
this.opcode('prepareArray', null, parts.length);
}
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 isLocal({ parts }, symbols) {
return symbols && symbols.has(parts[0]);
}
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 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);
}
}