UNPKG

@glimmer/syntax

Version:
1,421 lines (1,315 loc) 163 kB
import { assertNever, assign, dict } from "@glimmer/util"; import { parseWithoutProcessing, parse } from "@handlebars/parser"; import { EntityParser, EventedTokenizer, HTML5NamedCharRefs } from "simple-html-tokenizer"; import { DEBUG } from "@glimmer/env"; import { SexpOpcodes } from "@glimmer/wire-format"; const ATTR_VALUE_REGEX_TEST = /["\x26\xa0]/u, ATTR_VALUE_REGEX_REPLACE = new RegExp(ATTR_VALUE_REGEX_TEST.source, "gu"), TEXT_REGEX_TEST = /[&<>\xa0]/u, TEXT_REGEX_REPLACE = new RegExp(TEXT_REGEX_TEST.source, "gu"); // \x26 is ampersand, \xa0 is non-breaking space function attrValueReplacer(char) { switch (char.charCodeAt(0)) { case 160: return "&nbsp;"; case 34: return "&quot;"; case 38: return "&amp;"; default: return char; } } function textReplacer(char) { switch (char.charCodeAt(0)) { case 160: return "&nbsp;"; case 38: return "&amp;"; case 60: return "&lt;"; case 62: return "&gt;"; default: return char; } } function sortByLoc(a, b) { // If either is invisible, don't try to order them return a.loc.isInvisible || b.loc.isInvisible ? 0 : a.loc.startPosition.line < b.loc.startPosition.line || a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column < b.loc.startPosition.column ? -1 : a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column === b.loc.startPosition.column ? 0 : 1; } const voidMap = new Set([ "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr" ]); function getVoidTags() { return [ ...voidMap ]; } const NON_WHITESPACE = /^\S/u; /** * Examples when true: * - link * - liNK * * Examples when false: * - Link (component) */ function isVoidTag(tag) { return voidMap.has(tag.toLowerCase()) && tag[0]?.toLowerCase() === tag[0]; } class Printer { constructor(options) { this.buffer = "", this.options = options; } /* This is used by _all_ methods on this Printer class that add to `this.buffer`, it allows consumers of the printer to use alternate string representations for a given node. The primary use case for this are things like source -> source codemod utilities. For example, ember-template-recast attempts to always preserve the original string formatting in each AST node if no modifications are made to it. */ handledByOverride(node, ensureLeadingWhitespace = !1) { if (void 0 !== this.options.override) { let result = this.options.override(node, this.options); if ("string" == typeof result) return ensureLeadingWhitespace && NON_WHITESPACE.test(result) && (result = ` ${result}`), this.buffer += result, !0; } return !1; } 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`. */ block.chained && (block.body[0].chained = !0), this.handledByOverride(block) || this.TopLevelStatements(block.body); } TopLevelStatements(statements) { statements.forEach((statement => this.TopLevelStatement(statement))); } ElementNode(el) { this.handledByOverride(el) || (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) switch (this.buffer += " ", part.type) { case "AttrNode": this.AttrNode(part); break; case "ElementModifierStatement": this.ElementModifierStatement(part); break; case "MustacheCommentStatement": this.MustacheCommentStatement(part); } el.blockParams.length && this.BlockParams(el.blockParams), el.selfClosing && (this.buffer += " /"), this.buffer += ">"; } CloseElementNode(el) { el.selfClosing || isVoidTag(el.tag) || (this.buffer += `</${el.tag}>`); } AttrNode(attr) { if (this.handledByOverride(attr)) return; let {name: name, value: value} = attr; this.buffer += name, ("TextNode" !== value.type || value.chars.length > 0) && (this.buffer += "=", this.AttrNodeValue(value)); } AttrNodeValue(value) { "TextNode" === value.type ? (this.buffer += '"', this.TextNode(value, !0), this.buffer += '"') : this.Node(value); } TextNode(text, isAttr) { var attrValue; this.handledByOverride(text) || ("raw" === this.options.entityEncoding ? this.buffer += text.chars : this.buffer += isAttr ? (attrValue = text.chars, ATTR_VALUE_REGEX_TEST.test(attrValue) ? attrValue.replace(ATTR_VALUE_REGEX_REPLACE, attrValueReplacer) : attrValue) : function(text) { return TEXT_REGEX_TEST.test(text) ? text.replace(TEXT_REGEX_REPLACE, textReplacer) : text; }(text.chars)); } MustacheStatement(mustache) { this.handledByOverride(mustache) || (this.buffer += mustache.trusting ? "{{{" : "{{", mustache.strip.open && (this.buffer += "~"), this.Expression(mustache.path), this.Params(mustache.params), this.Hash(mustache.hash), mustache.strip.close && (this.buffer += "~"), this.buffer += mustache.trusting ? "}}}" : "}}"); } BlockStatement(block) { this.handledByOverride(block) || (block.chained ? (this.buffer += block.inverseStrip.open ? "{{~" : "{{", this.buffer += "else ") : this.buffer += block.openStrip.open ? "{{~#" : "{{#", this.Expression(block.path), this.Params(block.params), this.Hash(block.hash), block.program.blockParams.length && this.BlockParams(block.program.blockParams), block.chained ? this.buffer += block.inverseStrip.close ? "~}}" : "}}" : this.buffer += block.openStrip.close ? "~}}" : "}}", this.Block(block.program), block.inverse && (block.inverse.chained || (this.buffer += block.inverseStrip.open ? "{{~" : "{{", this.buffer += "else", this.buffer += block.inverseStrip.close ? "~}}" : "}}"), this.Block(block.inverse)), 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) { this.handledByOverride(concat) || (this.buffer += '"', concat.parts.forEach((part => { "TextNode" === part.type ? this.TextNode(part, !0) : this.Node(part); })), this.buffer += '"'); } MustacheCommentStatement(comment) { this.handledByOverride(comment) || (this.buffer += `{{!--${comment.value}--}}`); } ElementModifierStatement(mod) { this.handledByOverride(mod) || (this.buffer += "{{", this.Expression(mod.path), this.Params(mod.params), this.Hash(mod.hash), this.buffer += "}}"); } CommentStatement(comment) { this.handledByOverride(comment) || (this.buffer += `\x3c!--${comment.value}--\x3e`); } PathExpression(path) { this.handledByOverride(path) || (this.buffer += path.original); } SubExpression(sexp) { this.handledByOverride(sexp) || (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 params.length && params.forEach((param => { this.buffer += " ", this.Expression(param); })); } Hash(hash) { this.handledByOverride(hash, !0) || hash.pairs.forEach((pair => { this.buffer += " ", this.HashPair(pair); })); } HashPair(pair) { this.handledByOverride(pair) || (this.buffer += pair.key, this.buffer += "=", this.Node(pair.value)); } StringLiteral(str) { this.handledByOverride(str) || (this.buffer += JSON.stringify(str.value)); } BooleanLiteral(bool) { this.handledByOverride(bool) || (this.buffer += String(bool.value)); } NumberLiteral(number) { this.handledByOverride(number) || (this.buffer += String(number.value)); } UndefinedLiteral(node) { this.handledByOverride(node) || (this.buffer += "undefined"); } NullLiteral(node) { this.handledByOverride(node) || (this.buffer += "null"); } print(node) { let {options: options} = this; if (options.override) { let result = options.override(node, options); if (void 0 !== result) return result; } return this.buffer = "", this.Node(node), this.buffer; } } function build(ast, options = { entityEncoding: "transformed" }) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS users return ast ? new Printer(options).print(ast) : ""; } function isKeyword(word, type) { return word in KEYWORDS_TYPES && (void 0 === type || KEYWORDS_TYPES[word].includes(type)); } /** * 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" ] }; // import Logger from './logger'; function isPresentArray(list) { return !!list && list.length > 0; } function getLast(list) { return 0 === list.length ? void 0 : list[list.length - 1]; } function getFirst(list) { return 0 === list.length ? void 0 : list[0]; } const UNKNOWN_POSITION = Object.freeze({ line: 1, column: 0 }), SYNTHETIC_LOCATION = Object.freeze({ source: "(synthetic)", start: UNKNOWN_POSITION, end: UNKNOWN_POSITION }), NON_EXISTENT_LOCATION = Object.freeze({ source: "(nonexistent)", start: UNKNOWN_POSITION, end: UNKNOWN_POSITION }), BROKEN_LOCATION = Object.freeze({ source: "(broken)", start: UNKNOWN_POSITION, end: UNKNOWN_POSITION }); class WhenList { constructor(whens) { this._whens = whens; } first(kind) { for (const when of this._whens) { const value = when.match(kind); if (isPresentArray(value)) return value[0]; } return null; } } class When { get(pattern, or) { let value = this._map.get(pattern); return value || (value = or(), this._map.set(pattern, value), value); } add(pattern, out) { this._map.set(pattern, out); } match(kind) { const pattern = function(kind) { switch (kind) { case "Broken": case "InternalsSynthetic": case "NonExistent": return "IS_INVISIBLE"; default: return kind; } }(kind), out = [], exact = this._map.get(pattern), fallback = this._map.get("MATCH_ANY"); return exact && out.push(exact), fallback && out.push(fallback), out; } constructor() { this._map = new Map; } } function match(callback) { return callback(new Matcher).validate(); } class Matcher { /** * You didn't exhaustively match all possibilities. */ validate() { return (left, right) => this.matchFor(left.kind, right.kind)(left, right); } matchFor(left, right) { const nesteds = this._whens.match(left); return isPresentArray(), new WhenList(nesteds).first(right); } when(left, right, // eslint-disable-next-line @typescript-eslint/no-explicit-any callback) { return this._whens.get(left, (() => new When)).add(right, callback), this; } constructor() { this._whens = new When; } } class SourceSlice { static synthetic(chars) { let offsets = SourceSpan.synthetic(chars); return new SourceSlice({ loc: offsets, chars: chars }); } static load(source, slice) { return new SourceSlice({ loc: SourceSpan.load(source, slice[1]), chars: slice[0] }); } constructor(options) { this.loc = options.loc, this.chars = options.chars; } getString() { return this.chars; } serialize() { return [ this.chars, this.loc.serialize() ]; } } /** * A `SourceSpan` object represents a span of characters inside of a template source. * * There are three kinds of `SourceSpan` objects: * * - `ConcreteSourceSpan`, which contains byte offsets * - `LazySourceSpan`, which contains `SourceLocation`s from the Handlebars AST, which can be * converted to byte offsets on demand. * - `InvisibleSourceSpan`, which represent source strings that aren't present in the source, * because: * - they were created synthetically * - their location is nonsensical (the span is broken) * - they represent nothing in the source (this currently happens only when a bug in the * upstream Handlebars parser fails to assign a location to empty blocks) * * At a high level, all `SourceSpan` objects provide: * * - byte offsets * - source in column and line format * * And you can do these operations on `SourceSpan`s: * * - collapse it to a `SourceSpan` representing its starting or ending position * - slice out some characters, optionally skipping some characters at the beginning or end * - create a new `SourceSpan` with a different starting or ending offset * * All SourceSpan objects implement `SourceLocation`, for compatibility. All SourceSpan * objects have a `toJSON` that emits `SourceLocation`, also for compatibility. * * For compatibility, subclasses of `AbstractSourceSpan` must implement `locDidUpdate`, which * happens when an AST plugin attempts to modify the `start` or `end` of a span directly. * * The goal is to avoid creating any problems for use-cases like AST Explorer. */ class SourceSpan { static get NON_EXISTENT() { return new InvisibleSpan("NonExistent", NON_EXISTENT_LOCATION).wrap(); } static load(source, serialized) { return "number" == typeof serialized ? SourceSpan.forCharPositions(source, serialized, serialized) : "string" == typeof serialized ? SourceSpan.synthetic(serialized) : Array.isArray(serialized) ? SourceSpan.forCharPositions(source, serialized[0], serialized[1]) : "NonExistent" === serialized ? SourceSpan.NON_EXISTENT : "Broken" === serialized ? SourceSpan.broken(BROKEN_LOCATION) : void assertNever(serialized); } static forHbsLoc(source, loc) { const start = new HbsPosition(source, loc.start), end = new HbsPosition(source, loc.end); return new HbsSpan(source, { start: start, end: end }, loc).wrap(); } static forCharPositions(source, startPos, endPos) { const start = new CharPosition(source, startPos), end = new CharPosition(source, endPos); return new CharPositionSpan(source, { start: start, end: end }).wrap(); } static synthetic(chars) { return new InvisibleSpan("InternalsSynthetic", NON_EXISTENT_LOCATION, chars).wrap(); } static broken(pos = BROKEN_LOCATION) { return new InvisibleSpan("Broken", pos).wrap(); } constructor(data) { var kind; /** * This file implements the DSL used by span and offset in places where they need to exhaustively * consider all combinations of states (Handlebars offsets, character offsets and invisible/broken * offsets). * * It's probably overkill, but it makes the code that uses it clear. It could be refactored or * removed. */ this.data = data, this.isInvisible = "CharPosition" !== (kind = data.kind) && "HbsPosition" !== kind; } getStart() { return this.data.getStart().wrap(); } getEnd() { return this.data.getEnd().wrap(); } get loc() { const span = this.data.toHbsSpan(); return null === span ? BROKEN_LOCATION : span.toHbsLoc(); } get module() { return this.data.getModule(); } /** * Get the starting `SourcePosition` for this `SourceSpan`, lazily computing it if needed. */ get startPosition() { return this.loc.start; } /** * Get the ending `SourcePosition` for this `SourceSpan`, lazily computing it if needed. */ get endPosition() { return this.loc.end; } /** * Support converting ASTv1 nodes into a serialized format using JSON.stringify. */ toJSON() { return this.loc; } /** * Create a new span with the current span's end and a new beginning. */ withStart(other) { return span(other.data, this.data.getEnd()); } /** * Create a new span with the current span's beginning and a new ending. */ withEnd(other) { return span(this.data.getStart(), other.data); } asString() { return this.data.asString(); } /** * Convert this `SourceSpan` into a `SourceSlice`. In debug mode, this method optionally checks * that the byte offsets represented by this `SourceSpan` actually correspond to the expected * string. */ toSlice(expected) { const chars = this.data.asString(); return DEBUG && void 0 !== expected && chars !== expected && // eslint-disable-next-line no-console console.warn(`unexpectedly found ${JSON.stringify(chars)} when slicing source, but expected ${JSON.stringify(expected)}`), new SourceSlice({ loc: this, chars: expected || chars }); } /** * For compatibility with SourceLocation in AST plugins * * @deprecated use startPosition instead */ get start() { return this.loc.start; } /** * For compatibility with SourceLocation in AST plugins * * @deprecated use withStart instead */ set start(position) { this.data.locDidUpdate({ start: position }); } /** * For compatibility with SourceLocation in AST plugins * * @deprecated use endPosition instead */ get end() { return this.loc.end; } /** * For compatibility with SourceLocation in AST plugins * * @deprecated use withEnd instead */ set end(position) { this.data.locDidUpdate({ end: position }); } /** * For compatibility with SourceLocation in AST plugins * * @deprecated use module instead */ get source() { return this.module; } collapse(where) { switch (where) { case "start": return this.getStart().collapsed(); case "end": return this.getEnd().collapsed(); } } extend(other) { return span(this.data.getStart(), other.data.getEnd()); } serialize() { return this.data.serialize(); } slice({skipStart: skipStart = 0, skipEnd: skipEnd = 0}) { return span(this.getStart().move(skipStart).data, this.getEnd().move(-skipEnd).data); } sliceStartChars({skipStart: skipStart = 0, chars: chars}) { return span(this.getStart().move(skipStart).data, this.getStart().move(skipStart + chars).data); } sliceEndChars({skipEnd: skipEnd = 0, chars: chars}) { return span(this.getEnd().move(skipEnd - chars).data, this.getStart().move(-skipEnd).data); } } class CharPositionSpan { #locPosSpan; constructor(source, charPositions) { this.source = source, this.charPositions = charPositions, this.kind = "CharPosition", this.#locPosSpan = null; } wrap() { return new SourceSpan(this); } asString() { return this.source.slice(this.charPositions.start.charPos, this.charPositions.end.charPos); } getModule() { return this.source.module; } getStart() { return this.charPositions.start; } getEnd() { return this.charPositions.end; } locDidUpdate() {} toHbsSpan() { let locPosSpan = this.#locPosSpan; if (null === locPosSpan) { const start = this.charPositions.start.toHbsPos(), end = this.charPositions.end.toHbsPos(); locPosSpan = this.#locPosSpan = null === start || null === end ? BROKEN : new HbsSpan(this.source, { start: start, end: end }); } return locPosSpan === BROKEN ? null : locPosSpan; } serialize() { const {start: {charPos: start}, end: {charPos: end}} = this.charPositions; return start === end ? start : [ start, end ]; } toCharPosSpan() { return this; } } class HbsSpan { #charPosSpan; // the source location from Handlebars + AST Plugins -- could be wrong #providedHbsLoc; constructor(source, hbsPositions, providedHbsLoc = null) { this.source = source, this.hbsPositions = hbsPositions, this.kind = "HbsPosition", this.#charPosSpan = null, this.#providedHbsLoc = providedHbsLoc; } serialize() { const charPos = this.toCharPosSpan(); return null === charPos ? "Broken" : charPos.wrap().serialize(); } wrap() { return new SourceSpan(this); } updateProvided(pos, edge) { this.#providedHbsLoc && (this.#providedHbsLoc[edge] = pos), // invalidate computed character offsets this.#charPosSpan = null, this.#providedHbsLoc = { start: pos, end: pos }; } locDidUpdate({start: start, end: end}) { void 0 !== start && (this.updateProvided(start, "start"), this.hbsPositions.start = new HbsPosition(this.source, start, null)), void 0 !== end && (this.updateProvided(end, "end"), this.hbsPositions.end = new HbsPosition(this.source, end, null)); } asString() { const span = this.toCharPosSpan(); return null === span ? "" : span.asString(); } getModule() { return this.source.module; } getStart() { return this.hbsPositions.start; } getEnd() { return this.hbsPositions.end; } toHbsLoc() { return { start: this.hbsPositions.start.hbsPos, end: this.hbsPositions.end.hbsPos }; } toHbsSpan() { return this; } toCharPosSpan() { let charPosSpan = this.#charPosSpan; if (null === charPosSpan) { const start = this.hbsPositions.start.toCharPos(), end = this.hbsPositions.end.toCharPos(); if (!start || !end) return charPosSpan = this.#charPosSpan = BROKEN, null; charPosSpan = this.#charPosSpan = new CharPositionSpan(this.source, { start: start, end: end }); } return charPosSpan === BROKEN ? null : charPosSpan; } } class InvisibleSpan { constructor(kind, // whatever was provided, possibly broken loc, // if the span represents a synthetic string string = null) { this.kind = kind, this.loc = loc, this.string = string; } serialize() { switch (this.kind) { case "Broken": case "NonExistent": return this.kind; case "InternalsSynthetic": return this.string || ""; } } wrap() { return new SourceSpan(this); } asString() { return this.string || ""; } locDidUpdate({start: start, end: end}) { void 0 !== start && (this.loc.start = start), void 0 !== end && (this.loc.end = end); } getModule() { // TODO: Make this reflect the actual module this span originated from return "an unknown module"; } getStart() { return new InvisiblePosition(this.kind, this.loc.start); } getEnd() { return new InvisiblePosition(this.kind, this.loc.end); } toCharPosSpan() { return this; } toHbsSpan() { return null; } toHbsLoc() { return BROKEN_LOCATION; } } const span = match((m => m.when("HbsPosition", "HbsPosition", ((left, right) => new HbsSpan(left.source, { start: left, end: right }).wrap())).when("CharPosition", "CharPosition", ((left, right) => new CharPositionSpan(left.source, { start: left, end: right }).wrap())).when("CharPosition", "HbsPosition", ((left, right) => { const rightCharPos = right.toCharPos(); return null === rightCharPos ? new InvisibleSpan("Broken", BROKEN_LOCATION).wrap() : span(left, rightCharPos); })).when("HbsPosition", "CharPosition", ((left, right) => { const leftCharPos = left.toCharPos(); return null === leftCharPos ? new InvisibleSpan("Broken", BROKEN_LOCATION).wrap() : span(leftCharPos, right); })).when("IS_INVISIBLE", "MATCH_ANY", (left => new InvisibleSpan(left.kind, BROKEN_LOCATION).wrap())).when("MATCH_ANY", "IS_INVISIBLE", ((_, right) => new InvisibleSpan(right.kind, BROKEN_LOCATION).wrap())))), BROKEN = "BROKEN"; /** * Used to indicate that an attempt to convert a `SourcePosition` to a character offset failed. It * is separate from `null` so that `null` can be used to indicate that the computation wasn't yet * attempted (and therefore to cache the failure) */ /** * A `SourceOffset` represents a single position in the source. * * There are three kinds of backing data for `SourceOffset` objects: * * - `CharPosition`, which contains a character offset into the raw source string * - `HbsPosition`, which contains a `SourcePosition` from the Handlebars AST, which can be * converted to a `CharPosition` on demand. * - `InvisiblePosition`, which represents a position not in source (@see {InvisiblePosition}) */ class SourceOffset { /** * Create a `SourceOffset` from a Handlebars `SourcePosition`. It's stored as-is, and converted * into a character offset on demand, which avoids unnecessarily computing the offset of every * `SourceLocation`, but also means that broken `SourcePosition`s are not always detected. */ static forHbsPos(source, pos) { return new HbsPosition(source, pos, null).wrap(); } /** * Create a `SourceOffset` that corresponds to a broken `SourcePosition`. This means that the * calling code determined (or knows) that the `SourceLocation` doesn't correspond correctly to * any part of the source. */ static broken(pos = UNKNOWN_POSITION) { return new InvisiblePosition("Broken", pos).wrap(); } constructor(data) { this.data = data; } /** * Get the character offset for this `SourceOffset`, if possible. */ get offset() { const charPos = this.data.toCharPos(); return null === charPos ? null : charPos.offset; } /** * Compare this offset with another one. * * If both offsets are `HbsPosition`s, they're equivalent as long as their lines and columns are * the same. This avoids computing offsets unnecessarily. * * Otherwise, two `SourceOffset`s are equivalent if their successfully computed character offsets * are the same. */ eql(right) { return eql(this.data, right.data); } /** * Create a span that starts from this source offset and ends with another source offset. Avoid * computing character offsets if both `SourceOffset`s are still lazy. */ until(other) { return span(this.data, other.data); } /** * Create a `SourceOffset` by moving the character position represented by this source offset * forward or backward (if `by` is negative), if possible. * * If this `SourceOffset` can't compute a valid character offset, `move` returns a broken offset. * * If the resulting character offset is less than 0 or greater than the size of the source, `move` * returns a broken offset. */ move(by) { const charPos = this.data.toCharPos(); if (null === charPos) return SourceOffset.broken(); { const result = charPos.offset + by; return charPos.source.validate(result) ? new CharPosition(charPos.source, result).wrap() : SourceOffset.broken(); } } /** * Create a new `SourceSpan` that represents a collapsed range at this source offset. Avoid * computing the character offset if it has not already been computed. */ collapsed() { return span(this.data, this.data); } /** * Convert this `SourceOffset` into a Handlebars {@see SourcePosition} for compatibility with * existing plugins. */ toJSON() { return this.data.toJSON(); } } class CharPosition { constructor(source, charPos) { this.source = source, this.charPos = charPos, this.kind = "CharPosition", this._locPos = null; } /** * This is already a `CharPosition`. * * {@see HbsPosition} for the alternative. */ toCharPos() { return this; } /** * Produce a Handlebars {@see SourcePosition} for this `CharPosition`. If this `CharPosition` was * computed using {@see SourceOffset#move}, this will compute the `SourcePosition` for the offset. */ toJSON() { const hbs = this.toHbsPos(); return null === hbs ? UNKNOWN_POSITION : hbs.toJSON(); } wrap() { return new SourceOffset(this); } /** * A `CharPosition` always has an offset it can produce without any additional computation. */ get offset() { return this.charPos; } /** * Convert the current character offset to an `HbsPosition`, if it was not already computed. Once * a `CharPosition` has computed its `HbsPosition`, it will not need to do compute it again, and * the same `CharPosition` is retained when used as one of the ends of a `SourceSpan`, so * computing the `HbsPosition` should be a one-time operation. */ toHbsPos() { let locPos = this._locPos; if (null === locPos) { const hbsPos = this.source.hbsPosFor(this.charPos); this._locPos = locPos = null === hbsPos ? BROKEN : new HbsPosition(this.source, hbsPos, this.charPos); } return locPos === BROKEN ? null : locPos; } } class HbsPosition { constructor(source, hbsPos, charPos = null) { this.source = source, this.hbsPos = hbsPos, this.kind = "HbsPosition", this._charPos = null === charPos ? null : new CharPosition(source, charPos); } /** * Lazily compute the character offset from the {@see SourcePosition}. Once an `HbsPosition` has * computed its `CharPosition`, it will not need to do compute it again, and the same * `HbsPosition` is retained when used as one of the ends of a `SourceSpan`, so computing the * `CharPosition` should be a one-time operation. */ toCharPos() { let charPos = this._charPos; if (null === charPos) { const charPosNumber = this.source.charPosFor(this.hbsPos); this._charPos = charPos = null === charPosNumber ? BROKEN : new CharPosition(this.source, charPosNumber); } return charPos === BROKEN ? null : charPos; } /** * Return the {@see SourcePosition} that this `HbsPosition` was instantiated with. This operation * does not need to compute anything. */ toJSON() { return this.hbsPos; } wrap() { return new SourceOffset(this); } /** * This is already an `HbsPosition`. * * {@see CharPosition} for the alternative. */ toHbsPos() { return this; } } class InvisiblePosition { constructor(kind, // whatever was provided, possibly broken pos) { this.kind = kind, this.pos = pos; } /** * A broken position cannot be turned into a {@see CharacterPosition}. */ toCharPos() { return null; } /** * The serialization of an `InvisiblePosition is whatever Handlebars {@see SourcePosition} was * originally identified as broken, non-existent or synthetic. * * If an `InvisiblePosition` never had an source offset at all, this method returns * {@see UNKNOWN_POSITION} for compatibility. */ toJSON() { return this.pos; } wrap() { return new SourceOffset(this); } get offset() { return null; } } /** * Compare two {@see AnyPosition} and determine whether they are equal. * * @see {SourceOffset#eql} */ const eql = match((m => m.when("HbsPosition", "HbsPosition", (({hbsPos: left}, {hbsPos: right}) => left.column === right.column && left.line === right.line)).when("CharPosition", "CharPosition", (({charPos: left}, {charPos: right}) => left === right)).when("CharPosition", "HbsPosition", (({offset: left}, right) => left === right.toCharPos()?.offset)).when("HbsPosition", "CharPosition", ((left, {offset: right}) => left.toCharPos()?.offset === right)).when("MATCH_ANY", "MATCH_ANY", (() => !1)))); 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; } /** * 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: line, column: column }); } spanFor({start: start, end: end}) { return SourceSpan.forHbsLoc(this, { start: { line: start.line, column: start.column }, end: { line: end.line, column: end.column } }); } hbsPosFor(offset) { let seenLines = 0, seenChars = 0; if (offset > this.source.length) return null; for (;;) { let nextLine = this.source.indexOf("\n", seenChars); if (offset <= nextLine || -1 === nextLine) return { line: seenLines + 1, column: offset - seenChars }; seenLines += 1, seenChars = nextLine + 1; } } charPosFor(position) { let {line: line, column: column} = position, sourceLength = this.source.length, seenLines = 0, seenChars = 0; for (;seenChars < sourceLength; ) { let nextLine = this.source.indexOf("\n", seenChars); if (-1 === nextLine && (nextLine = this.source.length), seenLines === line - 1) { if (seenChars + column > nextLine) return nextLine; if (DEBUG) { let roundTrip = this.hbsPosFor(seenChars + column); roundTrip.line, roundTrip.column; } return seenChars + column; } if (-1 === nextLine) return 0; seenLines += 1, seenChars = nextLine + 1; } return sourceLength; } } class SpanList { static range(span, fallback = SourceSpan.NON_EXISTENT) { return new SpanList(span.map(loc)).getRangeOffset(fallback); } constructor(span = []) { this._span = span; } add(offset) { this._span.push(offset); } getRangeOffset(fallback) { if (isPresentArray(this._span)) { let first = getFirst(this._span), last = getLast(this._span); return first.extend(last); } return fallback; } } function loc(span) { if (Array.isArray(span)) { let first = getFirst(span), last = getLast(span); return loc(first).extend(loc(last)); } return span instanceof SourceSpan ? span : span.loc; } function hasSpan(span) { return !Array.isArray(span) || 0 !== span.length; } function maybeLoc(location, fallback) { return hasSpan(location) ? loc(location) : fallback; } var api$1 = Object.freeze({ __proto__: null, NON_EXISTENT_LOCATION: NON_EXISTENT_LOCATION, SYNTHETIC_LOCATION: SYNTHETIC_LOCATION, Source: Source, SourceOffset: SourceOffset, SourceSlice: SourceSlice, SourceSpan: SourceSpan, SpanList: SpanList, UNKNOWN_POSITION: UNKNOWN_POSITION, hasSpan: hasSpan, loc: loc, maybeLoc: maybeLoc }); function generateSyntaxError(message, location) { let {module: module, loc: loc} = location, {line: line, column: column} = loc.start, code = location.asString(), quotedCode = code ? `\n\n|\n| ${code.split("\n").join("\n| ")}\n|\n\n` : "", error = new Error(`${message}: ${quotedCode}(error occurred in '${module}' @ line ${line} : column ${column})`); return error.name = "SyntaxError", error.location = location, error.code = code, 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" ] }, TraversalError = function() { function TraversalError(message, node, parent, key) { let error = Error.call(this, message); this.key = key, this.message = message, this.node = node, this.parent = parent, error.stack && (this.stack = error.stack); } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment return TraversalError.prototype = Object.create(Error.prototype), // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access TraversalError.prototype.constructor = TraversalError, TraversalError; }(); function cannotRemoveNode(node, parent, key) { return new TraversalError("Cannot remove a node unless it is part of an array", node, parent, key); } function cannotReplaceNode(node, parent, key) { return new TraversalError("Cannot replace a node with multiple nodes unless it is part of an array", node, parent, key); } function cannotReplaceOrRemoveInKeyHandlerYet(node, key) { return new TraversalError("Replacing and removing in key handlers is not yet supported.", node, null, key); } class WalkerPath { 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]: () => new PathParentsIterator(this) }; } } class PathParentsIterator { constructor(path) { this.path = path; } next() { return this.path.parent ? (this.path = this.path.parent, { done: !1, value: this.path }) : { done: !0, value: null }; } } function getEnterFunction(handler) { return "function" == typeof handler ? handler : handler.enter; } function getExitFunction(handler) { return "function" == typeof handler ? void 0 : handler.exit; } function visitNode(visitor, path) { let enter, exit, result, {node: node, parent: parent, parentKey: parentKey} = path, handler = function(visitor, nodeType) { // eslint-disable-next-line @typescript-eslint/no-deprecated if (visitor.Program && ("Template" === nodeType && !visitor.Template || "Block" === nodeType && !visitor.Block)) // eslint-disable-next-line @typescript-eslint/no-deprecated return visitor.Program; let handler = visitor[nodeType]; return void 0 !== handler ? handler : visitor.All; }(visitor, node.type); if (void 0 !== handler && (enter = getEnterFunction(handler), exit = getExitFunction(handler)), void 0 !== enter && (result = enter(node, path)), null != result) { if (JSON.stringify(node) !== JSON.stringify(result)) return Array.isArray(result) ? (visitArray(visitor, result, parent, parentKey), result) : visitNode(visitor, new WalkerPath(result, parent, parentKey)) || result; result = void 0; } if (void 0 === result) { let keys = visitorKeys[node.type]; for (let i = 0; i < keys.length; i++) // we know if it has child keys we can widen to a ParentNode visitKey(visitor, handler, path, keys[i]); void 0 !== exit && (result = exit(node, path)); } return result; } function set(node, key, value) { node[key] = value; } function visitKey(visitor, handler, path, key) { let keyEnter, keyExit, {node: node} = path, value = function(node, key) { return node[key]; }(node, key); if (value) { if (void 0 !== handler) { let keyHandler = function(handler, key) { let keyVisitor = "function" != typeof handler ? handler.keys : void 0; if (void 0 === keyVisitor) return; let keyHandler = keyVisitor[key]; return void 0 !== keyHandler ? keyHandler : keyVisitor.All; }(handler, key); void 0 !== keyHandler && (keyEnter = getEnterFunction(keyHandler), keyExit = getExitFunction(keyHandler)); } if (void 0 !== keyEnter && void 0 !== keyEnter(node, key)) throw cannotReplaceOrRemoveInKeyHandlerYet(node, key); if (Array.isArray(value)) visitArray(visitor, value, path, key); else { let result = visitNode(visitor, new WalkerPath(value, path, key)); void 0 !== result && // 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-unsafe-argument, @typescript-eslint/no-explicit-any function(node, key, value, result) { if (null === result) throw cannotRemoveNode(value, node, key); if (Array.isArray(result)) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (1 !== result.length) throw 0 === result.length ? cannotRemoveNode(value, node, key) : cannotReplaceNode(value, node, key); set(node, key, result[0]); } else set(node, key, result); }(node, key, value, result); } if (void 0 !== keyExit && void 0 !== keyExit(node, key)) throw cannotReplaceOrRemoveInKeyHandlerYet(node, key); } } function visitArray(visitor, array, parent, parentKey) { for (let i = 0; i < array.length; i++) { let node = array[i], result = visitNode(visitor, new WalkerPath(node, parent, parentKey)); void 0 !== result && (i += spliceArray(array, i, result) - 1); } } function spliceArray(array, index, result) { return null === result ? (array.splice(index, 1), 0) : Array.isArray(result) ? (array.splice(index, 1, ...result), result.length) : (array.splice(index, 1, result), 1); } function traverse(node, visitor) { visitNode(visitor, new WalkerPath(node)); } class Walker { constructor(order) { this.order = order, this.stack = []; } visit(node, visitor) { node && (this.stack.push(node), "post" === this.order ? (this.children(node, visitor), visitor(node, this)) : (visitor(node, this), this.children(node, visitor)), this.stack.pop()); } children(node, callback) { switch (node.type) { case "Block": case "Template": return void walkBody(this, node.body, callback); case "ElementNode": return void walkBody(this, node.children, callback); case "BlockStatement": return this.visit(node.program, callback), void this.visit(node.inverse || null, callback); default: return; } } } function walkBody(walker, body, callback) { for (const child of body) walker.visit(child, callback); } function appendChild(parent, node) { (function(node) { switch (node.type) { case "Block": case "Template": return node.body; case "ElementNode": return node.children; } })(parent).push(node); } function isHBSLiteral(path) { return "StringLiteral" === path.type || "BooleanLiteral" === path.type || "NumberLiteral" === path.type || "NullLiteral" === path.type || "UndefinedLiteral" === path.type; } let _SOURCE; function SOURCE() { return _SOURCE || (_SOURCE = new Source("", "(synthetic)")), _SOURCE; } function buildVar(name, loc) { return b.var({ name: name, loc: buildLoc(loc || null) }); } function buildPath(path, loc) { let span = buildLoc(loc || null); if ("string" != typeof path) { if ("type" in path) return path; { path.head.indexOf("."); let {head: head, tail: tail} = path; return b.path({ head: b.head({ original: head, loc: span.sliceStartChars({ chars: head.length