UNPKG

ember-legacy-class-transform

Version:
392 lines 14.2 kB
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); } }