@glimmer/syntax
Version:
1,421 lines (1,315 loc) • 163 kB
JavaScript
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 " ";
case 34:
return """;
case 38:
return "&";
default:
return char;
}
}
function textReplacer(char) {
switch (char.charCodeAt(0)) {
case 160:
return " ";
case 38:
return "&";
case 60:
return "<";
case 62:
return ">";
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