ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,963 lines (1,917 loc) • 185 kB
JavaScript
import templateOnly from '../../component/template-only.js';
import { a9 as CURRIED_MODIFIER, az as CURRIED_HELPER, C as CURRIED_COMPONENT } from '../../../shared-chunks/capabilities-DHiXCCuB.js';
import '../../../shared-chunks/debug-to-string-BsFOvUtQ.js';
import { isDevelopingApp } from '@embroider/macros';
import { SexpOpcodes as opcodes, WellKnownTagNames, WellKnownAttrNames } from '../../../@glimmer/wire-format/index.js';
import { S as SourceOffset, a as SourceSpan, c as cannotReplaceOrRemoveInKeyHandlerYet, b as cannotRemoveNode, d as cannotReplaceNode, e as SYNTHETIC_LOCATION, i as isVoidTag, E as EntityParser, f as EventedTokenizer, n as namedCharRefs, N as NON_EXISTENT_LOCATION, p as parseWithoutProcessing, g as parse, h as build, v as voidMap, j as STRICT_RESOLUTION, T as Template$1, B as Block, k as NamedBlock$1, l as SourceSlice, A as Args$1, P as PositionalArguments, m as NamedArgument$1, o as NamedArguments$1, H as HtmlAttr, q as SplatAttr$1, C as ComponentArg, r as PathExpression$1, K as KeywordExpression, s as ThisReference, u as ArgReference, F as FreeVarReference, L as LocalVarReference, w as CallExpression$1, I as InterpolateExpression$1, x as LiteralExpression, y as AppendContent, z as ElementModifier, D as NamedBlocks$1, G as InvokeBlock$1, J as SimpleElement$1, M as InvokeComponent$1, O as SpanList, Q as LooseModeResolution, R as COMPONENT_NAMESPACE, U as MODIFIER_NAMESPACE, V as HELPER_NAMESPACE, W as HtmlText, X as HtmlComment, Y as GlimmerComment, Z as Printer, _ as node, $ as isLiteral$1, a0 as maybeLoc, a1 as loc } from '../../../shared-chunks/transform-resolutions-vHYYonpB.js';
import { a as assert } from '../../../shared-chunks/assert-CUCJBR2C.js';
import { s as setLocalDebugType } from '../../../shared-chunks/debug-brand-B1TWjOCH.js';
import { i as isPresentArray, b as asPresentArray, g as getLast, a as getFirst, m as mapPresentArray } from '../../../shared-chunks/present-B1rrjAVM.js';
import { a as assign } from '../../../shared-chunks/object-utils-AijlD-JH.js';
import { u as unwrap, e as expect, d as dict, b as exhausted } from '../../../shared-chunks/collections-B8me-ZlQ.js';
import '../../../@glimmer/global-context/index.js';
import '../../../@glimmer/validator/index.js';
import '../../../shared-chunks/reference-B6HMX4y0.js';
import '../../../@glimmer/destroyable/index.js';
import { s as setComponentTemplate } from '../../../shared-chunks/template-CMHIG4cn.js';
import { t as templateFactory } from '../../../shared-chunks/index-CQkjwqTv.js';
import compileOptions from './compile-options.js';
function isKeyword(word, type) {
if (word in KEYWORDS_TYPES) {
{
return true;
}
} else {
return false;
}
}
/**
* This includes the full list of keywords currently in use in the template
* language, and where their valid usages are.
*/
const KEYWORDS_TYPES = {
action: ['Call', 'Modifier'],
component: ['Call', 'Append', 'Block'],
debugger: ['Append'],
'each-in': ['Block'],
each: ['Block'],
'has-block-params': ['Call', 'Append'],
'has-block': ['Call', 'Append'],
helper: ['Call', 'Append'],
if: ['Call', 'Append', 'Block'],
'in-element': ['Block'],
let: ['Block'],
log: ['Call', 'Append'],
modifier: ['Call', 'Modifier'],
mount: ['Append'],
mut: ['Call', 'Append'],
outlet: ['Append'],
readonly: ['Call', 'Append'],
unbound: ['Call', 'Append'],
unless: ['Call', 'Append', 'Block'],
yield: ['Append']
};
class Source {
static from(source, options = {}) {
return new Source(source, options.meta?.moduleName);
}
constructor(source, module = 'an unknown module') {
this.source = source;
this.module = module;
setLocalDebugType('syntax:source', this);
}
/**
* Validate that the character offset represents a position in the source string.
*/
validate(offset) {
return offset >= 0 && offset <= this.source.length;
}
slice(start, end) {
return this.source.slice(start, end);
}
offsetFor(line, column) {
return SourceOffset.forHbsPos(this, {
line,
column
});
}
spanFor({
start,
end
}) {
return SourceSpan.forHbsLoc(this, {
start: {
line: start.line,
column: start.column
},
end: {
line: end.line,
column: end.column
}
});
}
hbsPosFor(offset) {
let seenLines = 0;
let seenChars = 0;
if (offset > this.source.length) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
let nextLine = this.source.indexOf('\n', seenChars);
if (offset <= nextLine || nextLine === -1) {
return {
line: seenLines + 1,
column: offset - seenChars
};
} else {
seenLines += 1;
seenChars = nextLine + 1;
}
}
}
charPosFor(position) {
let {
line,
column
} = position;
let sourceString = this.source;
let sourceLength = sourceString.length;
let seenLines = 0;
let seenChars = 0;
while (seenChars < sourceLength) {
let nextLine = this.source.indexOf('\n', seenChars);
if (nextLine === -1) nextLine = this.source.length;
if (seenLines === line - 1) {
if (seenChars + column > nextLine) return nextLine;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (isDevelopingApp()) {
let roundTrip = this.hbsPosFor(seenChars + column);
assert(roundTrip.line === line);
assert(roundTrip.column === column);
}
return seenChars + column;
} else if (nextLine === -1) {
return 0;
} else {
seenLines += 1;
seenChars = nextLine + 1;
}
}
return sourceLength;
}
}
function generateSyntaxError(message, location) {
let {
module,
loc
} = location;
let {
line,
column
} = loc.start;
let code = location.asString();
let quotedCode = code ? `\n\n|\n| ${code.split('\n').join('\n| ')}\n|\n\n` : '';
let error = new Error(`${message}: ${quotedCode}(error occurred in '${module}' @ line ${line} : column ${column})`);
error.name = 'SyntaxError';
error.location = location;
error.code = code;
return error;
}
// ensure stays in sync with typing
// ParentNode and ChildKey types are derived from VisitorKeysMap
const visitorKeys = {
Template: ['body'],
Block: ['body'],
MustacheStatement: ['path', 'params', 'hash'],
BlockStatement: ['path', 'params', 'hash', 'program', 'inverse'],
ElementModifierStatement: ['path', 'params', 'hash'],
CommentStatement: [],
MustacheCommentStatement: [],
ElementNode: ['attributes', 'modifiers', 'children', 'comments'],
AttrNode: ['value'],
TextNode: [],
ConcatStatement: ['parts'],
SubExpression: ['path', 'params', 'hash'],
PathExpression: [],
StringLiteral: [],
BooleanLiteral: [],
NumberLiteral: [],
NullLiteral: [],
UndefinedLiteral: [],
Hash: ['pairs'],
HashPair: ['value']
};
class WalkerPath {
node;
parent;
parentKey;
constructor(node, parent = null, parentKey = null) {
this.node = node;
this.parent = parent;
this.parentKey = parentKey;
}
get parentNode() {
return this.parent ? this.parent.node : null;
}
parents() {
return {
[Symbol.iterator]: () => {
return new PathParentsIterator(this);
}
};
}
}
class PathParentsIterator {
path;
constructor(path) {
this.path = path;
}
next() {
if (this.path.parent) {
this.path = this.path.parent;
return {
done: false,
value: this.path
};
} else {
return {
done: true,
value: null
};
}
}
}
function getEnterFunction(handler) {
if (typeof handler === 'function') {
return handler;
} else {
return handler.enter;
}
}
function getExitFunction(handler) {
if (typeof handler === 'function') {
return undefined;
} else {
return handler.exit;
}
}
function getKeyHandler(handler, key) {
let keyVisitor = typeof handler !== 'function' ? handler.keys : undefined;
if (keyVisitor === undefined) return;
let keyHandler = keyVisitor[key];
if (keyHandler !== undefined) {
return keyHandler;
}
return keyVisitor.All;
}
function getNodeHandler(visitor, nodeType) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (visitor.Program) {
if (nodeType === 'Template' && !visitor.Template || nodeType === 'Block' && !visitor.Block) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return visitor.Program;
}
}
let handler = visitor[nodeType];
if (handler !== undefined) {
return handler;
}
return visitor.All;
}
function visitNode(visitor, path) {
let {
node,
parent,
parentKey
} = path;
let handler = getNodeHandler(visitor, node.type);
let enter;
let exit;
if (handler !== undefined) {
enter = getEnterFunction(handler);
exit = getExitFunction(handler);
}
let result;
if (enter !== undefined) {
result = enter(node, path);
}
if (result !== undefined && result !== null) {
if (JSON.stringify(node) === JSON.stringify(result)) {
result = undefined;
} else if (Array.isArray(result)) {
visitArray(visitor, result, parent, parentKey);
return result;
} else {
let path = new WalkerPath(result, parent, parentKey);
return visitNode(visitor, path) || result;
}
}
if (result === undefined) {
let keys = visitorKeys[node.type];
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
// we know if it has child keys we can widen to a ParentNode
visitKey(visitor, handler, path, key);
}
if (exit !== undefined) {
result = exit(node, path);
}
}
return result;
}
function get(node, key) {
return node[key];
}
function set(node, key, value) {
node[key] = value;
}
function visitKey(visitor, handler, path, key) {
let {
node
} = path;
let value = get(node, key);
if (!value) {
return;
}
let keyEnter;
let keyExit;
if (handler !== undefined) {
let keyHandler = getKeyHandler(handler, key);
if (keyHandler !== undefined) {
keyEnter = getEnterFunction(keyHandler);
keyExit = getExitFunction(keyHandler);
}
}
if (keyEnter !== undefined) {
if (keyEnter(node, key) !== undefined) {
throw cannotReplaceOrRemoveInKeyHandlerYet(node, key);
}
}
if (Array.isArray(value)) {
visitArray(visitor, value, path, key);
} else {
let keyPath = new WalkerPath(value, path, key);
let result = visitNode(visitor, keyPath);
if (result !== undefined) {
// TODO: dynamically check the results by having a table of
// expected node types in value space, not just type space
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assignKey(node, key, value, result);
}
}
if (keyExit !== undefined) {
if (keyExit(node, key) !== undefined) {
throw cannotReplaceOrRemoveInKeyHandlerYet(node, key);
}
}
}
function visitArray(visitor, array, parent, parentKey) {
for (let i = 0; i < array.length; i++) {
let node = unwrap(array[i]);
let path = new WalkerPath(node, parent, parentKey);
let result = visitNode(visitor, path);
if (result !== undefined) {
i += spliceArray(array, i, result) - 1;
}
}
}
function assignKey(node, key, value, result) {
if (result === null) {
throw cannotRemoveNode(value, node, key);
} else if (Array.isArray(result)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (result.length === 1) {
set(node, key, result[0]);
} else {
if (result.length === 0) {
throw cannotRemoveNode(value, node, key);
} else {
throw cannotReplaceNode(value, node, key);
}
}
} else {
set(node, key, result);
}
}
function spliceArray(array, index, result) {
if (result === null) {
array.splice(index, 1);
return 0;
} else if (Array.isArray(result)) {
array.splice(index, 1, ...result);
return result.length;
} else {
array.splice(index, 1, result);
return 1;
}
}
function traverse(node, visitor) {
let path = new WalkerPath(node);
visitNode(visitor, path);
}
class Walker {
stack = [];
constructor(order) {
this.order = order;
}
visit(node, visitor) {
if (!node) {
return;
}
this.stack.push(node);
if (this.order === 'post') {
this.children(node, visitor);
visitor(node, this);
} else {
visitor(node, this);
this.children(node, visitor);
}
this.stack.pop();
}
children(node, callback) {
switch (node.type) {
case 'Block':
case 'Template':
walkBody(this, node.body, callback);
return;
case 'ElementNode':
walkBody(this, node.children, callback);
return;
case 'BlockStatement':
this.visit(node.program, callback);
this.visit(node.inverse || null, callback);
return;
default:
return;
}
}
}
function walkBody(walker, body, callback) {
for (const child of body) {
walker.visit(child, callback);
}
}
function childrenFor(node) {
switch (node.type) {
case 'Block':
case 'Template':
return node.body;
case 'ElementNode':
return node.children;
}
}
function appendChild(parent, node) {
childrenFor(parent).push(node);
}
function isHBSLiteral(path) {
return path.type === 'StringLiteral' || path.type === 'BooleanLiteral' || path.type === 'NumberLiteral' || path.type === 'NullLiteral' || path.type === 'UndefinedLiteral';
}
function printLiteral(literal) {
if (literal.type === 'UndefinedLiteral') {
return 'undefined';
} else {
return JSON.stringify(literal.value);
}
}
function isUpperCase(tag) {
return tag[0] === tag[0]?.toUpperCase() && tag[0] !== tag[0]?.toLowerCase();
}
function isLowerCase(tag) {
return tag[0] === tag[0]?.toLowerCase() && tag[0] !== tag[0]?.toUpperCase();
}
let _SOURCE;
function SOURCE() {
if (!_SOURCE) {
_SOURCE = new Source('', '(synthetic)');
}
return _SOURCE;
}
// const SOURCE = new Source('', '(tests)');
// Statements
function buildMustache(path, params = [], hash = buildHash([]), trusting = false, loc, strip) {
return b.mustache({
path: buildPath(path),
params,
hash,
trusting,
strip,
loc: buildLoc(loc || null)
});
}
function buildBlock(path, params, hash, _defaultBlock, _elseBlock = null, loc, openStrip, inverseStrip, closeStrip) {
let defaultBlock;
let elseBlock = null;
if (_defaultBlock.type === 'Template') {
defaultBlock = b.blockItself({
params: buildBlockParams(_defaultBlock.blockParams),
body: _defaultBlock.body,
loc: _defaultBlock.loc
});
} else {
defaultBlock = _defaultBlock;
}
if (_elseBlock?.type === 'Template') {
assert(_elseBlock.blockParams.length === 0);
elseBlock = b.blockItself({
params: [],
body: _elseBlock.body,
loc: _elseBlock.loc
});
} else {
elseBlock = _elseBlock;
}
return b.block({
path: buildPath(path),
params: params || [],
hash: hash || buildHash([]),
defaultBlock,
elseBlock,
loc: buildLoc(loc || null),
openStrip,
inverseStrip,
closeStrip
});
}
function buildElementModifier(path, params, hash, loc) {
return b.elementModifier({
path: buildPath(path),
params: params || [],
hash: hash || buildHash([]),
loc: buildLoc(loc || null)
});
}
function buildComment(value, loc) {
return b.comment({
value: value,
loc: buildLoc(loc || null)
});
}
function buildMustacheComment(value, loc) {
return b.mustacheComment({
value: value,
loc: buildLoc(loc || null)
});
}
function buildConcat(parts, loc) {
if (!isPresentArray(parts)) {
throw new Error(`b.concat requires at least one part`);
}
return b.concat({
parts,
loc: buildLoc(loc || null)
});
}
// Nodes
function buildElement(tag, options = {}) {
let {
attrs,
blockParams,
modifiers,
comments,
children,
openTag,
closeTag: _closeTag,
loc
} = options;
// this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST
let path;
let selfClosing;
if (typeof tag === 'string') {
if (tag.endsWith('/')) {
path = buildPath(tag.slice(0, -1));
selfClosing = true;
} else {
path = buildPath(tag);
}
} else if ('type' in tag) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- supports JS users
assert(tag.type === 'PathExpression', `Invalid tag type ${tag.type}`);
path = tag;
} else if ('path' in tag) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- supports JS users
assert(tag.path.type === 'PathExpression', `Invalid tag type ${tag.path.type}`);
path = tag.path;
selfClosing = tag.selfClosing;
} else {
path = buildPath(tag.name);
selfClosing = tag.selfClosing;
}
let params = blockParams?.map(param => {
if (typeof param === 'string') {
return buildVar(param);
} else {
return param;
}
});
let closeTag = null;
if (_closeTag) {
closeTag = buildLoc(_closeTag);
} else if (_closeTag === undefined) {
closeTag = selfClosing || isVoidTag(path.original) ? null : buildLoc(null);
}
return b.element({
path,
selfClosing: selfClosing || false,
attributes: attrs || [],
params: params || [],
modifiers: modifiers || [],
comments: comments || [],
children: children || [],
openTag: buildLoc(openTag || null),
closeTag,
loc: buildLoc(loc || null)
});
}
function buildAttr(name, value, loc) {
return b.attr({
name: name,
value: value,
loc: buildLoc(loc || null)
});
}
function buildText(chars = '', loc) {
return b.text({
chars,
loc: buildLoc(loc || null)
});
}
// Expressions
function buildSexpr(path, params = [], hash = buildHash([]), loc) {
return b.sexpr({
path: buildPath(path),
params,
hash,
loc: buildLoc(loc || null)
});
}
function buildHead(original, loc) {
let [head, ...tail] = asPresentArray(original.split('.'));
let headNode = b.head({
original: head,
loc: buildLoc(loc || null)
});
return b.path({
head: headNode,
tail,
loc: buildLoc(loc || null)
});
}
function buildThis(loc) {
return b.this({
loc: buildLoc(loc || null)
});
}
function buildAtName(name, loc) {
return b.atName({
name,
loc: buildLoc(loc || null)
});
}
function buildVar(name, loc) {
return b.var({
name,
loc: buildLoc(loc || null)
});
}
function buildHeadFromString(original, loc) {
return b.head({
original,
loc: buildLoc(loc || null)
});
}
function buildCleanPath(head, tail = [], loc) {
return b.path({
head,
tail,
loc: buildLoc(loc || null)
});
}
function buildPath(path, loc) {
let span = buildLoc(loc || null);
if (typeof path !== 'string') {
if ('type' in path) {
return path;
} else {
assert(path.head.indexOf('.') === -1);
let {
head,
tail
} = path;
return b.path({
head: b.head({
original: head,
loc: span.sliceStartChars({
chars: head.length
})
}),
tail,
loc: buildLoc(loc || null)
});
}
}
let {
head,
tail
} = buildHead(path, span);
return b.path({
head,
tail,
loc: span
});
}
function buildLiteral(type, value, loc) {
return b.literal({
type,
value,
loc: buildLoc(loc || null)
});
}
// Miscellaneous
function buildHash(pairs = [], loc) {
return b.hash({
pairs,
loc: buildLoc(loc || null)
});
}
function buildPair(key, value, loc) {
return b.pair({
key,
value,
loc: buildLoc(loc || null)
});
}
function buildProgram(body, blockParams, loc) {
if (blockParams && blockParams.length) {
return buildBlockItself(body, blockParams, false, loc);
} else {
return buildTemplate(body, [], loc);
}
}
function buildBlockParams(params) {
return params.map(p => typeof p === 'string' ? b.var({
name: p,
loc: SourceSpan.synthetic(p)
}) : p);
}
function buildBlockItself(body = [], params = [], chained = false, loc) {
return b.blockItself({
body,
params: buildBlockParams(params),
chained,
loc: buildLoc(loc || null)
});
}
function buildTemplate(body = [], blockParams = [], loc) {
return b.template({
body,
blockParams,
loc: buildLoc(loc || null)
});
}
function buildPosition(line, column) {
return b.pos({
line,
column
});
}
function buildLoc(...args) {
if (args.length === 1) {
let loc = args[0];
if (loc && typeof loc === 'object') {
return SourceSpan.forHbsLoc(SOURCE(), loc);
} else {
return SourceSpan.forHbsLoc(SOURCE(), SYNTHETIC_LOCATION);
}
} else {
let [startLine, startColumn, endLine, endColumn, _source] = args;
let source = _source ? new Source('', _source) : SOURCE();
return SourceSpan.forHbsLoc(source, {
start: {
line: startLine,
column: startColumn
},
end: {
line: endLine || startLine,
column: endColumn || startColumn
}
});
}
}
const publicBuilder = {
mustache: buildMustache,
block: buildBlock,
comment: buildComment,
mustacheComment: buildMustacheComment,
element: buildElement,
elementModifier: buildElementModifier,
attr: buildAttr,
text: buildText,
sexpr: buildSexpr,
concat: buildConcat,
hash: buildHash,
pair: buildPair,
literal: buildLiteral,
program: buildProgram,
blockItself: buildBlockItself,
template: buildTemplate,
loc: buildLoc,
pos: buildPosition,
path: buildPath,
fullPath: buildCleanPath,
head: buildHeadFromString,
at: buildAtName,
var: buildVar,
this: buildThis,
string: literal('StringLiteral'),
boolean: literal('BooleanLiteral'),
number: literal('NumberLiteral'),
undefined() {
return buildLiteral('UndefinedLiteral', undefined);
},
null() {
return buildLiteral('NullLiteral', null);
}
};
function literal(type) {
return function (value, loc) {
return buildLiteral(type, value, loc);
};
}
function buildLegacyMustache({
path,
params,
hash,
trusting,
strip,
loc
}) {
const node = {
type: 'MustacheStatement',
path,
params,
hash,
trusting,
strip,
loc
};
Object.defineProperty(node, 'escaped', {
enumerable: false,
get() {
return !this.trusting;
},
set(value) {
this.trusting = !value;
}
});
return node;
}
function buildLegacyPath({
head,
tail,
loc
}) {
const node = {
type: 'PathExpression',
head,
tail,
get original() {
return [this.head.original, ...this.tail].join('.');
},
set original(value) {
let [head, ...tail] = asPresentArray(value.split('.'));
this.head = publicBuilder.head(head, this.head.loc);
this.tail = tail;
},
loc
};
Object.defineProperty(node, 'parts', {
enumerable: false,
get() {
let parts = asPresentArray(this.original.split('.'));
if (parts[0] === 'this') {
// parts does not include `this`
parts.shift();
} else if (parts[0].startsWith('@')) {
// parts does not include leading `@`
parts[0] = parts[0].slice(1);
}
return Object.freeze(parts);
},
set(values) {
let parts = [...values];
// you are not supposed to already have `this` or `@` in the parts, but since this is
// deprecated anyway, we will infer what you meant and allow it
if (parts[0] !== 'this' && !parts[0]?.startsWith('@')) {
if (this.head.type === 'ThisHead') {
parts.unshift('this');
} else if (this.head.type === 'AtHead') {
parts[0] = `@${parts[0]}`;
}
}
this.original = parts.join('.');
}
});
Object.defineProperty(node, 'this', {
enumerable: false,
get() {
return this.head.type === 'ThisHead';
}
});
Object.defineProperty(node, 'data', {
enumerable: false,
get() {
return this.head.type === 'AtHead';
}
});
return node;
}
function buildLegacyLiteral({
type,
value,
loc
}) {
const node = {
type,
value,
loc
};
Object.defineProperty(node, 'original', {
enumerable: false,
get() {
return this.value;
},
set(value) {
this.value = value;
}
});
return node;
}
const DEFAULT_STRIP = {
close: false,
open: false
};
/**
* The Parser Builder differentiates from the public builder API by:
*
* 1. Offering fewer different ways to instantiate nodes
* 2. Mandating source locations
*/
class Builders {
pos({
line,
column
}) {
return {
line,
column
};
}
blockItself({
body,
params,
chained = false,
loc
}) {
return {
type: 'Block',
body,
params,
get blockParams() {
return this.params.map(p => p.name);
},
set blockParams(params) {
this.params = params.map(name => {
return b.var({
name,
loc: SourceSpan.synthetic(name)
});
});
},
chained,
loc
};
}
template({
body,
blockParams,
loc
}) {
return {
type: 'Template',
body,
blockParams,
loc
};
}
mustache({
path,
params,
hash,
trusting,
loc,
strip = DEFAULT_STRIP
}) {
return buildLegacyMustache({
path,
params,
hash,
trusting,
strip,
loc
});
}
block({
path,
params,
hash,
defaultBlock,
elseBlock = null,
loc,
openStrip = DEFAULT_STRIP,
inverseStrip = DEFAULT_STRIP,
closeStrip = DEFAULT_STRIP
}) {
return {
type: 'BlockStatement',
path: path,
params,
hash,
program: defaultBlock,
inverse: elseBlock,
loc,
openStrip,
inverseStrip,
closeStrip
};
}
comment({
value,
loc
}) {
return {
type: 'CommentStatement',
value,
loc
};
}
mustacheComment({
value,
loc
}) {
return {
type: 'MustacheCommentStatement',
value,
loc
};
}
concat({
parts,
loc
}) {
return {
type: 'ConcatStatement',
parts,
loc
};
}
element({
path,
selfClosing,
attributes,
modifiers,
params,
comments,
children,
openTag,
closeTag,
loc
}) {
let _selfClosing = selfClosing;
return {
type: 'ElementNode',
path,
attributes,
modifiers,
params,
comments,
children,
openTag,
closeTag,
loc,
get tag() {
return this.path.original;
},
set tag(name) {
this.path.original = name;
},
get blockParams() {
return this.params.map(p => p.name);
},
set blockParams(params) {
this.params = params.map(name => {
return b.var({
name,
loc: SourceSpan.synthetic(name)
});
});
},
get selfClosing() {
return _selfClosing;
},
set selfClosing(selfClosing) {
_selfClosing = selfClosing;
if (selfClosing) {
this.closeTag = null;
} else {
this.closeTag = SourceSpan.synthetic(`</${this.tag}>`);
}
}
};
}
elementModifier({
path,
params,
hash,
loc
}) {
return {
type: 'ElementModifierStatement',
path,
params,
hash,
loc
};
}
attr({
name,
value,
loc
}) {
return {
type: 'AttrNode',
name: name,
value: value,
loc
};
}
text({
chars,
loc
}) {
return {
type: 'TextNode',
chars,
loc
};
}
sexpr({
path,
params,
hash,
loc
}) {
return {
type: 'SubExpression',
path,
params,
hash,
loc
};
}
path({
head,
tail,
loc
}) {
return buildLegacyPath({
head,
tail,
loc
});
}
head({
original,
loc
}) {
if (original === 'this') {
return this.this({
loc
});
}
if (original[0] === '@') {
return this.atName({
name: original,
loc
});
} else {
return this.var({
name: original,
loc
});
}
}
this({
loc
}) {
return {
type: 'ThisHead',
get original() {
return 'this';
},
loc
};
}
atName({
name,
loc
}) {
let _name = '';
const node = {
type: 'AtHead',
get name() {
return _name;
},
set name(value) {
assert(value[0] === '@');
assert(value.indexOf('.') === -1);
_name = value;
},
get original() {
return this.name;
},
set original(value) {
this.name = value;
},
loc
};
// trigger the assertions
node.name = name;
return node;
}
var({
name,
loc
}) {
let _name = '';
const node = {
type: 'VarHead',
get name() {
return _name;
},
set name(value) {
assert(value[0] !== '@');
assert(value.indexOf('.') === -1);
_name = value;
},
get original() {
return this.name;
},
set original(value) {
this.name = value;
},
loc
};
// trigger the assertions
node.name = name;
return node;
}
hash({
pairs,
loc
}) {
return {
type: 'Hash',
pairs,
loc
};
}
pair({
key,
value,
loc
}) {
return {
type: 'HashPair',
key,
value,
loc
};
}
literal({
type,
value,
loc
}) {
return buildLegacyLiteral({
type,
value,
loc
});
}
}
const b = new Builders();
class Parser {
elementStack = [];
lines;
source;
currentAttribute = null;
currentNode = null;
tokenizer;
constructor(source, entityParser = new EntityParser(namedCharRefs), mode = 'precompile') {
this.source = source;
this.lines = source.source.split(/\r\n?|\n/u);
this.tokenizer = new EventedTokenizer(this, entityParser, mode);
}
offset() {
let {
line,
column
} = this.tokenizer;
return this.source.offsetFor(line, column);
}
pos({
line,
column
}) {
return this.source.offsetFor(line, column);
}
finish(node) {
return assign({}, node, {
loc: node.start.until(this.offset())
});
// node.loc = node.loc.withEnd(end);
}
get currentAttr() {
return expect(this.currentAttribute);
}
get currentTag() {
let node = this.currentNode;
assert(node && (node.type === 'StartTag' || node.type === 'EndTag'));
return node;
}
get currentStartTag() {
let node = this.currentNode;
assert(node && node.type === 'StartTag');
return node;
}
get currentEndTag() {
let node = this.currentNode;
assert(node && node.type === 'EndTag');
return node;
}
get currentComment() {
let node = this.currentNode;
assert(node && node.type === 'CommentStatement');
return node;
}
get currentData() {
let node = this.currentNode;
assert(node && node.type === 'TextNode');
return node;
}
acceptNode(node) {
return this[node.type](node);
}
currentElement() {
return getLast(asPresentArray(this.elementStack));
}
sourceForNode(node, endNode) {
let firstLine = node.loc.start.line - 1;
let currentLine = firstLine - 1;
let firstColumn = node.loc.start.column;
let string = [];
let line;
let lastLine;
let lastColumn;
if (endNode) {
lastLine = endNode.loc.end.line - 1;
lastColumn = endNode.loc.end.column;
} else {
lastLine = node.loc.end.line - 1;
lastColumn = node.loc.end.column;
}
while (currentLine < lastLine) {
currentLine++;
line = unwrap(this.lines[currentLine]);
if (currentLine === firstLine) {
if (firstLine === lastLine) {
string.push(line.slice(firstColumn, lastColumn));
} else {
string.push(line.slice(firstColumn));
}
} else if (currentLine === lastLine) {
string.push(line.slice(0, lastColumn));
} else {
string.push(line);
}
}
return string.join('\n');
}
}
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName';
const ATTRIBUTE_VALUE_UNQUOTED = 'attributeValueUnquoted';
class HandlebarsNodeVisitors extends Parser {
// Because we interleave the HTML and HBS parsing, sometimes the HTML
// tokenizer can run out of tokens when we switch into {{...}} or reached
// EOF. There are positions where neither of these are expected, and it would
// like to generate an error, but there is no span to attach the error to.
// This allows the HTML tokenization to stash an error message and the next
// mustache visitor will attach the message to the appropriate span and throw
// the error.
pendingError = null;
parse(program, blockParams) {
assert(program.loc);
let node = b.template({
body: [],
blockParams,
loc: this.source.spanFor(program.loc)
});
let template = this.parseProgram(node, program);
// TODO: we really need to verify that the tokenizer is in an acceptable
// state when we are "done" parsing. For example, right now, `<foo` parses
// into `Template { body: [] }` which is obviously incorrect
this.pendingError?.eof(template.loc.getEnd());
return template;
}
Program(program, blockParams) {
assert(program.loc);
let node = b.blockItself({
body: [],
params: blockParams,
chained: program.chained,
loc: this.source.spanFor(program.loc)
});
return this.parseProgram(node, program);
}
parseProgram(node, program) {
if (program.body.length === 0) {
return node;
}
let poppedNode;
try {
this.elementStack.push(node);
for (let child of program.body) {
this.acceptNode(child);
}
} finally {
poppedNode = this.elementStack.pop();
}
// Ensure that that the element stack is balanced properly.
if (node !== poppedNode) {
if (poppedNode?.type === 'ElementNode') {
throw generateSyntaxError(`Unclosed element \`${poppedNode.tag}\``, poppedNode.loc);
} else {
assert(false, `[BUG] mismatched parser elementStack node: ${node.type}`);
}
}
return node;
}
BlockStatement(block) {
if (this.tokenizer.state === 'comment') {
assert(block.loc);
this.appendToCommentData(this.sourceForNode(block));
return;
}
if (this.tokenizer.state !== 'data' && this.tokenizer.state !== 'beforeData') {
throw generateSyntaxError('A block may only be used inside an HTML element or another block.', this.source.spanFor(block.loc));
}
const {
path,
params,
hash
} = acceptCallNodes(this, block);
const loc = this.source.spanFor(block.loc);
// Backfill block params loc for the default block
let blockParams = [];
let repairedBlock;
if (block.program.blockParams?.length) {
// Start from right after the hash
let span = hash.loc.collapse('end');
// Extend till the beginning of the block
if (block.program.loc) {
span = span.withEnd(this.source.spanFor(block.program.loc).getStart());
} else if (block.program.body[0]) {
span = span.withEnd(this.source.spanFor(block.program.body[0].loc).getStart());
} else {
// ...or if all else fail, use the end of the block statement
// this can only happen if the block statement is empty anyway
span = span.withEnd(loc.getEnd());
}
repairedBlock = repairBlock(this.source, block, span);
// Now we have a span for something like this:
//
// {{#foo bar baz=bat as |wow wat|}}
// ~~~~~~~~~~~~~~~
//
// Or, if we are unlucky:
//
// {{#foo bar baz=bat as |wow wat|}}{{/foo}}
// ~~~~~~~~~~~~~~~~~~~~~~~
//
// Either way, within this span, there should be exactly two pipes
// fencing our block params, neatly whitespace separated and with
// legal identifiers only
const content = span.asString();
let skipStart = content.indexOf('|') + 1;
const limit = content.indexOf('|', skipStart);
for (const name of block.program.blockParams) {
let nameStart;
let loc;
if (skipStart >= limit) {
nameStart = -1;
} else {
nameStart = content.indexOf(name, skipStart);
}
if (nameStart === -1 || nameStart + name.length > limit) {
skipStart = limit;
loc = this.source.spanFor(NON_EXISTENT_LOCATION);
} else {
skipStart = nameStart;
loc = span.sliceStartChars({
skipStart,
chars: name.length
});
skipStart += name.length;
}
blockParams.push(b.var({
name,
loc
}));
}
} else {
repairedBlock = repairBlock(this.source, block, loc);
}
const program = this.Program(repairedBlock.program, blockParams);
const inverse = repairedBlock.inverse ? this.Program(repairedBlock.inverse, []) : null;
const node = b.block({
path,
params,
hash,
defaultBlock: program,
elseBlock: inverse,
loc: this.source.spanFor(block.loc),
openStrip: block.openStrip,
inverseStrip: block.inverseStrip,
closeStrip: block.closeStrip
});
const parentProgram = this.currentElement();
appendChild(parentProgram, node);
}
MustacheStatement(rawMustache) {
this.pendingError?.mustache(this.source.spanFor(rawMustache.loc));
const {
tokenizer
} = this;
if (tokenizer.state === 'comment') {
this.appendToCommentData(this.sourceForNode(rawMustache));
return;
}
let mustache;
const {
escaped,
loc,
strip
} = rawMustache;
if ('original' in rawMustache.path && rawMustache.path.original === '...attributes') {
throw generateSyntaxError('Illegal use of ...attributes', this.source.spanFor(rawMustache.loc));
}
if (isHBSLiteral(rawMustache.path)) {
mustache = b.mustache({
path: this.acceptNode(rawMustache.path),
params: [],
hash: b.hash({
pairs: [],
loc: this.source.spanFor(rawMustache.path.loc).collapse('end')
}),
trusting: !escaped,
loc: this.source.spanFor(loc),
strip
});
} else {
const {
path,
params,
hash
} = acceptCallNodes(this, rawMustache);
mustache = b.mustache({
path,
params,
hash,
trusting: !escaped,
loc: this.source.spanFor(loc),
strip
});
}
switch (tokenizer.state) {
// Tag helpers
case 'tagOpen':
case 'tagName':
throw generateSyntaxError(`Cannot use mustaches in an elements tagname`, mustache.loc);
case 'beforeAttributeName':
addElementModifier(this.currentStartTag, mustache);
break;
case 'attributeName':
case 'afterAttributeName':
this.beginAttributeValue(false);
this.finishAttributeValue();
addElementModifier(this.currentStartTag, mustache);
tokenizer.transitionTo(BEFORE_ATTRIBUTE_NAME);
break;
case 'afterAttributeValueQuoted':
addElementModifier(this.currentStartTag, mustache);
tokenizer.transitionTo(BEFORE_ATTRIBUTE_NAME);
break;
// Attribute values
case 'beforeAttributeValue':
this.beginAttributeValue(false);
this.appendDynamicAttributeValuePart(mustache);
tokenizer.transitionTo(ATTRIBUTE_VALUE_UNQUOTED);
break;
case 'attributeValueDoubleQuoted':
case 'attributeValueSingleQuoted':
case 'attributeValueUnquoted':
this.appendDynamicAttributeValuePart(mustache);
break;
// TODO: Only append child when the tokenizer state makes
// sense to do so, otherwise throw an error.
default:
appendChild(this.currentElement(), mustache);
}
return mustache;
}
appendDynamicAttributeValuePart(part) {
this.finalizeTextPart();
const attr = this.currentAttr;
attr.isDynamic = true;
attr.parts.push(part);
}
finalizeTextPart() {
const attr = this.currentAttr;
const text = attr.currentPart;
if (text !== null) {
this.currentAttr.parts.push(text);
this.startTextPart();
}
}
startTextPart() {
this.currentAttr.currentPart = null;
}
ContentStatement(content) {
updateTokenizerLocation(this.tokenizer, content);
this.tokenizer.tokenizePart(content.value);
this.tokenizer.flushData();
}
CommentStatement(rawComment) {
const {
tokenizer
} = this;
if (tokenizer.state === 'comment') {
this.appendToCommentData(this.sourceForNode(rawComment));
return null;
}
const {
value,
loc
} = rawComment;
const comment = b.mustacheComment({
value,
loc: this.source.spanFor(loc)
});
switch (tokenizer.state) {
case 'beforeAttributeName':
case 'afterAttributeName':
this.currentStartTag.comments.push(comment);
break;
case 'beforeData':
case 'data':
appendChild(this.currentElement(), comment);
break;
default:
throw generateSyntaxError(`Using a Handlebars comment when in the \`${tokenizer['state']}\` state is not supported`, this.source.spanFor(rawComment.loc));
}
return comment;
}
PartialStatement(partial) {
throw generateSyntaxError(`Handlebars partials are not supported`, this.source.spanFor(partial.loc));
}
PartialBlockStatement(partialBlock) {
throw generateSyntaxError(`Handlebars partial blocks are not supported`, this.source.spanFor(partialBlock.loc));
}
Decorator(decorator) {
throw generateSyntaxError(`Handlebars decorators are not supported`, this.source.spanFor(decorator.loc));
}
DecoratorBlock(decoratorBlock) {
throw generateSyntaxError(`Handlebars decorator blocks are not supported`, this.source.spanFor(decoratorBlock.loc));
}
SubExpression(sexpr) {
const {
path,
params,
hash
} = acceptCallNodes(this, sexpr);
return b.sexpr({
path,
params,
hash,
loc: this.source.spanFor(sexpr.loc)
});
}
PathExpression(path) {
const {
original
} = path;
let parts;
if (original.indexOf('/') !== -1) {
if (original.slice(0, 2) === './') {
throw generateSyntaxError(`Using "./" is not supported in Glimmer and unnecessary`, this.source.spanFor(path.loc));
}
if (original.slice(0, 3) === '../') {
throw generateSyntaxError(`Changing context using "../" is not supported in Glimmer`, this.source.spanFor(path.loc));
}
if (original.indexOf('.') !== -1) {
throw generateSyntaxError(`Mixing '.' and '/' in paths is not supported in Glimmer; use only '.' to separate property paths`, this.source.spanFor(path.loc));
}
parts = [path.parts.join('/')];
} else if (original === '.') {
throw generateSyntaxError(`'.' is not a supported path in Glimmer; check for a path with a trailing '.'`, this.source.spanFor(path.loc));
} else {
parts = path.parts;
}
let thisHead = false;
// This is to fix a bug in the Handlebars AST where the path expressions in
// `{{this.foo}}` (and similarly `{{foo-bar this.foo named=this.foo}}` etc)
// are simply turned into `{{foo}}`. The fix is to push it back onto the
// parts array and let the runtime see the difference. However, we cannot
// simply use the string `this` as it means literally the property called
// "this" in the current context (it can be expressed in the syntax as
// `{{[this]}}`, where the square bracket are generally for this kind of
// escaping – such as `{{foo.["bar.baz"]}}` would mean lookup a property
// named literally "bar.baz" on `this.foo`). By convention, we use `null`
// for this purpose.
if (/^this(?:\..+)?$/u.test(original)) {
thisHead = true;
}
let pathHead;
if (thisHead) {
pathHead = b.this({
loc: this.source.spanFor({
start: path.loc.start,
end: {
line: path.loc.start.line,
column: path.loc.start.column + 4
}
})
});
} else if (path.data) {
const head = parts.shift();
if (head === undefined) {
throw generateSyntaxError(`Attempted to parse a path expression, but it was not valid. Paths beginning with @ must start with a-z.`, this.source.spanFor(path.loc));
}
pathHead = b.atName({
name: `@${head}`,
loc: this.source.spanFor({
start: path.loc.start,
end: {
line: path.loc.start.line,
column: path.loc.start.column + head.length + 1
}
})
});
} else {
const head = parts.shift();
if (head === undefined) {
throw generateSyntaxError(`Attempted to parse a path expression, but it was not valid. Paths must start with a-z or A-Z.`, this.source.spanFor(path.loc));
}
pathHead = b.var({
name: head,
loc: this.source.spanFor({
start: path.loc.start,
end: {
line: path.loc.start.line,
column: path.loc.start.column + head.length
}
})
});
}
return b.path({
head: pathHead,
tail: parts,
loc: this.source.spanFor(path.loc)
});
}
Hash(hash) {
const pairs = hash.pairs.map(pair => b.pair({
key: pair.key,
value: this.acceptNode(pair.value),
loc: this.source.spanFor(pair.loc)
}));
return b.hash({
pairs,
loc: this.source.spanFor(hash.loc)
});
}
StringLiteral(string) {
return b.literal({
type: 'StringLiteral',
value: string.value,
loc: this.source.spanFor(string.loc)
});
}
BooleanLiteral(boolean) {
return b.literal({
type: 'BooleanLiteral',
value: boolean.value,
loc: this.source.spanFor(boolean.loc)
});
}
NumberLiteral(number) {
return b.literal({
type: 'NumberLiteral',
value: number.value,
loc: this.source.spanFor(number.loc)
});
}
UndefinedLiteral(undef) {
return b.literal({
type: 'UndefinedLiteral',
value: undefined,
loc: this.source.spanFor(undef.loc)
});
}
NullLiteral(nul) {
return b.literal({
type: 'NullLiteral',
value: null,
loc: this.source.spanFor(nul.loc)
});
}
}
function calculateRightStrippedOffsets(original, value) {
if (value === '') {
// if it is empty, just return the count of newlines
// in original
return {
lines: original.split('\n').length - 1,
columns: 0
};
}
// otherwise, return the number of newlines prior to
// `value`
const [difference] = original.split(value);
const lines = difference.split(/\n/u);
const lineCount = lines.length - 1;
return {
lines: lineCount,
columns: unwrap(lines[lineCount]).length
};
}
function updateTokenizerLocation(tokenizer, content) {
let line = content.loc.start.line;
let column = content.loc.start.column;
const offsets = calculateRightStrippedOffsets(content.original, content.value);
line = line + offsets.lines;
if (offsets.lines) {
column = offsets.columns;
} else {
column = column + offsets.columns;
}
tokenizer.line = line;
tokenizer.column = column;
}
function acceptCallNodes(compiler, node) {
let path;
switch (node.path.type) {
case 'PathExpression':
path = compiler.PathExpression(node.path);
break;
case 'SubExpression':
path = compiler.SubExpression(node.path);
break;
case 'StringLiteral':
case 'UndefinedLiteral':
case 'NullLiteral':
case 'NumberLiteral':
case 'BooleanLiteral':
{
let value;
if (node.path.type === 'BooleanLiteral') {
value = node.path.original.toString();
} else if (node.path.type === 'StringLiteral') {
value = `"${node.path.original}"`;
} else if (node.path.type === 'NullLiteral') {
value = 'null';
} else if (node.path.type === 'NumberLiteral') {
value = node.path.value.toString();
} else {
value = 'undefined';
}
throw generateSyntaxError(`${node.path.type} "${node.path.type === 'StringLiteral' ? node.path.original : value}" cannot be called as a sub-expression, replace (${value}) with ${value}`, compiler.source.spanFor(node.path.loc));
}
}
const params = node.params.map(e => compiler.acceptNode(e));
// if there is no hash, position it as a collapsed node immediately after the last param (or the
// path, if there are also no params)
const end = isPresentArray(params) ? getLast(params).loc : path.loc;
const hash = node.hash ? compiler.Hash(node.hash) : b.hash({
pairs: [],
loc: compiler.source.spanFor(end).collapse('end')
});
return {
path,
params,
hash
};
}
function addElementModifier(element, mustache) {
const {
path,
params,
hash,
loc
} = mustache;
if (isHBSLiteral(path)) {
const modifier = `{{${printLiteral(path)}}}`;
const tag = `<${element.name} ... ${modifier} ...`;
throw generateSyntaxError(`In ${tag}, ${m