@glimmer/compiler
Version:
1,695 lines (1,672 loc) • 484 kB
JavaScript
'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 ' ';
case Char.QUOT:
return '"';
case Char.AMP:
return '&';
default:
return char;
}
}
function textReplacer(char) {
switch(char.charCodeAt(0)){
case Char.NBSP:
return ' ';
case Char.AMP:
return '&';
case Char.LT:
return '<';
case Char.GT:
return '>';
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