UNPKG

@glimmer/compiler

Version:
1,695 lines (1,672 loc) 484 kB
'use strict'; /// Builder /// const BUILDER_LITERAL = 0; const BUILDER_COMMENT = 1; const BUILDER_APPEND = 2; const BUILDER_MODIFIER = 3; const BUILDER_DYNAMIC_COMPONENT = 4; const BUILDER_GET = 5; const BUILDER_CONCAT = 6; const BUILDER_HAS_BLOCK = 7; const BUILDER_HAS_BLOCK_PARAMS = 8; const BLOCK_HEAD = 'Block'; const CALL_HEAD = 'Call'; const ELEMENT_HEAD = 'Element'; const APPEND_PATH_HEAD = 'AppendPath'; const APPEND_EXPR_HEAD = 'AppendExpr'; const LITERAL_HEAD = 'Literal'; const MODIFIER_HEAD = 'Modifier'; const DYNAMIC_COMPONENT_HEAD = 'DynamicComponent'; const COMMENT_HEAD = 'Comment'; const SPLAT_HEAD = 'Splat'; const KEYWORD_HEAD = 'Keyword'; const LOCAL_VAR = 'Local'; const FREE_VAR = 'Free'; const ARG_VAR = 'Arg'; const BLOCK_VAR = 'Block'; const THIS_VAR = 'This'; const LITERAL_EXPR = 'Literal'; const CALL_EXPR = 'Call'; const GET_PATH_EXPR = 'GetPath'; const GET_VAR_EXPR = 'GetVar'; const CONCAT_EXPR = 'Concat'; const HAS_BLOCK_EXPR = 'HasBlock'; const HAS_BLOCK_PARAMS_EXPR = 'HasBlockParams'; const CURRIED_COMPONENT = 0; const CURRIED_HELPER = 1; const CURRIED_MODIFIER = 2; const NS_XLINK = 'http://www.w3.org/1999/xlink'; const NS_XML = 'http://www.w3.org/XML/1998/namespace'; const NS_XMLNS = 'http://www.w3.org/2000/xmlns/'; // import Logger from './logger'; function assert(test, msg) { } function setLocalDebugType(type, ...brand) { } function unwrap(val) { return val; } function expect(val, message) { return val; } function exhausted(value) { } function isPresentArray(list) { return list ? list.length > 0 : false; } function asPresentArray(list, message = `unexpected empty list`) { return list; } function getLast(list) { return list.length === 0 ? undefined : list[list.length - 1]; } function getFirst(list) { return list.length === 0 ? undefined : list[0]; } function mapPresentArray(list, mapper) { if (list === null) { return null; } let out = []; for (let item of list){ out.push(mapper(item)); } return out; } function dict() { return Object.create(null); } const assign = Object.assign; function values(obj) { return Object.values(obj); } /** * This constant exists to make it easier to differentiate normal logs from * errant console.logs. LOGGER can be used outside of LOCAL_TRACE_LOGGING checks, * and is meant to be used in the rare situation where a console.* call is * actually appropriate. */ const LOGGER = console; function assertNever(value, desc = 'unexpected unreachable branch') { LOGGER.log('unreachable', value); LOGGER.log(`${desc} :: ${JSON.stringify(value)} (${value})`); throw new Error(`code reached unreachable`); } const opcodes = { Append: 1, TrustingAppend: 2, Comment: 3, Modifier: 4, Block: 6, Component: 8, OpenElement: 10, OpenElementWithSplat: 11, FlushElement: 12, CloseElement: 13, StaticAttr: 14, DynamicAttr: 15, ComponentAttr: 16, AttrSplat: 17, Yield: 18, DynamicArg: 20, StaticArg: 21, TrustingDynamicAttr: 22, TrustingComponentAttr: 23, StaticComponentAttr: 24, Debugger: 26, Undefined: 27, Call: 28, Concat: 29, GetSymbol: 30, GetLexicalSymbol: 32, GetStrictKeyword: 31, GetFreeAsComponentOrHelperHead: 35, GetFreeAsHelperHead: 37, GetFreeAsModifierHead: 38, GetFreeAsComponentHead: 39, InElement: 40, If: 41, Each: 42, Let: 44, WithDynamicVars: 45, InvokeComponent: 46, HasBlock: 48, HasBlockParams: 49, Curry: 50, Not: 51, IfInline: 52, GetDynamicVar: 53, Log: 54 }; const resolution = { Strict: 0, ResolveAsComponentOrHelperHead: 1, ResolveAsHelperHead: 5, ResolveAsModifierHead: 6, ResolveAsComponentHead: 7 }; const WellKnownAttrNames = { class: 0, id: 1, value: 2, name: 3, type: 4, style: 5, href: 6 }; const WellKnownTagNames = { div: 0, span: 1, p: 2, a: 3 }; function normalizeStatement(statement) { if (Array.isArray(statement)) { if (statementIsExpression(statement)) { return normalizeAppendExpression(statement); } else if (isSugaryArrayStatement(statement)) { return normalizeSugaryArrayStatement(statement); } else { return normalizeVerboseStatement(statement); } } else if (typeof statement === 'string') { return normalizeAppendHead(normalizeDottedPath(statement), false); } else { assertNever(statement); } } function normalizeAppendHead(head, trusted) { if (head.type === GET_PATH_EXPR) { return { kind: APPEND_PATH_HEAD, path: head, trusted }; } else { return { kind: APPEND_EXPR_HEAD, expr: head, trusted }; } } function isSugaryArrayStatement(statement) { if (Array.isArray(statement) && typeof statement[0] === 'string') { switch(statement[0][0]){ case '(': case '#': case '<': case '!': return true; default: return false; } } return false; } function normalizeSugaryArrayStatement(statement) { const name = statement[0]; switch(name[0]){ case '(': { let params = null; let hash = null; if (statement.length === 3) { params = normalizeParams(statement[1]); hash = normalizeHash(statement[2]); } else if (statement.length === 2) { if (Array.isArray(statement[1])) { params = normalizeParams(statement[1]); } else { hash = normalizeHash(statement[1]); } } return { kind: CALL_HEAD, head: normalizeCallHead(name), params, hash, trusted: false }; } case '#': { const { head: path, params, hash, blocks, blockParams } = normalizeBuilderBlockStatement(statement); return { kind: BLOCK_HEAD, head: path, params, hash, blocks, blockParams }; } case '!': { const name = statement[0].slice(1); const { params, hash, blocks, blockParams } = normalizeBuilderBlockStatement(statement); return { kind: KEYWORD_HEAD, name, params, hash, blocks, blockParams }; } case '<': { let attrs = dict(); let block = []; if (statement.length === 3) { attrs = normalizeAttrs(statement[1]); block = normalizeBlock(statement[2]); } else if (statement.length === 2) { if (Array.isArray(statement[1])) { block = normalizeBlock(statement[1]); } else { attrs = normalizeAttrs(statement[1]); } } return { kind: ELEMENT_HEAD, name: expect(extractElement(name)), attrs, block }; } default: throw new Error(`Unreachable ${JSON.stringify(statement)} in normalizeSugaryArrayStatement`); } } function normalizeVerboseStatement(statement) { switch(statement[0]){ case BUILDER_LITERAL: { return { kind: LITERAL_HEAD, value: statement[1] }; } case BUILDER_APPEND: { return normalizeAppendExpression(statement[1], statement[2]); } case BUILDER_MODIFIER: { return { kind: MODIFIER_HEAD, params: normalizeParams(statement[1]), hash: normalizeHash(statement[2]) }; } case BUILDER_DYNAMIC_COMPONENT: { return { kind: DYNAMIC_COMPONENT_HEAD, expr: normalizeExpression(statement[1]), hash: normalizeHash(statement[2]), block: normalizeBlock(statement[3]) }; } case BUILDER_COMMENT: { return { kind: COMMENT_HEAD, value: statement[1] }; } } } function extractBlockHead(name) { const result = /^(#|!)(.*)$/u.exec(name); if (result === null) { throw new Error(`Unexpected missing # in block head`); } return normalizeDottedPath(result[2]); } function normalizeCallHead(name) { const result = /^\((.*)\)$/u.exec(name); if (result === null) { throw new Error(`Unexpected missing () in call head`); } return normalizeDottedPath(result[1]); } function normalizePath(head, tail = []) { const pathHead = normalizePathHead(head); if (isPresentArray(tail)) { return { type: GET_PATH_EXPR, path: { head: pathHead, tail } }; } else { return { type: GET_VAR_EXPR, variable: pathHead }; } } function normalizeDottedPath(whole) { const { kind, name: rest } = normalizePathHead(whole); const [name, ...tail] = rest.split('.'); const variable = { kind, name, mode: 'loose' }; if (isPresentArray(tail)) { return { type: GET_PATH_EXPR, path: { head: variable, tail } }; } else { return { type: GET_VAR_EXPR, variable }; } } function normalizePathHead(whole) { let kind; let name; if (/^this(?:\.|$)/u.test(whole)) { return { kind: THIS_VAR, name: whole, mode: 'loose' }; } switch(whole[0]){ case '^': kind = FREE_VAR; name = whole.slice(1); break; case '@': kind = ARG_VAR; name = whole.slice(1); break; case '&': kind = BLOCK_VAR; name = whole.slice(1); break; default: kind = LOCAL_VAR; name = whole; } return { kind, name, mode: 'loose' }; } function normalizeBuilderBlockStatement(statement) { const head = statement[0]; let blocks = dict(); let params = null; let hash = null; let blockParams = null; if (statement.length === 2) { blocks = normalizeBlocks(statement[1]); } else if (statement.length === 3) { if (Array.isArray(statement[1])) { params = normalizeParams(statement[1]); } else { ({ hash, blockParams } = normalizeBlockHash(statement[1])); } blocks = normalizeBlocks(statement[2]); } else { params = normalizeParams(statement[1]); ({ hash, blockParams } = normalizeBlockHash(statement[2])); blocks = normalizeBlocks(statement[3]); } return { head: extractBlockHead(head), params, hash, blockParams, blocks }; } function normalizeBlockHash(hash) { if (hash === null) { return { hash: null, blockParams: null }; } let out = null; let blockParams = null; entries(hash, (key, value)=>{ if (key === 'as') { blockParams = Array.isArray(value) ? value : [ value ]; } else { out = out || dict(); out[key] = normalizeExpression(value); } }); return { hash: out, blockParams }; } function entries(dict, callback) { Object.keys(dict).forEach((key)=>{ const value = dict[key]; callback(key, value); }); } function normalizeBlocks(value) { if (Array.isArray(value)) { return { default: normalizeBlock(value) }; } else { return mapObject(value, normalizeBlock); } } function normalizeBlock(block) { return block.map((s)=>normalizeStatement(s)); } function normalizeAttrs(attrs) { return mapObject(attrs, (a)=>normalizeAttr(a).expr); } function normalizeAttr(attr) { if (attr === 'splat') { return { expr: SPLAT_HEAD, trusted: false }; } else { const expr = normalizeExpression(attr); return { expr, trusted: false }; } } function mapObject(object, mapper) { const out = dict(); Object.keys(object).forEach((k)=>{ out[k] = mapper(object[k], k); }); return out; } function extractElement(input) { const match = /^<([\d\-a-z][\d\-A-Za-z]*)>$/u.exec(input); return match?.[1] ?? null; } function normalizeAppendExpression(expression, forceTrusted = false) { if (expression === null || expression === undefined) { return { expr: { type: LITERAL_EXPR, value: expression }, kind: APPEND_EXPR_HEAD, trusted: false }; } else if (Array.isArray(expression)) { switch(expression[0]){ case BUILDER_LITERAL: return { expr: { type: LITERAL_EXPR, value: expression[1] }, kind: APPEND_EXPR_HEAD, trusted: false }; case BUILDER_GET: { return normalizeAppendHead(normalizePath(expression[1], expression[2]), forceTrusted); } case BUILDER_CONCAT: { const expr = { type: CONCAT_EXPR, params: normalizeParams(expression.slice(1)) }; return { expr, kind: APPEND_EXPR_HEAD, trusted: forceTrusted }; } case BUILDER_HAS_BLOCK: return { expr: { type: HAS_BLOCK_EXPR, name: expression[1] }, kind: APPEND_EXPR_HEAD, trusted: forceTrusted }; case BUILDER_HAS_BLOCK_PARAMS: return { expr: { type: HAS_BLOCK_PARAMS_EXPR, name: expression[1] }, kind: APPEND_EXPR_HEAD, trusted: forceTrusted }; default: { if (isBuilderCallExpression(expression)) { return { expr: normalizeCallExpression(expression), kind: APPEND_EXPR_HEAD, trusted: forceTrusted }; } else { throw new Error(`Unexpected array in expression position (wasn't a tuple expression and ${expression[0]} isn't wrapped in parens, so it isn't a call): ${JSON.stringify(expression)}`); } } } } else if (typeof expression !== 'object') { switch(typeof expression){ case 'string': { return normalizeAppendHead(normalizeDottedPath(expression), forceTrusted); } case 'boolean': case 'number': return { expr: { type: LITERAL_EXPR, value: expression }, kind: APPEND_EXPR_HEAD, trusted: true }; default: assertNever(expression); } } else { assertNever(expression); } } function normalizeExpression(expression) { if (expression === null || expression === undefined) { return { type: LITERAL_EXPR, value: expression }; } else if (Array.isArray(expression)) { switch(expression[0]){ case BUILDER_LITERAL: return { type: LITERAL_EXPR, value: expression[1] }; case BUILDER_GET: { return normalizePath(expression[1], expression[2]); } case BUILDER_CONCAT: { const expr = { type: CONCAT_EXPR, params: normalizeParams(expression.slice(1)) }; return expr; } case BUILDER_HAS_BLOCK: return { type: HAS_BLOCK_EXPR, name: expression[1] }; case BUILDER_HAS_BLOCK_PARAMS: return { type: HAS_BLOCK_PARAMS_EXPR, name: expression[1] }; default: { if (isBuilderCallExpression(expression)) { return normalizeCallExpression(expression); } else { throw new Error(`Unexpected array in expression position (wasn't a tuple expression and ${expression[0]} isn't wrapped in parens, so it isn't a call): ${JSON.stringify(expression)}`); } } } } else if (typeof expression !== 'object') { switch(typeof expression){ case 'string': { return normalizeDottedPath(expression); } case 'boolean': case 'number': return { type: LITERAL_EXPR, value: expression }; default: assertNever(expression); } } else { assertNever(expression); } } function statementIsExpression(statement) { if (!Array.isArray(statement)) { return false; } const name = statement[0]; if (typeof name === 'number') { switch(name){ case BUILDER_LITERAL: case BUILDER_GET: case BUILDER_CONCAT: case BUILDER_HAS_BLOCK: case BUILDER_HAS_BLOCK_PARAMS: return true; default: return false; } } if (name[0] === '(') { return true; } return false; } function isBuilderCallExpression(value) { return typeof value[0] === 'string' && value[0][0] === '('; } function normalizeParams(input) { return input.map(normalizeExpression); } function normalizeHash(input) { if (input === null) return null; return mapObject(input, normalizeExpression); } function normalizeCallExpression(expr) { switch(expr.length){ case 1: return { type: CALL_EXPR, head: normalizeCallHead(expr[0]), params: null, hash: null }; case 2: { if (Array.isArray(expr[1])) { return { type: CALL_EXPR, head: normalizeCallHead(expr[0]), params: normalizeParams(expr[1]), hash: null }; } else { return { type: CALL_EXPR, head: normalizeCallHead(expr[0]), params: null, hash: normalizeHash(expr[1]) }; } } case 3: return { type: CALL_EXPR, head: normalizeCallHead(expr[0]), params: normalizeParams(expr[1]), hash: normalizeHash(expr[2]) }; } } class ProgramSymbols { toSymbols() { return this._symbols.slice(1); } toUpvars() { return this._freeVariables; } freeVar(name) { return addString(this._freeVariables, name); } block(name) { return this.symbol(name); } arg(name) { return addString(this._symbols, name); } local(name) { throw new Error(`No local ${name} was found. Maybe you meant ^${name} for upvar, or !${name} for keyword?`); } this() { return 0; } hasLocal(_name) { return false; } // any symbol symbol(name) { return addString(this._symbols, name); } child(locals) { return new LocalSymbols(this, locals); } constructor(){ this._freeVariables = []; this._symbols = [ 'this' ]; this.top = this; } } class LocalSymbols { constructor(parent, locals){ this.parent = parent; this.locals = dict(); for (let local of locals){ this.locals[local] = parent.top.symbol(local); } } get paramSymbols() { return values(this.locals); } get top() { return this.parent.top; } freeVar(name) { return this.parent.freeVar(name); } arg(name) { return this.parent.arg(name); } block(name) { return this.parent.block(name); } local(name) { if (name in this.locals) { return this.locals[name]; } else { return this.parent.local(name); } } this() { return this.parent.this(); } hasLocal(name) { if (name in this.locals) { return true; } else { return this.parent.hasLocal(name); } } child(locals) { return new LocalSymbols(this, locals); } } function addString(array, item) { let index = array.indexOf(item); if (index === -1) { index = array.length; array.push(item); return index; } else { return index; } } function unimpl(message) { return new Error(`unimplemented ${message}`); } function buildStatements(statements, symbols) { let out = []; statements.forEach((s)=>out.push(...buildStatement(normalizeStatement(s), symbols))); return out; } function buildNormalizedStatements(statements, symbols) { let out = []; statements.forEach((s)=>out.push(...buildStatement(s, symbols))); return out; } function buildStatement(normalized, symbols = new ProgramSymbols()) { switch(normalized.kind){ case APPEND_PATH_HEAD: { return [ [ normalized.trusted ? opcodes.TrustingAppend : opcodes.Append, buildGetPath(normalized.path, symbols) ] ]; } case APPEND_EXPR_HEAD: { return [ [ normalized.trusted ? opcodes.TrustingAppend : opcodes.Append, buildExpression(normalized.expr, normalized.trusted ? 'TrustedAppend' : 'Append', symbols) ] ]; } case CALL_HEAD: { let { head: path, params, hash, trusted } = normalized; let builtParams = params ? buildParams(params, symbols) : null; let builtHash = hash ? buildHash$1(hash, symbols) : null; let builtExpr = buildCallHead(path, trusted ? resolution.ResolveAsHelperHead : resolution.ResolveAsComponentOrHelperHead, symbols); return [ [ trusted ? opcodes.TrustingAppend : opcodes.Append, [ opcodes.Call, builtExpr, builtParams, builtHash ] ] ]; } case LITERAL_HEAD: { return [ [ opcodes.Append, normalized.value ] ]; } case COMMENT_HEAD: { return [ [ opcodes.Comment, normalized.value ] ]; } case BLOCK_HEAD: { let blocks = buildBlocks(normalized.blocks, normalized.blockParams, symbols); let hash = buildHash$1(normalized.hash, symbols); let params = buildParams(normalized.params, symbols); let path = buildCallHead(normalized.head, resolution.ResolveAsComponentHead, symbols); return [ [ opcodes.Block, path, params, hash, blocks ] ]; } case KEYWORD_HEAD: { return [ buildKeyword(normalized, symbols) ]; } case ELEMENT_HEAD: return buildElement$1(normalized, symbols); case MODIFIER_HEAD: throw unimpl('modifier'); case DYNAMIC_COMPONENT_HEAD: throw unimpl('dynamic component'); default: assertNever(normalized); } } function s(arr, ...interpolated) { let result = arr.reduce(// eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme (result, string, i)=>result + `${string}${interpolated[i] ? String(interpolated[i]) : ''}`, ''); return [ BUILDER_LITERAL, result ]; } function c(arr, ...interpolated) { let result = arr.reduce(// eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme (result, string, i)=>result + `${string}${interpolated[i] ? String(interpolated[i]) : ''}`, ''); return [ BUILDER_COMMENT, result ]; } function unicode(charCode) { return String.fromCharCode(parseInt(charCode, 16)); } const NEWLINE = '\n'; function buildKeyword(normalized, symbols) { let { name } = normalized; let params = buildParams(normalized.params, symbols); let childSymbols = symbols.child(normalized.blockParams || []); let block = buildBlock$1(normalized.blocks['default'], childSymbols, childSymbols.paramSymbols); let inverse = normalized.blocks['else'] ? buildBlock$1(normalized.blocks['else'], symbols, []) : null; switch(name){ case 'let': return [ opcodes.Let, expect(params), block ]; case 'if': return [ opcodes.If, expect(params)[0], block, inverse ]; case 'each': { let keyExpr = normalized.hash ? normalized.hash['key'] : null; let key = keyExpr ? buildExpression(keyExpr, 'Strict', symbols) : null; return [ opcodes.Each, expect(params)[0], key, block, inverse ]; } default: throw new Error('unimplemented keyword'); } } function buildElement$1({ name, attrs, block }, symbols) { let out = [ hasSplat(attrs) ? [ opcodes.OpenElementWithSplat, name ] : [ opcodes.OpenElement, name ] ]; if (attrs) { let { params} = buildElementParams(attrs, symbols); out.push(...params); } out.push([ opcodes.FlushElement ]); if (Array.isArray(block)) { block.forEach((s)=>out.push(...buildStatement(s, symbols))); } out.push([ opcodes.CloseElement ]); return out; } function hasSplat(attrs) { if (attrs === null) return false; return Object.keys(attrs).some((a)=>attrs[a] === SPLAT_HEAD); } function buildElementParams(attrs, symbols) { let params = []; let keys = []; let values = []; for (const [key, value] of Object.entries(attrs)){ if (value === SPLAT_HEAD) { params.push([ opcodes.AttrSplat, symbols.block('&attrs') ]); } else if (key[0] === '@') { keys.push(key); values.push(buildExpression(value, 'Strict', symbols)); } else { params.push(...buildAttributeValue(key, value, // TODO: extract namespace from key extractNamespace(key), symbols)); } } return { params, args: isPresentArray(keys) && isPresentArray(values) ? [ keys, values ] : null }; } function extractNamespace(name) { if (name === 'xmlns') { return NS_XMLNS; } let match = /^([^:]*):([^:]*)$/u.exec(name); if (match === null) { return null; } let namespace = match[1]; switch(namespace){ case 'xlink': return NS_XLINK; case 'xml': return NS_XML; case 'xmlns': return NS_XMLNS; } return null; } function buildAttributeValue(name, value, namespace, symbols) { switch(value.type){ case LITERAL_EXPR: { let val = value.value; if (val === false) { return []; } else if (val === true) { return [ [ opcodes.StaticAttr, name, '', namespace ?? undefined ] ]; } else if (typeof val === 'string') { return [ [ opcodes.StaticAttr, name, val, namespace ?? undefined ] ]; } else { throw new Error(`Unexpected/unimplemented literal attribute ${JSON.stringify(val)}`); } } default: return [ [ opcodes.DynamicAttr, name, buildExpression(value, 'AttrValue', symbols), namespace ?? undefined ] ]; } } function varContext(context, bare) { switch(context){ case 'Append': return bare ? 'AppendBare' : 'AppendInvoke'; case 'TrustedAppend': return bare ? 'TrustedAppendBare' : 'TrustedAppendInvoke'; case 'AttrValue': return bare ? 'AttrValueBare' : 'AttrValueInvoke'; default: return context; } } function buildExpression(expr, context, symbols) { switch(expr.type){ case GET_PATH_EXPR: { return buildGetPath(expr, symbols); } case GET_VAR_EXPR: { return buildVar$1(expr.variable, varContext(context, true), symbols); } case CONCAT_EXPR: { return [ opcodes.Concat, buildConcat$1(expr.params, symbols) ]; } case CALL_EXPR: { let builtParams = buildParams(expr.params, symbols); let builtHash = buildHash$1(expr.hash, symbols); let builtExpr = buildCallHead(expr.head, context === 'Strict' ? 'SubExpression' : varContext(context, false), symbols); return [ opcodes.Call, builtExpr, builtParams, builtHash ]; } case HAS_BLOCK_EXPR: { return [ opcodes.HasBlock, buildVar$1({ kind: BLOCK_VAR, name: expr.name}, resolution.Strict, symbols) ]; } case HAS_BLOCK_PARAMS_EXPR: { return [ opcodes.HasBlockParams, buildVar$1({ kind: BLOCK_VAR, name: expr.name}, resolution.Strict, symbols) ]; } case LITERAL_EXPR: { if (expr.value === undefined) { return [ opcodes.Undefined ]; } else { return expr.value; } } default: assertNever(expr); } } function buildCallHead(callHead, context, symbols) { if (callHead.type === GET_VAR_EXPR) { return buildVar$1(callHead.variable, context, symbols); } else { return buildGetPath(callHead, symbols); } } function buildGetPath(head, symbols) { return buildVar$1(head.path.head, resolution.Strict, symbols, head.path.tail); } function buildVar$1(head, context, symbols, path) { let op = opcodes.GetSymbol; let sym; switch(head.kind){ case FREE_VAR: if (context === 'Strict') { op = opcodes.GetStrictKeyword; } else if (context === 'AppendBare') { op = opcodes.GetFreeAsComponentOrHelperHead; } else if (context === 'AppendInvoke') { op = opcodes.GetFreeAsComponentOrHelperHead; } else if (context === 'TrustedAppendBare') { op = opcodes.GetFreeAsHelperHead; } else if (context === 'TrustedAppendInvoke') { op = opcodes.GetFreeAsHelperHead; } else if (context === 'AttrValueBare') { op = opcodes.GetFreeAsHelperHead; } else if (context === 'AttrValueInvoke') { op = opcodes.GetFreeAsHelperHead; } else if (context === 'SubExpression') { op = opcodes.GetFreeAsHelperHead; } else { op = expressionContextOp(context); } sym = symbols.freeVar(head.name); break; default: op = opcodes.GetSymbol; sym = getSymbolForVar(head.kind, symbols, head.name); } if (path === undefined || path.length === 0) { return [ op, sym ]; } else { return [ op, sym, path ]; } } function getSymbolForVar(kind, symbols, name) { switch(kind){ case ARG_VAR: return symbols.arg(name); case BLOCK_VAR: return symbols.block(name); case LOCAL_VAR: return symbols.local(name); case THIS_VAR: return symbols.this(); default: return exhausted(); } } function expressionContextOp(context) { switch(context){ case resolution.Strict: return opcodes.GetStrictKeyword; case resolution.ResolveAsComponentOrHelperHead: return opcodes.GetFreeAsComponentOrHelperHead; case resolution.ResolveAsHelperHead: return opcodes.GetFreeAsHelperHead; case resolution.ResolveAsModifierHead: return opcodes.GetFreeAsModifierHead; case resolution.ResolveAsComponentHead: return opcodes.GetFreeAsComponentHead; default: return exhausted(); } } function buildParams(exprs, symbols) { if (exprs === null || !isPresentArray(exprs)) return null; return exprs.map((e)=>buildExpression(e, 'Strict', symbols)); } function buildConcat$1(exprs, symbols) { return exprs.map((e)=>buildExpression(e, 'AttrValue', symbols)); } function buildHash$1(exprs, symbols) { if (exprs === null) return null; let out = [ [], [] ]; for (const [key, value] of Object.entries(exprs)){ out[0].push(key); out[1].push(buildExpression(value, 'Strict', symbols)); } return out; } function buildBlocks(blocks, blockParams, parent) { let keys = []; let values = []; for (const [name, block] of Object.entries(blocks)){ keys.push(name); if (name === 'default') { let symbols = parent.child(blockParams || []); values.push(buildBlock$1(block, symbols, symbols.paramSymbols)); } else { values.push(buildBlock$1(block, parent, [])); } } return [ keys, values ]; } function buildBlock$1(block, symbols, locals = []) { return [ buildNormalizedStatements(block, symbols), locals ]; } const Char = { NBSP: 0xa0, QUOT: 0x22, LT: 0x3c, GT: 0x3e, AMP: 0x26 }; // \x26 is ampersand, \xa0 is non-breaking space const ATTR_VALUE_REGEX_TEST = /["\x26\xa0]/u; const ATTR_VALUE_REGEX_REPLACE = new RegExp(ATTR_VALUE_REGEX_TEST.source, 'gu'); const TEXT_REGEX_TEST = /[&<>\xa0]/u; const TEXT_REGEX_REPLACE = new RegExp(TEXT_REGEX_TEST.source, 'gu'); function attrValueReplacer(char) { switch(char.charCodeAt(0)){ case Char.NBSP: return '&nbsp;'; case Char.QUOT: return '&quot;'; case Char.AMP: return '&amp;'; default: return char; } } function textReplacer(char) { switch(char.charCodeAt(0)){ case Char.NBSP: return '&nbsp;'; case Char.AMP: return '&amp;'; case Char.LT: return '&lt;'; case Char.GT: return '&gt;'; default: return char; } } function escapeAttrValue(attrValue) { if (ATTR_VALUE_REGEX_TEST.test(attrValue)) { return attrValue.replace(ATTR_VALUE_REGEX_REPLACE, attrValueReplacer); } return attrValue; } function escapeText(text) { if (TEXT_REGEX_TEST.test(text)) { return text.replace(TEXT_REGEX_REPLACE, textReplacer); } return text; } function sortByLoc(a, b) { // If either is invisible, don't try to order them if (a.loc.isInvisible || b.loc.isInvisible) { return 0; } if (a.loc.startPosition.line < b.loc.startPosition.line) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column < b.loc.startPosition.column) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column === b.loc.startPosition.column) { return 0; } return 1; } const voidMap = new Set([ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]); const NON_WHITESPACE = /^\S/u; /** * Examples when true: * - link * - liNK * * Examples when false: * - Link (component) */ function isVoidTag(tag) { return voidMap.has(tag.toLowerCase()) && tag[0]?.toLowerCase() === tag[0]; } class Printer { constructor(options){ this.buffer = ''; this.options = options; } /* This is used by _all_ methods on this Printer class that add to `this.buffer`, it allows consumers of the printer to use alternate string representations for a given node. The primary use case for this are things like source -> source codemod utilities. For example, ember-template-recast attempts to always preserve the original string formatting in each AST node if no modifications are made to it. */ handledByOverride(node, ensureLeadingWhitespace = false) { if (this.options.override !== undefined) { let result = this.options.override(node, this.options); if (typeof result === 'string') { if (ensureLeadingWhitespace && NON_WHITESPACE.test(result)) { result = ` ${result}`; } this.buffer += result; return true; } } return false; } Node(node) { switch(node.type){ case 'MustacheStatement': case 'BlockStatement': case 'MustacheCommentStatement': case 'CommentStatement': case 'TextNode': case 'ElementNode': case 'AttrNode': case 'Block': case 'Template': return this.TopLevelStatement(node); case 'StringLiteral': case 'BooleanLiteral': case 'NumberLiteral': case 'UndefinedLiteral': case 'NullLiteral': case 'PathExpression': case 'SubExpression': return this.Expression(node); case 'ConcatStatement': // should have an AttrNode parent return this.ConcatStatement(node); case 'Hash': return this.Hash(node); case 'HashPair': return this.HashPair(node); case 'ElementModifierStatement': return this.ElementModifierStatement(node); } } Expression(expression) { switch(expression.type){ case 'StringLiteral': case 'BooleanLiteral': case 'NumberLiteral': case 'UndefinedLiteral': case 'NullLiteral': return this.Literal(expression); case 'PathExpression': return this.PathExpression(expression); case 'SubExpression': return this.SubExpression(expression); } } Literal(literal) { switch(literal.type){ case 'StringLiteral': return this.StringLiteral(literal); case 'BooleanLiteral': return this.BooleanLiteral(literal); case 'NumberLiteral': return this.NumberLiteral(literal); case 'UndefinedLiteral': return this.UndefinedLiteral(literal); case 'NullLiteral': return this.NullLiteral(literal); } } TopLevelStatement(statement) { switch(statement.type){ case 'MustacheStatement': return this.MustacheStatement(statement); case 'BlockStatement': return this.BlockStatement(statement); case 'MustacheCommentStatement': return this.MustacheCommentStatement(statement); case 'CommentStatement': return this.CommentStatement(statement); case 'TextNode': return this.TextNode(statement); case 'ElementNode': return this.ElementNode(statement); case 'Block': return this.Block(statement); case 'Template': return this.Template(statement); case 'AttrNode': // should have element return this.AttrNode(statement); } } Template(template) { this.TopLevelStatements(template.body); } Block(block) { /* When processing a template like: ```hbs {{#if whatever}} whatever {{else if somethingElse}} something else {{else}} fallback {{/if}} ``` The AST still _effectively_ looks like: ```hbs {{#if whatever}} whatever {{else}}{{#if somethingElse}} something else {{else}} fallback {{/if}}{{/if}} ``` The only way we can tell if that is the case is by checking for `block.chained`, but unfortunately when the actual statements are processed the `block.body[0]` node (which will always be a `BlockStatement`) has no clue that its ancestor `Block` node was chained. This "forwards" the `chained` setting so that we can check it later when processing the `BlockStatement`. */ if (block.chained) { let firstChild = block.body[0]; firstChild.chained = true; } if (this.handledByOverride(block)) { return; } this.TopLevelStatements(block.body); } TopLevelStatements(statements) { statements.forEach((statement)=>this.TopLevelStatement(statement)); } ElementNode(el) { if (this.handledByOverride(el)) { return; } this.OpenElementNode(el); this.TopLevelStatements(el.children); this.CloseElementNode(el); } OpenElementNode(el) { this.buffer += `<${el.tag}`; const parts = [ ...el.attributes, ...el.modifiers, ...el.comments ].sort(sortByLoc); for (const part of parts){ this.buffer += ' '; switch(part.type){ case 'AttrNode': this.AttrNode(part); break; case 'ElementModifierStatement': this.ElementModifierStatement(part); break; case 'MustacheCommentStatement': this.MustacheCommentStatement(part); break; } } if (el.blockParams.length) { this.BlockParams(el.blockParams); } if (el.selfClosing) { this.buffer += ' /'; } this.buffer += '>'; } CloseElementNode(el) { if (el.selfClosing || isVoidTag(el.tag)) { return; } this.buffer += `</${el.tag}>`; } AttrNode(attr) { if (this.handledByOverride(attr)) { return; } let { name, value } = attr; this.buffer += name; const isAttribute = !name.startsWith('@'); const shouldElideValue = isAttribute && value.type == 'TextNode' && value.chars.length === 0; if (!shouldElideValue) { this.buffer += '='; this.AttrNodeValue(value); } } AttrNodeValue(value) { if (value.type === 'TextNode') { let quote = '"'; if (this.options.entityEncoding === 'raw') { if (value.chars.includes('"') && !value.chars.includes("'")) { quote = "'"; } } this.buffer += quote; this.TextNode(value, quote); this.buffer += quote; } else { this.Node(value); } } TextNode(text, isInAttr) { if (this.handledByOverride(text)) { return; } if (this.options.entityEncoding === 'raw') { if (isInAttr && text.chars.includes(isInAttr)) { this.buffer += escapeAttrValue(text.chars); } else { this.buffer += text.chars; } } else if (isInAttr) { this.buffer += escapeAttrValue(text.chars); } else { this.buffer += escapeText(text.chars); } } MustacheStatement(mustache) { if (this.handledByOverride(mustache)) { return; } this.buffer += mustache.trusting ? '{{{' : '{{'; if (mustache.strip.open) { this.buffer += '~'; } this.Expression(mustache.path); this.Params(mustache.params); this.Hash(mustache.hash); if (mustache.strip.close) { this.buffer += '~'; } this.buffer += mustache.trusting ? '}}}' : '}}'; } BlockStatement(block) { if (this.handledByOverride(block)) { return; } if (block.chained) { this.buffer += block.inverseStrip.open ? '{{~' : '{{'; this.buffer += 'else '; } else { this.buffer += block.openStrip.open ? '{{~#' : '{{#'; } this.Expression(block.path); this.Params(block.params); this.Hash(block.hash); if (block.program.blockParams.length) { this.BlockParams(block.program.blockParams); } if (block.chained) { this.buffer += block.inverseStrip.close ? '~}}' : '}}'; } else { this.buffer += block.openStrip.close ? '~}}' : '}}'; } this.Block(block.program); if (block.inverse) { if (!block.inverse.chained) { this.buffer += block.inverseStrip.open ? '{{~' : '{{'; this.buffer += 'else'; this.buffer += block.inverseStrip.close ? '~}}' : '}}'; } this.Block(block.inverse); } if (!block.chained) { this.buffer += block.closeStrip.open ? '{{~/' : '{{/'; this.Expression(block.path); this.buf