UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

2,056 lines (2,011 loc) 191 kB
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 '&nbsp;'; case Char.QUOT: return '&quot;'; case Char.AMP: return '&amp;'; default: return char; } } function textReplacer(char) { switch (char.charCodeAt(0)) { case Char.NBSP: return '&nbsp;'; case Char.AMP: return '&amp;'; case Char.LT: return '&lt;'; case Char.GT: return '&gt;'; default: return char; } } function escapeAttrValue(attrValue) { if (ATTR_VALUE_REGEX_TEST.test(attrValue)) { return attrValue.replace(ATTR_VALUE_REGEX_REPLACE, attrValueReplacer); } return attrValue; } function escapeText(text) { if (TEXT_REGEX_TEST.test(text)) { return text.replace(TEXT_REGEX_REPLACE, textReplacer); } return text; } function sortByLoc(a, b) { // If either is invisible, don't try to order them if (a.loc.isInvisible || b.loc.isInvisible) { return 0; } if (a.loc.startPosition.line < b.loc.startPosition.line) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column < b.loc.startPosition.column) { return -1; } if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column === b.loc.startPosition.column) { return 0; } return 1; } const voidMap = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']); const NON_WHITESPACE = /^\S/u; /** * Examples when true: * - link * - liNK * * Examples when false: * - Link (component) */ function isVoidTag(tag) { return voidMap.has(tag.toLowerCase()) && tag[0]?.toLowerCase() === tag[0]; } class Printer { 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