ember-source
Version:
A JavaScript framework for creating ambitious web applications
2,056 lines (2,011 loc) • 191 kB
JavaScript
import { S as SourceOffset, a as SourceSpan, c as cannotReplaceOrRemoveInKeyHandlerYet, b as cannotRemoveNode, d as cannotReplaceNode, e as SYNTHETIC_LOCATION, E as EntityParser, f as EventedTokenizer, n as namedCharRefs, N as NON_EXISTENT_LOCATION, p as parseWithoutProcessing, g as parse, h as STRICT_RESOLUTION, T as Template$1, B as Block, i as NamedBlock$1, j as SourceSlice, A as Args$1, P as PositionalArguments, k as NamedArgument$1, l as NamedArguments$1, H as HtmlAttr, m as SplatAttr$1, C as ComponentArg, o as PathExpression$1, K as KeywordExpression, q as ThisReference, r as ArgReference, F as FreeVarReference, L as LocalVarReference, s as CallExpression$1, I as InterpolateExpression$1, t as LiteralExpression, u as AppendContent, v as ElementModifier, w as NamedBlocks$1, x as InvokeBlock$1, y as SimpleElement$1, z as InvokeComponent$1, D as SpanList, G as LooseModeResolution, J as COMPONENT_NAMESPACE, M as MODIFIER_NAMESPACE, O as HELPER_NAMESPACE, Q as HtmlText, R as HtmlComment, U as GlimmerComment, V as node, W as isLiteral$1, X as maybeLoc, Y as loc } from './transform-resolutions-C7wq_Q_c.js';
import { s as setLocalDebugType } from './debug-brand-B1TWjOCH.js';
import { SexpOpcodes as opcodes, WellKnownTagNames, WellKnownAttrNames } from '../@glimmer/wire-format/index.js';
import { a as assert } from './assert-CUCJBR2C.js';
import { u as unwrap, a as isPresentArray, f as asPresentArray, e as expect, b as getLast, g as getFirst, d as dict, m as mapPresentArray, h as exhausted } from './collections-GpG8lT2g.js';
import { a as assign } from './object-utils-AijlD-JH.js';
import { a4 as CURRIED_MODIFIER, au as CURRIED_HELPER, C as CURRIED_COMPONENT } from './fragment-EpVz5Xuc.js';
import '../@glimmer/validator/index.js';
import './reference-BNqcwZWH.js';
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 {
buffer = '';
options;
constructor(options) {
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.buffer += block.closeStrip.close ? '~}}' : '}}';
}
}
BlockParams(blockParams) {
this.buffer += ` as |${blockParams.join(' ')}|`;
}
ConcatStatement(concat) {
if (this.handledByOverride(concat)) {
return;
}
this.buffer += '"';
concat.parts.forEach(part => {
if (part.type === 'TextNode') {
this.TextNode(part, '"');
} else {
this.Node(part);
}
});
this.buffer += '"';
}
MustacheCommentStatement(comment) {
if (this.handledByOverride(comment)) {
return;
}
this.buffer += `{{!--${comment.value}--}}`;
}
ElementModifierStatement(mod) {
if (this.handledByOverride(mod)) {
return;
}
this.buffer += '{{';
this.Expression(mod.path);
this.Params(mod.params);
this.Hash(mod.hash);
this.buffer += '}}';
}
CommentStatement(comment) {
if (this.handledByOverride(comment)) {
return;
}
this.buffer += `<!--${comment.value}-->`;
}
PathExpression(path) {
if (this.handledByOverride(path)) {
return;
}
this.buffer += path.original;
}
SubExpression(sexp) {
if (this.handledByOverride(sexp)) {
return;
}
this.buffer += '(';
this.Expression(sexp.path);
this.Params(sexp.params);
this.Hash(sexp.hash);
this.buffer += ')';
}
Params(params) {
// TODO: implement a top level Params AST node (just like the Hash object)
// so that this can also be overridden
if (params.length) {
params.forEach(param => {
this.buffer += ' ';
this.Expression(param);
});
}
}
Hash(hash) {
if (this.handledByOverride(hash, true)) {
return;
}
hash.pairs.forEach(pair => {
this.buffer += ' ';
this.HashPair(pair);
});
}
HashPair(pair) {
if (this.handledByOverride(pair)) {
return;
}
this.buffer += pair.key;
this.buffer += '=';
this.Node(pair.value);
}
StringLiteral(str) {
if (this.handledByOverride(str)) {
return;
}
this.buffer += JSON.stringify(str.value);
}
BooleanLiteral(bool) {
if (this.handledByOverride(bool)) {
return;
}
this.buffer += String(bool.value);
}
NumberLiteral(number) {
if (this.handledByOverride(number)) {
return;
}
this.buffer += String(number.value);
}
UndefinedLiteral(node) {
if (this.handledByOverride(node)) {
return;
}
this.buffer += 'undefined';
}
NullLiteral(node) {
if (this.handledByOverride(node)) {
return;
}
this.buffer += 'null';
}
print(node) {
let {
options
} = this;
if (options.override) {
let result = options.override(node, options);
if (result !== undefined) {
return result;
}
}
this.buffer = '';
this.Node(node);
return this.buffer;
}
}
function build(ast, options = {
entityEncoding: 'transformed'
}) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS users
if (!ast) {
return '';
}
let printer = new Printer(options);
return printer.print(ast);
}
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;
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.pus