UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

1,963 lines (1,917 loc) 185 kB
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