ember-material-icons
Version:
Google Material icons for your ember-cli app
470 lines (380 loc) • 12.3 kB
text/typescript
import TemplateVisitor, { SymbolTable } from "./template-visitor";
import JavaScriptCompiler, { Template } from "./javascript-compiler";
import { getAttrNamespace } from "@glimmer/util";
import { assert } from "@glimmer/util";
import { TemplateMeta } from "@glimmer/wire-format";
export interface CompileOptions<T extends TemplateMeta> {
meta?: T;
}
function isTrustedValue(value) {
return value.escaped !== undefined && !value.escaped;
}
export default class TemplateCompiler<T extends TemplateMeta> {
static compile<T>(options: CompileOptions<T>, ast): Template<T> {
let templateVisitor = new TemplateVisitor();
templateVisitor.visit(ast);
let compiler = new TemplateCompiler(options);
let opcodes = compiler.process(templateVisitor.actions);
return JavaScriptCompiler.process<T>(opcodes, options.meta);
}
private options: CompileOptions<T>;
private templateId = 0;
private templateIds: number[] = [];
private symbols: SymbolTable = null;
private opcodes: any[] = [];
private includeMeta = false;
constructor(options: CompileOptions<T>) {
this.options = options || {};
}
process(actions): any[] {
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);
}
startBlock(program) {
this.symbols = program[0].symbols;
this.templateId++;
this.opcode('startBlock', program, program);
}
endBlock() {
this.symbols = null;
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.tag, action.blockParams);
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);
}
closeElement([action]) {
this.opcode('closeElement', null, action.tag);
}
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, 'modifier');
let { path: { parts } } = action;
this.prepareHelper(action);
this.opcode('modifier', action, parts);
}
mustache([action]) {
if (isYield(action)) {
let to = assertValidYield(action);
this.yield(to, action);
} else if (isPartial(action)) {
let params = assertValidPartial(action);
this.partial(params, action);
}else if (isDebugger(action)) {
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, templateId, inverseId);
}
/// Internal actions, not found in the original processed actions
arg([path]) {
let { parts } = path;
this.opcode('arg', path, parts);
}
mustacheExpression(expr) {
if (isBuiltInHelper(expr)) {
this.builtInHelper(expr);
} else if (isLiteral(expr)) {
this.opcode('literal', expr, expr.path.value);
} else if (isArg(expr)) {
this.arg([expr.path]);
} else if (isHelperInvocation(expr)) {
this.prepareHelper(expr);
this.opcode('helper', expr, expr.path.parts);
} else if (!isSimplePath(expr) || isSelfGet(expr) || isLocalVariable(expr, this.symbols)) {
this.opcode('get', expr, expr.path.parts);
} else {
this.opcode('unknown', expr, expr.path.parts);
}
}
/// Internal Syntax
yield(to: string, action) {
this.prepareParams(action.params);
this.opcode('yield', action, to);
}
debugger(name, action) {
this.opcode('debugger', null);
}
hasBlock(name: string, action) {
this.opcode('hasBlock', action, name);
}
hasBlockParams(name: string, action) {
this.opcode('hasBlockParams', action, name);
}
partial(params, action) {
this.prepareParams(action.params);
this.opcode('partial', action);
}
builtInHelper(expr) {
if (isHasBlock(expr)) {
let name = assertValidHasBlockUsage(expr.path.original, expr);
this.hasBlock(name, expr);
} else if (isHasBlockParams(expr)) {
let name = assertValidHasBlockUsage(expr.path.original, expr);
this.hasBlockParams(name, expr);
}
}
/// Expressions, invoked recursively from prepareParams and prepareHash
SubExpression(expr) {
if (isBuiltInHelper(expr)) {
this.builtInHelper(expr);
} else {
this.prepareHelper(expr);
this.opcode('helper', expr, expr.path.parts);
}
}
PathExpression(expr) {
if (expr.data) {
this.arg([expr]);
} else {
this.opcode('get', expr, 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, 'helper');
let { params, hash } = expr;
this.prepareHash(hash);
this.prepareParams(params);
}
preparePath(path) {
this.opcode('literal', path, path.parts);
}
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(mustache) {
let { parts } = mustache.path;
return parts.length === 1;
}
function isSelfGet(mustache) {
let { parts } = mustache.path;
return parts[0] === null;
}
function isLocalVariable(mustache, symbols) {
let { parts } = mustache.path;
return parts.length === 1 && symbols && symbols.hasLocalVariable(parts[0]);
}
function isYield({ path }) {
return path.original === 'yield';
}
function isPartial({ path }) {
return path.original === 'partial';
}
function isDebugger({ path }) {
return path.original === 'debugger';
}
function isArg({ path }) {
return path.data;
}
function isLiteral({ path }) {
return path.type === 'StringLiteral'
|| path.type === 'BooleanLiteral'
|| path.type === 'NumberLiteral'
|| path.type === 'NullLiteral'
|| path.type === 'UndefinedLiteral';
}
function isHasBlock({ path }) {
return path.original === 'has-block';
}
function assertIsSimplePath(expr, context) {
if (!isSimplePath(expr)) {
let { path: { original }, loc: { start: { line } } } = expr;
throw new Error(`\`${original}\` is not a valid name for a ${context} on line ${line}.`);
}
}
function isHasBlockParams({ path }) {
return path.original === 'has-block-params';
}
function isBuiltInHelper(expr) {
return isHasBlock(expr)
|| isHasBlockParams(expr);
}
function assertValidYield({ hash }): string {
let pairs = hash.pairs;
if ((pairs.length === 1 && pairs[0].key !== 'to') || pairs.length > 1) {
throw new Error(`yield only takes a single named argument: 'to'`);
} else if (pairs.length === 1 && pairs[0].value.type !== 'StringLiteral') {
throw new Error(`you can only yield to a literal value`);
} else if (pairs.length === 0) {
return 'default';
} else {
return pairs[0].value.value;
}
}
function assertValidPartial({ params, hash, escaped, loc }) /* : expr */ {
if (params && params.length !== 1) {
throw new Error(`Partial found with no arguments. You must specify a template name. (on line ${loc.start.line})`);
} else if (hash && hash.pairs.length > 0) {
throw new Error(`partial does not take any named arguments (on line ${loc.start.line})`);
} else if (!escaped) {
throw new Error(`{{{partial ...}}} is not supported, please use {{partial ...}} instead (on line ${loc.start.line})`);
}
return params;
}
function assertValidHasBlockUsage(type, { params, hash, loc }): string {
if (hash && hash.pairs.length > 0) {
throw new Error(`${type} does not take any named arguments`);
}
if (params.length === 0) {
return 'default';
} else if (params.length === 1) {
if (params[0].type === 'StringLiteral') {
return params[0].value;
} else {
throw new Error(`you can only yield to a literal value (on line ${loc.start.line})`);
}
} else {
throw new Error(`${type} only takes a single positional argument (on line ${loc.start.line})`);
}
}
function assertValidDebuggerUsage({ params, hash }) {
if (hash && hash.pairs.length > 0) {
throw new Error(`debugger does not take any named arguments`);
}
if (params.length === 0) {
return 'default';
} else {
throw new Error(`debugger does not take any positional arguments`);
}
}