@glimmer/syntax
Version:
4,380 lines • 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
})
}),
tail: tail,
loc: buildLoc(loc || null)
});
}
}
let {head: head, tail: tail} = function(original, loc) {
let [head, ...tail] = original.split("."), headNode = b.head({
original: head,
loc: buildLoc(loc || null)
});
return b.path({
head: headNode,
tail: tail,
loc: buildLoc(loc || null)
});
}(path, span);
return b.path({
head: head,
tail: tail,
loc: span
});
}
function buildLiteral(type, value, loc) {
return b.literal({
type: type,
value: value,
loc: buildLoc(loc || null)
});
}
// Miscellaneous
function buildHash(pairs = [], loc) {
return b.hash({
pairs: pairs,
loc: buildLoc(loc || null)
});
}
function buildBlockParams(params) {
return params.map((p => "string" == typeof p ? b.var({
name: p,
loc: SourceSpan.synthetic(p)
}) : p));
}
function buildBlockItself(body = [], params = [], chained = !1, loc) {
return b.blockItself({
body: body,
params: buildBlockParams(params),
chained: chained,
loc: buildLoc(loc || null)
});
}
function buildTemplate(body = [], blockParams = [], loc) {
return b.template({
body: body,
blockParams: blockParams,
loc: buildLoc(loc || null)
});
}
function buildLoc(...args) {
if (1 === args.length) {
let loc = args[0];
return loc && "object" == typeof loc ? SourceSpan.forHbsLoc(SOURCE(), loc) : SourceSpan.forHbsLoc(SOURCE(), SYNTHETIC_LOCATION);
}
{
let [startLine, startColumn, endLine, endColumn, _source] = args, source = _source ? new Source("", _source) : SOURCE();
return SourceSpan.forHbsLoc(source, {
start: {
line: startLine,
column: startColumn
},
end: {
line: endLine || startLine,
column: endColumn || startColumn
}
});
}
}
var publicBuilder = {
mustache: function(path, params = [], hash = buildHash([]), trusting = !1, loc, strip) {
return b.mustache({
path: buildPath(path),
params: params,
hash: hash,
trusting: trusting,
strip: strip,
loc: buildLoc(loc || null)
});
},
block: function(path, params, hash, _defaultBlock, _elseBlock = null, loc, openStrip, inverseStrip, closeStrip) {
let defaultBlock, elseBlock = null;
return defaultBlock = "Template" === _defaultBlock.type ? b.blockItself({
params: buildBlockParams(_defaultBlock.blockParams),
body: _defaultBlock.body,
loc: _defaultBlock.loc
}) : _defaultBlock, "Template" === _elseBlock?.type ? (_elseBlock.blockParams.length,
elseBlock = b.blockItself({
params: [],
body: _elseBlock.body,
loc: _elseBlock.loc
})) : elseBlock = _elseBlock, b.block({
path: buildPath(path),
params: params || [],
hash: hash || buildHash([]),
defaultBlock: defaultBlock,
elseBlock: elseBlock,
loc: buildLoc(loc || null),
openStrip: openStrip,
inverseStrip: inverseStrip,
closeStrip: closeStrip
});
},
comment: function(value, loc) {
return b.comment({
value: value,
loc: buildLoc(loc || null)
});
},
mustacheComment: function(value, loc) {
return b.mustacheComment({
value: value,
loc: buildLoc(loc || null)
});
},
element: function(tag, options = {}) {
let path, selfClosing, {attrs: attrs, blockParams: blockParams, modifiers: modifiers, comments: comments, children: children, openTag: openTag, closeTag: _closeTag, loc: loc} = options;
// this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST
"string" == typeof tag ? tag.endsWith("/") ? (path = buildPath(tag.slice(0, -1)),
selfClosing = !0) : path = buildPath(tag) : "type" in tag ? (tag.type, tag.type,
path = tag) : "path" in tag ? (tag.path.type, tag.path.type, path = tag.path, selfClosing = tag.selfClosing) : (path = buildPath(tag.name),
selfClosing = tag.selfClosing);
let params = blockParams?.map((param => "string" == typeof param ? buildVar(param) : param)), closeTag = null;
return _closeTag ? closeTag = buildLoc(_closeTag) : void 0 === _closeTag && (closeTag = selfClosing || isVoidTag(path.original) ? null : buildLoc(null)),
b.element({
path: path,
selfClosing: selfClosing || !1,
attributes: attrs || [],
params: params || [],
modifiers: modifiers || [],
comments: comments || [],
children: children || [],
openTag: buildLoc(openTag || null),
closeTag: closeTag,
loc: buildLoc(loc || null)
});
},
elementModifier: function(path, params, hash, loc) {
return b.elementModifier({
path: buildPath(path),
params: params || [],
hash: hash || buildHash([]),
loc: buildLoc(loc || null)
});
},
attr: function(name, value, loc) {
return b.attr({
name: name,
value: value,
loc: buildLoc(loc || null)
});
},
text: function(chars = "", loc) {
return b.text({
chars: chars,
loc: buildLoc(loc || null)
});
}
// Expressions
,
sexpr: function(path, params = [], hash = buildHash([]), loc) {
return b.sexpr({
path: buildPath(path),
params: params,
hash: hash,
loc: buildLoc(loc || null)
});
},
concat: function(parts, loc) {
if (!isPresentArray(parts)) throw new Error("b.concat requires at least one part");
return b.concat({
parts: parts,
loc: buildLoc(loc || null)
});
},
hash: buildHash,
pair: function(key, value, loc) {
return b.pair({
key: key,
value: value,
loc: buildLoc(loc || null)
});
},
literal: buildLiteral,
program: function(body, blockParams, loc) {
return blockParams && blockParams.length ? buildBlockItself(body, blockParams, !1, loc) : buildTemplate(body, [], loc);
},
blockItself: buildBlockItself,
template: buildTemplate,
loc: buildLoc,
pos: function(line, column) {
return b.pos({
line: line,
column: column
});
},
path: buildPath,
fullPath: function(head, tail = [], loc) {
return b.path({
head: head,
tail: tail,
loc: buildLoc(loc || null)
});
},
head: function(original, loc) {
return b.head({
original: original,
loc: buildLoc(loc || null)
});
},
at: function(name, loc) {
return b.atName({
name: name,
loc: buildLoc(loc || null)
});
},
var: buildVar,
this: function(loc) {
return b.this({
loc: buildLoc(loc || null)
});
},
string: literal("StringLiteral"),
boolean: literal("BooleanLiteral"),
number: literal("NumberLiteral"),
undefined: () => buildLiteral("UndefinedLiteral", void 0),
null: () => buildLiteral("NullLiteral", null)
};
function literal(type) {
return function(value, loc) {
return buildLiteral(type, value, loc);
};
}
const DEFAULT_STRIP = {
close: !1,
open: !1
}, b = new
/**
* The Parser Builder differentiates from the public builder API by:
*
* 1. Offering fewer different ways to instantiate nodes
* 2. Mandating source locations
*/
class {
pos({line: line, column: column}) {
return {
line: line,
column: column
};
}
blockItself({body: body, params: params1, chained: chained = !1, loc: loc}) {
return {
type: "Block",
body: body,
params: params1,
get blockParams() {
return this.params.map((p => p.name));
},
set blockParams(params) {
this.params = params.map((name1 => b.var({
name: name1,
loc: SourceSpan.synthetic(name1)
})));
},
chained: chained,
loc: loc
};
}
template({body: body, blockParams: blockParams, loc: loc}) {
return {
type: "Template",
body: body,
blockParams: blockParams,
loc: loc
};
}
mustache({path: path, params: params1, hash: hash, trusting: trusting, loc: loc, strip: strip = DEFAULT_STRIP}) {
return function({path: path, params: params, hash: hash, trusting: trusting, strip: strip, loc: loc}) {
const node = {
type: "MustacheStatement",
path: path,
params: params,
hash: hash,
trusting: trusting,
strip: strip,
loc: loc
};
return Object.defineProperty(node, "escaped", {
enumerable: !1,
get() {
return !this.trusting;
},
set(value1) {
this.trusting = !value1;
}
}), node;
}({
path: path,
params: params1,
hash: hash,
trusting: trusting,
strip: strip,
loc: loc
});
}
block({path: path, params: params1, hash: hash, defaultBlock: defaultBlock, elseBlock: elseBlock = null, loc: loc, openStrip: openStrip = DEFAULT_STRIP, inverseStrip: inverseStrip = DEFAULT_STRIP, closeStrip: closeStrip = DEFAULT_STRIP}) {
return {
type: "BlockStatement",
path: path,
params: params1,
hash: hash,
program: defaultBlock,
inverse: elseBlock,
loc: loc,
openStrip: openStrip,
inverseStrip: inverseStrip,
closeStrip: closeStrip
};
}
comment({value: value1, loc: loc}) {
return {
type: "CommentStatement",
value: value1,
loc: loc
};
}
mustacheComment({value: value1, loc: loc}) {
return {
type: "MustacheCommentStatement",
value: value1,
loc: loc
};
}
concat({parts: parts, loc: loc}) {
return {
type: "ConcatStatement",
parts: parts,
loc: loc
};
}
element({path: path, selfClosing: selfClosing1, attributes: attributes, modifiers: modifiers, params: params1, comments: comments, children: children, openTag: openTag, closeTag: closeTag, loc: loc}) {
let _selfClosing = selfClosing1;
return {
type: "ElementNode",
path: path,
attributes: attributes,
modifiers: modifiers,
params: params1,
comments: comments,
children: children,
openTag: openTag,
closeTag: closeTag,
loc: 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((name1 => b.var({
name: name1,
loc: SourceSpan.synthetic(name1)
})));
},
get selfClosing() {
return _selfClosing;
},
set selfClosing(selfClosing) {
_selfClosing = selfClosing, this.closeTag = selfClosing ? null : SourceSpan.synthetic(`</${this.tag}>`);
}
};
}
elementModifier({path: path, params: params1, hash: hash, loc: loc}) {
return {
type: "ElementModifierStatement",
path: path,
params: params1,
hash: hash,
loc: loc
};
}
attr({name: name1, value: value1, loc: loc}) {
return {
type: "AttrNode",
name: name1,
value: value1,
loc: loc
};
}
text({chars: chars, loc: loc}) {
return {
type: "TextNode",
chars: chars,
loc: loc
};
}
sexpr({path: path, params: params1, hash: hash, loc: loc}) {
return {
type: "SubExpression",
path: path,
params: params1,
hash: hash,
loc: loc
};
}
path({head: head, tail: tail, loc: loc}) {
return function({head: head, tail: tail, loc: loc}) {
const node = {
type: "PathExpression",
head: head,
tail: tail,
get original() {
return [ this.head.original, ...this.tail ].join(".");
},
set original(value) {
let [head, ...tail] = value.split(".");
this.head = publicBuilder.head(head, this.head.loc), this.tail = tail;
},
loc: loc
};
return Object.defineProperty(node, "parts", {
enumerable: !1,
get() {
let parts = this.original.split(".");
return "this" === parts[0] ?
// parts does not include `this`
parts.shift() : parts[0].startsWith("@") && (
// parts does not include leading `@`
parts[0] = parts[0].slice(1)), 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
"this" === parts[0] || parts[0]?.startsWith("@") || ("ThisHead" === this.head.type ? parts.unshift("this") : "AtHead" === this.head.type && (parts[0] = `@${parts[0]}`)),
this.original = parts.join(".");
}
}), Object.defineProperty(node, "this", {
enumerable: !1,
get() {
return "ThisHead" === this.head.type;
}
}), Object.defineProperty(node, "data", {
enumerable: !1,
get() {
return "AtHead" === this.head.type;
}
}), node;
}({
head: head,
tail: tail,
loc: loc
});
}
head({original: original, loc: loc}) {
return "this" === original ? this.this({
loc: loc
}) : "@" === original[0] ? this.atName({
name: original,
loc: loc
}) : this.var({
name: original,
loc: loc
});
}
this({loc: loc}) {
return {
type: "ThisHead",
get original() {
return "this";
},
loc: loc
};
}
atName({name: name1, loc: loc}) {
let _name = "";
const node = {
type: "AtHead",
get name() {
return _name;
},
set name(value) {
value[0], value.indexOf("."), _name = value;
},
get original() {
return this.name;
},
set original(value) {
this.name = value;
},
loc: loc
};
// trigger the assertions
return node.name = name1, node;
}
var({name: name1, loc: loc}) {
let _name = "";
const node = {
type: "VarHead",
get name() {
return _name;
},
set name(value) {
value[0], value.indexOf("."), _name = value;
},
get original() {
return this.name;
},
set original(value) {
this.name = value;
},
loc: loc
};
// trigger the assertions
return node.name = name1, node;
}
hash({pairs: pairs, loc: loc}) {
return {
type: "Hash",
pairs: pairs,
loc: loc
};
}
pair({key: key, value: value1, loc: loc}) {
return {
type: "HashPair",
key: key,
value: value1,
loc: loc
};
}
literal({type: type, value: value1, loc: loc}) {
return function({type: type, value: value1, loc: loc}) {
const node = {
type: type,
value: value1,
loc: loc
};
return Object.defineProperty(node, "original", {
enumerable: !1,
get() {
return this.value;
},
set(value1) {
this.value = value1;
}
}), node;
}({
type: type,
value: value1,
loc: loc
});
}
};
class Parser {
constructor(source, entityParser = new EntityParser(HTML5NamedCharRefs), mode = "precompile") {
this.elementStack = [], this.currentAttribute = null, this.currentNode = null, this.source = source,
this.lines = source.source.split(/\r\n?|\n/u), this.tokenizer = new EventedTokenizer(this, entityParser, mode);
}
offset() {
let {line: line, column: column} = this.tokenizer;
return this.source.offsetFor(line, column);
}
pos({line: line, column: 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 this.currentAttribute;
}
get currentTag() {
let node = this.currentNode;
return node && ("StartTag" === node.type || node.type), node;
}
get currentStartTag() {
let node = this.currentNode;
return node && node.type, node;
}
get currentEndTag() {
let node = this.currentNode;
return node && node.type, node;
}
get currentComment() {
let node = this.currentNode;
return node && node.type, node;
}
get currentData() {
let node = this.currentNode;
return node && node.type, node;
}
acceptNode(node) {
return this[node.type](node);
}
currentElement() {
return getLast(this.elementStack);
}
sourceForNode(node, endNode) {
let line, lastLine, lastColumn, firstLine = node.loc.start.line - 1, currentLine = firstLine - 1, firstColumn = node.loc.start.column, string = [];
for (endNode ? (lastLine = endNode.loc.end.line - 1, lastColumn = endNode.loc.end.column) : (lastLine = node.loc.end.line - 1,
lastColumn = node.loc.end.column); currentLine < lastLine; ) currentLine++, line = this.lines[currentLine],
currentLine === firstLine ? firstLine === lastLine ? string.push(line.slice(firstColumn, lastColumn)) : string.push(line.slice(firstColumn)) : currentLine === lastLine ? string.push(line.slice(0, lastColumn)) : string.push(line);
return string.join("\n");
}
}
class HandlebarsNodeVisitors extends Parser {
parse(program, blockParams) {
program.loc;
let node = b.template({
body: [],
blockParams: blockParams,
loc: this.source.spanFor(program.loc)
}), 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
return this.pendingError?.eof(template.loc.getEnd()), template;
}
Program(program, blockParams) {
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 (0 === program.body.length) 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 ("ElementNode" === poppedNode?.type) throw generateSyntaxError(`Unclosed element \`${poppedNode.tag}\``, poppedNode.loc);
node.type;
}
return node;
}
BlockStatement(block) {
if ("comment" === this.tokenizer.state) return block.loc, void this.appendToCommentData(this.sourceForNode(block));
if ("data" !== this.tokenizer.state && "beforeData" !== this.tokenizer.state) throw generateSyntaxError("A block may only be used inside an HTML element or another block.", this.source.spanFor(block.loc));
const {path: path, params: params, hash: hash} = acceptCallNodes(this, block), loc = this.source.spanFor(block.loc);
// Backfill block params loc for the default block
let repairedBlock, blockParams = [];
if (block.program.blockParams?.length) {
// Start from right after the hash
let span = hash.loc.collapse("end");
// Extend till the beginning of the block
span = block.program.loc ? span.withEnd(this.source.spanFor(block.program.loc).getStart()) : block.program.body[0] ? span.withEnd(this.source.spanFor(block.program.body[0].loc).getStart()) : 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, loc;
nameStart = skipStart >= limit ? -1 : content.indexOf(name, skipStart), -1 === nameStart || nameStart + name.length > limit ? (skipStart = limit,
loc = this.source.spanFor(NON_EXISTENT_LOCATION)) : (skipStart = nameStart, loc = span.sliceStartChars({
skipStart: skipStart,
chars: name.length
}), skipStart += name.length), blockParams.push(b.var({
name: name,
loc: loc
}));
}
} else repairedBlock = repairBlock(this.source, block, loc);
const program = this.Program(repairedBlock.program, blockParams), inverse = repairedBlock.inverse ? this.Program(repairedBlock.inverse, []) : null, node = b.block({
path: path,
params: params,
hash: hash,
defaultBlock: program,
elseBlock: inverse,
loc: this.source.spanFor(block.loc),
openStrip: block.openStrip,
inverseStrip: block.inverseStrip,
closeStrip: block.closeStrip
});
appendChild(this.currentElement(), node);
}
MustacheStatement(rawMustache) {
this.pendingError?.mustache(this.source.spanFor(rawMustache.loc));
const {tokenizer: tokenizer} = this;
if ("comment" === tokenizer.state) return void this.appendToCommentData(this.sourceForNode(rawMustache));
let mustache;
const {escaped: escaped, loc: loc, strip: strip} = rawMustache;
if ("original" in rawMustache.path && "...attributes" === rawMustache.path.original) 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: strip
}); else {
const {path: path, params: params, hash: hash} = acceptCallNodes(this, rawMustache);
mustache = b.mustache({
path: path,
params: params,
hash: hash,
trusting: !escaped,
loc: this.source.spanFor(loc),
strip: 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(!1), this.finishAttributeValue(), addElementModifier(this.currentStartTag, mustache),
tokenizer.transitionTo("beforeAttributeName");
break;
case "afterAttributeValueQuoted":
addElementModifier(this.currentStartTag, mustache), tokenizer.transitionTo("beforeAttributeName");
break;
// Attribute values
case "beforeAttributeValue":
this.beginAttributeValue(!1), this.appendDynamicAttributeValuePart(mustache), tokenizer.transitionTo("attributeValueUnquoted");
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 = !0, attr.parts.push(part);
}
finalizeTextPart() {
const text = this.currentAttr.currentPart;
null !== text && (this.currentAttr.parts.push(text), this.startTextPart());
}
startTextPart() {
this.currentAttr.currentPart = null;
}
ContentStatement(content) {
!function(tokenizer, content) {
let line = content.loc.start.line, column = content.loc.start.column;
const offsets = function(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), lines = difference.split(/\n/u), lineCount = lines.length - 1;
return {
lines: lineCount,
columns: lines[lineCount].length
};
}(content.original, content.value);
line += offsets.lines, offsets.lines ? column = offsets.columns : column += offsets.columns,
tokenizer.line = line, tokenizer.column = column;
}(this.tokenizer, content), this.tokenizer.tokenizePart(content.value), this.tokenizer.flushData();
}
CommentStatement(rawComment) {
const {tokenizer: tokenizer} = this;
if ("comment" === tokenizer.state) return this.appendToCommentData(this.sourceForNode(rawComment)),
null;
const {value: value, loc: loc} = rawComment, comment = b.mustacheComment({
value: 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: path, params: params, hash: hash} = acceptCallNodes(this, sexpr);
return b.sexpr({
path: path,
params: params,
hash: hash,
loc: this.source.spanFor(sexpr.loc)
});
}
PathExpression(path) {
const {original: original} = path;
let parts;
if (-1 !== original.indexOf("/")) {
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 (-1 !== original.indexOf(".")) 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));
parts = path.parts;
}
let pathHead, thisHead = !1;
// 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 = !0), 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 (void 0 === head) 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 (void 0 === head) 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: 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: void 0,
loc: this.source.spanFor(undef.loc)
});
}
NullLiteral(nul) {
return b.literal({
type: "NullLiteral",
value: null,
loc: this.source.spanFor(nul.loc)
});
}
constructor(...args) {
super(...args), // 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.
this.pendingError = null;
}
}
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;
throw value = "BooleanLiteral" === node.path.type ? node.path.original.toString() : "StringLiteral" === node.path.type ? `"${node.path.original}"` : "NullLiteral" === node.path.type ? "null" : "NumberLiteral" === node.path.type ? node.path.value.toString() : "undefined",
generateSyntaxError(`${node.path.type} "${"StringLiteral" === node.path.type ? 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))), end = isPresentArray(params) ? getLast(params).loc : path.loc;
// 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)
return {
path: path,
params: params,
hash: node.hash ? compiler.Hash(node.hash) : b.hash({
pairs: [],
loc: compiler.source.spanFor(end).collapse("end")
})
};
}
function addElementModifier(element, mustache) {
const {path: path, params: params, hash: hash, loc: loc} = mustache;
if (isHBSLiteral(path)) {
const modifier = `{{${function(literal) {
return "UndefinedLiteral" === literal.type ? "undefined" : JSON.stringify(literal.value);
}(path)}}}`;
throw generateSyntaxError(`In <${element.name} ... ${modifier} ..., ${modifier} is not a valid modifier`, mustache.loc);
}
const modifier = b.elementModifier({
path: path,
params: params,
hash: hash,
loc: loc
});
element.modifiers.push(modifier);
}
function repairBlock(source, block, fallbackStart) {
// Extend till the beginning of the block
if (!block.program.loc) {
const start = block.program.body.at(0), end = block.program.body.at(-1);
if (start && end) block.program.loc = {
...start.loc,
end: end.loc.end
}; else {
const loc = source.spanFor(block.loc);
block.program.loc = fallbackStart.withEnd(loc.getEnd());
}
}
let endProgram = source.spanFor(block.program.loc).getEnd();
return block.inverse && !block.inverse.loc && (block.inverse.loc = endProgram.collapsed()),
block;
}
// vendored from simple-html-tokenizer because it's unexported
function isSpace(char) {
return /[\t\n\f ]/u.test(char);
}
class TokenizerEventHandlers extends HandlebarsNodeVisitors {
reset() {
this.currentNode = null;
}
// Comment
beginComment() {
this.currentNode = {
type: "CommentStatement",
value: "",
start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn)
};
}
appendToCommentData(char) {
this.currentComment.value += char;
}
finishComment() {
appendChild(this.currentElement(), b.comment(this.finish(this.currentComment)));
}
// Data
beginData() {
this.currentNode = {
type: "TextNode",
chars: "",
start: this.offset()
};
}
appendToData(char) {
this.currentData.chars += char;
}
finishData() {
appendChild(this.currentElement(), b.text(this.finish(this.currentData)));
}
// Tags - basic
tagOpen() {
this.tagOpenLine = this.tokenizer.line, this.tagOpenColumn = this.tokenizer.column;
}
beginStartTag() {
this.currentNode = {
type: "StartTag",
name: "",
nameStart: null,
nameEnd: null,
attributes: [],
modifiers: [],
comments: [],
params: [],
selfClosing: !1,
start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn)
};
}
beginEndTag() {
this.currentNode = {
type: "EndTag",
name: "",
start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn)
};
}
finishTag() {
let tag = this.finish(this.currentTag);
if ("StartTag" === tag.type) {
if (this.finishStartTag(), ":" === tag.name) throw generateSyntaxError("Invalid named block named detected, you may have created a named block without a name, or you may have began your name with a number. Named blocks must have names that are at least one character long, and begin with a lower case letter", this.source.spanFor({
start: this.currentTag.start.toJSON(),
end: this.offset().toJSON()
}));
(voidMap.has(tag.name) || tag.selfClosing) && this.finishEndTag(!0);
} else tag.type, tag.type, this.finishEndTag(!1);
}
finishStartTag() {
let {name: name, nameStart: nameStart, nameEnd: nameEnd} = this.currentStartTag, nameLoc = nameStart.until(nameEnd), [head, ...tail] = name.split("."), path = b.path({
head: b.head({
original: head,
loc: nameLoc.sliceStartChars({
chars: head.length
})
}),
tail: tail,
loc: nameLoc
}), {attributes: attributes, modifiers: modifiers, comments: comments, params: params, selfClosing: selfClosing, loc: loc} = this.finish(this.currentStartTag), element = b.element({
path: path,
selfClosing: selfClosing,
attributes: attributes,
modifiers: modifiers,
comments: comments,
params: params,
children: [],
openTag: loc,
closeTag: selfClosing ? null : SourceSpan.broken(),
loc: loc
});
this.elementStack.push(element);
}
finishEndTag(isVoid) {
let {start: closeTagStart} = this.currentTag, tag = this.finish(this.currentTag), element = this.elementStack.pop();
this.validateEndTag(tag, element, isVoid);
let parent = this.currentElement();
isVoid ? element.closeTag = null : element.selfClosing ? element.closeTag : element.closeTag = closeTagStart.until(this.offset()),
element.loc = element.loc.withEnd(this.offset()), appendChild(parent, b.element(element));
}
markTagAsSelfClosing() {
let tag = this.currentTag;
if ("StartTag" !== tag.type) throw generateSyntaxError("Invalid end tag: closing tag must not be self-closing", this.source.spanFor({
start: tag.start.toJSON(),
end: this.offset().toJSON()
}));
tag.selfClosing = !0;
}
// Tags - name
appendToTagName(char) {
let tag = this.currentTag;
if (tag.name += char, "StartTag" === tag.type) {
let offset = this.offset();
null === tag.nameStart && (tag.nameEnd,
// Note that the tokenizer already consumed the token here
tag.nameStart = offset.move(-1)), tag.nameEnd = offset;
}
}
// Tags - attributes
beginAttribute() {
let offset = this.offset();
this.currentAttribute = {
name: "",
parts: [],
currentPart: null,
isQuoted: !1,
isDynamic: !1,
start: offset,
valueSpan: offset.collapsed()
};
}
appendToAttributeName(char) {
this.currentAttr.name += char,
// The block params parsing code can actually handle peek=non-space just
// fine, but this check was added as an optimization, as there is a little
// bit of setup overhead for the parsing logic just to immediately bail
"as" === this.currentAttr.name && this.parsePossibleBlockParams();
}
beginAttributeValue(isQuoted) {
this.currentAttr.isQuoted = isQuoted, this.startTextPart(), this.currentAttr.valueSpan = this.offset().collapsed();
}
appendToAttributeValue(char) {
let parts = this.currentAttr.parts, lastPart = parts[parts.length - 1], current = this.currentAttr.currentPart;
if (current) current.chars += char,
// update end location for each added char
current.loc = current.loc.withEnd(this.offset()); else {
// initially assume the text node is a single char
let loc = this.offset();
// the tokenizer line/column have already been advanced, correct location info
loc = "\n" === char ? lastPart ? lastPart.loc.getEnd() : this.currentAttr.valueSpan.getStart() : loc.move(-1),
this.currentAttr.currentPart = b.text({
chars: char,
loc: loc.collapsed()
});
}
}
finishAttributeValue() {
this.finalizeTextPart();
let tag = this.currentTag, tokenizerPos = this.offset();
if ("EndTag" === tag.type) throw generateSyntaxError("Invalid end tag: closing tag must not have attributes", this.source.spanFor({
start: tag.start.toJSON(),
end: tokenizerPos.toJSON()
}));
let {name: name, parts: parts, start: start, isQuoted: isQuoted, isDynamic: isDynamic, valueSpan: valueSpan} = this.currentAttr;
// Just trying to be helpful with `<Hello |foo|>` rather than letting it through as an attribute
if (name.startsWith("|") && 0 === parts.length && !isQuoted && !isDynamic) throw generateSyntaxError("Invalid block parameters syntax: block parameters must be preceded by the `as` keyword", start.until(start.move(name.length)));
let value = this.assembleAttributeValue(parts, isQuoted, isDynamic, start.until(tokenizerPos));
value.loc = valueSpan.withEnd(tokenizerPos);
let attribute = b.attr({
name: name,
value: value,
loc: start.until(tokenizerPos)
});
this.currentStartTag.attributes.push(attribute);
}
parsePossibleBlockParams() {
// const enums that we can't use directly
const ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u;
this.tokenizer.state;
const element = this.currentStartTag, as = this.currentAttr;
let state = {
state: "PossibleAs"
};
const handlers = {
PossibleAs: next => {
if (state.state, isSpace(next))
// " as ..."
state = {
state: "BeforeStartPipe"
}, this.tokenizer.transitionTo("afterAttributeName"), this.tokenizer.consume(); else {
if ("|" === next)
// " as|..."
// Following Handlebars and require a space between "as" and the pipe
throw generateSyntaxError('Invalid block parameters syntax: expecting at least one space character between "as" and "|"', as.start.until(this.offset().move(1)));
// " as{{...", " async...", " as=...", " as>...", " as/>..."
// Don't consume, let the normal tokenizer code handle the next steps
state = {
state: "Done"
};
}
},
BeforeStartPipe: next => {
state.state, isSpace(next) ? this.tokenizer.consume() : "|" === next ? (state = {
state: "BeforeBlockParamName"
}, this.tokenizer.transitionTo("beforeAttributeName"), this.tokenizer.consume()) :
// " as {{...", " as bs...", " as =...", " as ...", " as/>..."
// Don't consume, let the normal tokenizer code handle the next steps
state = {
state: "Done"
};
},
BeforeBlockParamName: next => {
if (state.state, isSpace(next)) this.tokenizer.consume(); else if ("" === next)
// The HTML tokenizer ran out of characters, so we are either
// encountering mustache or <EOF>
state = {
state: "Done"
}, this.pendingError = {
mustache(loc) {
throw generateSyntaxError("Invalid block parameters syntax: mustaches cannot be used inside parameters list", loc);
},
eof(loc) {
throw generateSyntaxError('Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', as.start.until(loc));
}
}; else if ("|" === next) {
if (0 === element.params.length)
// Following Handlebars and treat empty block params a syntax error
throw generateSyntaxError("Invalid block parameters syntax: empty parameters list, expecting at least one identifier", as.start.until(this.offset().move(1)));
state = {
state: "AfterEndPipe"
}, this.tokenizer.consume();
} else {
if (">" === next || "/" === next) throw generateSyntaxError('Invalid block parameters syntax: incomplete parameters list, expecting "|" but the tag was closed prematurely', as.start.until(this.offset().move(1)));
// slurp up anything else into the name, validate later
state = {
state: "BlockParamName",
name: next,
start: this.offset()
}, this.tokenizer.consume();
}
},
BlockParamName: next => {
if (state.state, "" === next)
// The HTML tokenizer ran out of characters, so we are either
// encountering mustache or <EOF>, HBS side will attach the error
// to the next span
state = {
state: "Done"
}, this.pendingError = {
mustache(loc) {
throw generateSyntaxError("Invalid block parameters syntax: mustaches cannot be used inside parameters list", loc);
},
eof(loc) {
throw generateSyntaxError('Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', as.start.until(loc));
}
}; else if ("|" === next || isSpace(next)) {
let loc = state.start.until(this.offset());
if ("this" === state.name || ID_INVERSE_PATTERN.test(state.name)) throw generateSyntaxError(`Invalid block parameters syntax: invalid identifier name \`${state.name}\``, loc);
element.params.push(b.var({
name: state.name,
loc: loc
})), state = "|" === next ? {
state: "AfterEndPipe"
} : {
state: "BeforeBlockParamName"
}, this.tokenizer.consume();
} else {
if (">" === next || "/" === next) throw generateSyntaxError('Invalid block parameters syntax: expecting "|" but the tag was closed prematurely', as.start.until(this.offset().move(1)));
// slurp up anything else into the name, validate later
state.name += next, this.tokenizer.consume();
}
},
AfterEndPipe: next => {
state.state, isSpace(next) ? this.tokenizer.consume() : "" === next ? (
// The HTML tokenizer ran out of characters, so we are either
// encountering mustache or <EOF>, HBS side will attach the error
// to the next span
state = {
state: "Done"
}, this.pendingError = {
mustache(loc) {
throw generateSyntaxError("Invalid block parameters syntax: modifiers cannot follow parameters list", loc);
},
eof(loc) {
throw generateSyntaxError('Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', as.start.until(loc));
}
}) : ">" === next || "/" === next ?
// Don't consume, let the normal tokenizer code handle the next steps
state = {
state: "Done"
} : (
// Slurp up the next "token" for the error span
state = {
state: "Error",
message: 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list',
start: this.offset()
}, this.tokenizer.consume());
},
Error: next => {
if (state.state, "" === next || "/" === next || ">" === next || isSpace(next)) throw generateSyntaxError(state.message, state.start.until(this.offset()));
// Slurp up the next "token" for the error span
this.tokenizer.consume();
},
Done: () => {}
};
let next;
do {
next = this.tokenizer.peek(), handlers[state.state](next);
} while ("Done" !== state.state && "" !== next);
state.state;
}
reportSyntaxError(message) {
throw generateSyntaxError(message, this.offset().collapsed());
}
assembleConcatenatedValue(parts) {
let first = getFirst(parts), last = getLast(parts);
return b.concat({
parts: parts,
loc: this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc))
});
}
validateEndTag(tag, element, selfClosing) {
if (voidMap.has(tag.name) && !selfClosing)
// EngTag is also called by StartTag for void and self-closing tags (i.e.
// <input> or <br />, so we need to check for that here. Otherwise, we would
// throw an error for those cases.
throw generateSyntaxError(`<${tag.name}> elements do not need end tags. You should remove it`, tag.loc);
if ("ElementNode" !== element.type) throw generateSyntaxError(`Closing tag </${tag.name}> without an open tag`, tag.loc);
if (element.tag !== tag.name) throw generateSyntaxError(`Closing tag </${tag.name}> did not match last open tag <${element.tag}> (on line ${element.loc.startPosition.line})`, tag.loc);
}
assembleAttributeValue(parts, isQuoted, isDynamic, span) {
if (isDynamic) {
if (isQuoted) return this.assembleConcatenatedValue(parts);
{
const [head, a] = parts;
if (void 0 === a || "TextNode" === a.type && "/" === a.chars) return head;
throw generateSyntaxError("An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'", span);
}
}
return isPresentArray(parts) ? parts[0] : b.text({
chars: "",
loc: span
});
}
constructor(...args) {
super(...args), this.tagOpenLine = 0, this.tagOpenColumn = 0;
}
}
const syntax = {
parse: preprocess,
builders: publicBuilder,
print: build,
traverse: traverse,
Walker: Walker
};
class CodemodEntityParser extends EntityParser {
// match upstream types, but never match an entity
constructor() {
super({});
}
parse() {}
}
function preprocess(input, options = {}) {
let source, ast, entityParser, mode = options.mode || "precompile";
"string" == typeof input ? (source = new Source(input, options.meta?.moduleName),
ast = "codemod" === mode ? parseWithoutProcessing(input, options.parseOptions) : parse(input, options.parseOptions)) : input instanceof Source ? (source = input,
ast = "codemod" === mode ? parseWithoutProcessing(input.source, options.parseOptions) : parse(input.source, options.parseOptions)) : (source = new Source("", options.meta?.moduleName),
ast = input), "codemod" === mode && (entityParser = new CodemodEntityParser);
let offsets = SourceSpan.forCharPositions(source, 0, source.source.length);
ast.loc = {
source: "(program)",
start: offsets.startPosition,
end: offsets.endPosition
};
let template = new TokenizerEventHandlers(source, entityParser, mode).parse(ast, options.locals ?? []);
if (options.plugins?.ast) for (const transform of options.plugins.ast) traverse(template, transform(assign({}, options, {
syntax: syntax
}, {
plugins: void 0
})).visitor);
return template;
}
/**
* Gets the correct Token from the Node based on it's type
*/
/**
* Adds tokens to the tokensSet based on their node.type
*/
function addTokens(tokensSet, node, scopedTokens, options) {
const maybePathName = function(node, scopedTokens, options) {
if ("PathExpression" === node.type) {
if ("AtHead" === node.head.type || "ThisHead" === node.head.type) return;
const possbleToken = node.head.name;
if (-1 === scopedTokens.indexOf(possbleToken)) return possbleToken;
} else if ("ElementNode" === node.type) {
const {tag: tag} = node, char = tag.charAt(0);
if (":" === char || "@" === char) return;
if (!options.includeHtmlElements && -1 === tag.indexOf(".") && tag.toLowerCase() === tag) return;
// eslint-disable-next-line @typescript-eslint/no-deprecated -- @fixme
if ("this." === tag.substr(0, 5)) return;
// the tag may be from a yielded object
// example:
// <x.button>
// An ElementNode does not parse the "tag" in to a PathExpression
// so we have to split on `.`, just like how `this` presence is checked.
if (tag.includes(".")) {
let [potentialLocal] = tag.split(".");
if (scopedTokens.includes(potentialLocal)) return;
}
if (scopedTokens.includes(tag)) return;
return tag;
}
}(node, scopedTokens, options);
if (void 0 !== maybePathName && "@" !== maybePathName[0]) {
const maybeFirstPathSegment = maybePathName.split(".")[0];
maybeFirstPathSegment && !scopedTokens.includes(maybeFirstPathSegment) && tokensSet.add(maybeFirstPathSegment);
}
}
/**
* Parses and traverses a given handlebars html template to extract all template locals
* referenced that could possible come from the parent scope. Can exclude known keywords
* optionally.
*/ function getTemplateLocals(html, options = {
includeHtmlElements: !1,
includeKeywords: !1
}) {
const ast = preprocess(html), tokensSet = new Set, scopedTokens = [];
traverse(ast, {
Block: {
enter({blockParams: blockParams}) {
blockParams.forEach((param => {
scopedTokens.push(param);
}));
},
exit({blockParams: blockParams}) {
blockParams.forEach((() => {
scopedTokens.pop();
}));
}
},
ElementNode: {
enter(node) {
node.blockParams.forEach((param => {
scopedTokens.push(param);
})), addTokens(tokensSet, node, scopedTokens, options);
},
exit({blockParams: blockParams}) {
blockParams.forEach((() => {
scopedTokens.pop();
}));
}
},
PathExpression(node) {
addTokens(tokensSet, node, scopedTokens, options);
}
});
let tokens = [];
return tokensSet.forEach((s => tokens.push(s))), options.includeKeywords || (tokens = tokens.filter((token => !isKeyword(token)))),
tokens;
}
function node(name) {
if (void 0 !== name) {
const type = name;
return {
fields: () => class {
constructor(fields) {
this.type = type, assign(this, fields);
}
}
};
}
return {
fields: () => class {
constructor(fields) {
assign(this, fields);
}
}
};
}
/**
* Corresponds to syntaxes with positional and named arguments:
*
* - SubExpression
* - Invoking Append
* - Invoking attributes
* - InvokeBlock
*
* If `Args` is empty, the `SourceOffsets` for this node should be the collapsed position
* immediately after the parent call node's `callee`.
*/ class Args extends(node().fields()){
static empty(loc) {
return new Args({
loc: loc,
positional: PositionalArguments.empty(loc),
named: NamedArguments.empty(loc)
});
}
static named(named) {
return new Args({
loc: named.loc,
positional: PositionalArguments.empty(named.loc.collapse("end")),
named: named
});
}
nth(offset) {
return this.positional.nth(offset);
}
get(name) {
return this.named.get(name);
}
isEmpty() {
return this.positional.isEmpty() && this.named.isEmpty();
}
}
/**
* Corresponds to positional arguments.
*
* If `PositionalArguments` is empty, the `SourceOffsets` for this node should be the collapsed
* position immediately after the parent call node's `callee`.
*/ class PositionalArguments extends(node().fields()){
static empty(loc) {
return new PositionalArguments({
loc: loc,
exprs: []
});
}
get size() {
return this.exprs.length;
}
nth(offset) {
return this.exprs[offset] || null;
}
isEmpty() {
return 0 === this.exprs.length;
}
}
/**
* Corresponds to named arguments.
*
* If `PositionalArguments` and `NamedArguments` are empty, the `SourceOffsets` for this node should
* be the same as the `Args` node that contains this node.
*
* If `PositionalArguments` is not empty but `NamedArguments` is empty, the `SourceOffsets` for this
* node should be the collapsed position immediately after the last positional argument.
*/ class NamedArguments extends(node().fields()){
static empty(loc) {
return new NamedArguments({
loc: loc,
entries: []
});
}
get size() {
return this.entries.length;
}
get(name) {
let entry = this.entries.filter((e => e.name.chars === name))[0];
return entry ? entry.value : null;
}
isEmpty() {
return 0 === this.entries.length;
}
}
/**
* Corresponds to a single named argument.
*
* ```hbs
* x=<expr>
* ```
*/ class NamedArgument {
constructor(options) {
this.loc = options.name.loc.extend(options.value.loc), this.name = options.name,
this.value = options.value;
}
}
/**
* `HtmlAttr` nodes are valid HTML attributes, with or without a value.
*
* Exceptions:
*
* - `...attributes` is `SplatAttr`
* - `@x=<value>` is `ComponentArg`
*/ class HtmlAttr extends(node("HtmlAttr").fields()){}
class SplatAttr extends(node("SplatAttr").fields()){}
/**
* Corresponds to an argument passed by a component (`@x=<value>`)
*/ class ComponentArg extends(node().fields()){
/**
* Convert the component argument into a named argument node
*/
toNamedArgument() {
return new NamedArgument({
name: this.name,
value: this.value
});
}
}
/**
* An `ElementModifier` is just a normal call node in modifier position.
*/ class ElementModifier extends(node("ElementModifier").fields()){}
class GlimmerComment extends(node("GlimmerComment").fields()){}
class HtmlText extends(node("HtmlText").fields()){}
class HtmlComment extends(node("HtmlComment").fields()){}
class AppendContent extends(node("AppendContent").fields()){
get callee() {
return "Call" === this.value.type ? this.value.callee : this.value;
}
get args() {
return "Call" === this.value.type ? this.value.args : Args.empty(this.value.loc.collapse("end"));
}
}
class InvokeBlock extends(node("InvokeBlock").fields()){}
/**
* Corresponds to a component invocation. When the content of a component invocation contains no
* named blocks, `blocks` contains a single named block named `"default"`. When a component
* invocation is self-closing, `blocks` is empty.
*/ class InvokeComponent extends(node("InvokeComponent").fields()){
get args() {
let entries = this.componentArgs.map((a => a.toNamedArgument()));
return Args.named(new NamedArguments({
loc: SpanList.range(entries, this.callee.loc.collapse("end")),
entries: entries
}));
}
}
/**
* Corresponds to a simple HTML element. The AST allows component arguments and modifiers to support
* future extensions.
*/ class SimpleElement extends(node("SimpleElement").fields()){
get args() {
let entries = this.componentArgs.map((a => a.toNamedArgument()));
return Args.named(new NamedArguments({
loc: SpanList.range(entries, this.tag.loc.collapse("end")),
entries: entries
}));
}
}
/**
* Corresponds to a Handlebars literal.
*
* @see {LiteralValue}
*/ class LiteralExpression extends(node("Literal").fields()){
toSlice() {
return new SourceSlice({
loc: this.loc,
chars: this.value
});
}
}
/**
* Returns true if an input {@see ExpressionNode} is a literal.
*/
/**
* Corresponds to a path in expression position.
*
* ```hbs
* this
* this.x
* @x
* @x.y
* x
* x.y
* ```
*/
class PathExpression extends(node("Path").fields()){}
/**
* Corresponds to a known strict-mode keyword. It behaves similarly to a
* PathExpression with a FreeVarReference, but implies StrictResolution and
* is guaranteed to not have a tail, since `{{outlet.foo}}` would have been
* illegal.
*/ class KeywordExpression extends(node("Keyword").fields()){}
/**
* Corresponds to a parenthesized call expression.
*
* ```hbs
* (x)
* (x.y)
* (x y)
* (x.y z)
* ```
*/ class CallExpression extends(node("Call").fields()){}
/**
* Corresponds to an interpolation in attribute value position.
*
* ```hbs
* <a href="{{url}}.html"
* ```
*/ class InterpolateExpression extends(node("Interpolate").fields()){}
/**
* Corresponds to an entire template.
*/ class Template extends(node().fields()){}
/**
* Represents a block. In principle this could be merged with `NamedBlock`, because all cases
* involving blocks have at least a notional name.
*/ class Block extends(node().fields()){}
/**
* Corresponds to a collection of named blocks.
*/ class NamedBlocks extends(node().fields()){
get(name) {
return this.blocks.filter((block => block.name.chars === name))[0] || null;
}
}
/**
* Corresponds to a single named block. This is used for anonymous named blocks (`default` and
* `else`).
*/ class NamedBlock extends(node().fields()){
get args() {
let entries = this.componentArgs.map((a => a.toNamedArgument()));
return Args.named(new NamedArguments({
loc: SpanList.range(entries, this.name.loc.collapse("end")),
entries: entries
}));
}
}
/**
* Corresponds to `this` at the head of an expression.
*/ class ThisReference extends(node("This").fields()){}
/**
* Corresponds to `@<ident>` at the beginning of an expression.
*/ class ArgReference extends(node("Arg").fields()){}
/**
* Corresponds to `<ident>` at the beginning of an expression, when `<ident>` is in the current
* block's scope.
*/ class LocalVarReference extends(node("Local").fields()){}
/**
* Corresponds to `<ident>` at the beginning of an expression, when `<ident>` is *not* in the
* current block's scope.
*
* The `resolution: FreeVarResolution` field describes how to resolve the free variable.
*
* Note: In strict mode, it must always be a variable that is in a concrete JavaScript scope that
* the template will be installed into.
*/ class FreeVarReference extends(node("Free").fields()){}
/// FreeVarNamespace ///
const STRICT_RESOLUTION = {
resolution: () => SexpOpcodes.GetStrictKeyword,
serialize: () => "Strict",
isAngleBracket: !1
}, HTML_RESOLUTION = {
...STRICT_RESOLUTION,
isAngleBracket: !0
};
/**
* A `LooseModeResolution` includes one or more namespaces to resolve the variable in
*
* In practice, there are a limited number of possible combinations of these degrees of freedom,
* and they are captured by the `Namespaces` union below.
*/
class LooseModeResolution {
/**
* Namespaced resolution is used in an unambiguous syntax position:
*
* 1. `(sexp)` (namespace: `Helper`)
* 2. `{{#block}}` (namespace: `Component`)
* 3. `<a {{modifier}}>` (namespace: `Modifier`)
* 4. `<Component />` (namespace: `Component`)
*/
static namespaced(namespace, isAngleBracket = !1) {
return new LooseModeResolution([ namespace ], isAngleBracket);
}
/**
* Append resolution is used when the variable should be resolved in both the `component` and
* `helper` namespaces.
*
* ```hbs
* {{x}}
* ```
*
* ```hbs
* {{x y}}
* ```
*
* ^ In either case, `x` should be resolved in the `component` and `helper` namespaces.
*/ static append() {
return new LooseModeResolution([ "Component", "Helper" ]);
}
/**
* Trusting append resolution is used when the variable should be resolved only in the
* `helper` namespaces.
*
* ```hbs
* {{{x}}}
* ```
*
* ```hbs
* {{{x y}}}
* ```
*
* ^ In either case, `x` should be resolved in the `helper` namespace.
*/ static trustingAppend() {
return this.namespaced("Helper");
}
constructor(namespaces, isAngleBracket = !1) {
this.namespaces = namespaces, this.isAngleBracket = isAngleBracket;
}
resolution() {
if (1 !== this.namespaces.length) return SexpOpcodes.GetFreeAsComponentOrHelperHead;
switch (this.namespaces[0]) {
case "Helper":
return SexpOpcodes.GetFreeAsHelperHead;
case "Modifier":
return SexpOpcodes.GetFreeAsModifierHead;
case "Component":
return SexpOpcodes.GetFreeAsComponentHead;
}
}
serialize() {
return 1 === this.namespaces.length ? this.namespaces[0] : "ComponentOrHelper";
}
}
var api = Object.freeze({
__proto__: null,
AppendContent: AppendContent,
ArgReference: ArgReference,
Args: Args,
Block: Block,
COMPONENT_NAMESPACE: "Component",
CallExpression: CallExpression,
ComponentArg: ComponentArg,
ElementModifier: ElementModifier,
FreeVarReference: FreeVarReference,
GlimmerComment: GlimmerComment,
HELPER_NAMESPACE: "Helper",
HTML_RESOLUTION: HTML_RESOLUTION,
HtmlAttr: HtmlAttr,
HtmlComment: HtmlComment,
HtmlText: HtmlText,
InterpolateExpression: InterpolateExpression,
InvokeBlock: InvokeBlock,
InvokeComponent: InvokeComponent,
KeywordExpression: KeywordExpression,
LiteralExpression: LiteralExpression,
LocalVarReference: LocalVarReference,
LooseModeResolution: LooseModeResolution,
MODIFIER_NAMESPACE: "Modifier",
NamedArgument: NamedArgument,
NamedArguments: NamedArguments,
NamedBlock: NamedBlock,
NamedBlocks: NamedBlocks,
PathExpression: PathExpression,
PositionalArguments: PositionalArguments,
STRICT_RESOLUTION: STRICT_RESOLUTION,
SimpleElement: SimpleElement,
SplatAttr: SplatAttr,
Template: Template,
ThisReference: ThisReference,
isLiteral: function(node, kind) {
return "Literal" === node.type && (void 0 === kind || ("null" === kind ? null === node.value : typeof node.value === kind));
},
isStrictResolution: function(value) {
return value === STRICT_RESOLUTION;
},
loadResolution: function(resolution) {
return "Strict" === resolution ? STRICT_RESOLUTION : "ComponentOrHelper" === resolution ? LooseModeResolution.append() : LooseModeResolution.namespaced(resolution);
},
node: node
});
class SymbolTable {
static top(locals, keywords, options) {
return new ProgramSymbolTable(locals, keywords, options);
}
child(locals) {
let symbols = locals.map((name => this.allocate(name)));
return new BlockSymbolTable(this, locals, symbols);
}
}
class ProgramSymbolTable extends SymbolTable {
constructor(templateLocals, keywords, options) {
super(), this.templateLocals = templateLocals, this.keywords = keywords, this.options = options,
this.symbols = [], this.upvars = [], this.size = 1, this.named = dict(), this.blocks = dict(),
this.usedTemplateLocals = [];
}
root() {
return this;
}
hasLexical(name) {
return this.options.lexicalScope(name);
}
hasKeyword(name) {
return this.keywords.includes(name);
}
getKeyword(name) {
return this.allocateFree(name, STRICT_RESOLUTION);
}
getUsedTemplateLocals() {
return this.usedTemplateLocals;
}
has(name) {
return this.templateLocals.includes(name);
}
get(name) {
let index = this.usedTemplateLocals.indexOf(name);
return -1 !== index || (index = this.usedTemplateLocals.length, this.usedTemplateLocals.push(name)),
[ index, !0 ];
}
getLocalsMap() {
return dict();
}
getDebugInfo() {
return [ this.getLocalsMap(), this.named ];
}
allocateFree(name, resolution) {
// If the name in question is an uppercase (i.e. angle-bracket) component invocation, run
// the optional `customizeComponentName` function provided to the precompiler.
resolution.resolution() === SexpOpcodes.GetFreeAsComponentHead && resolution.isAngleBracket && (name = this.options.customizeComponentName(name));
let index = this.upvars.indexOf(name);
return -1 !== index || (index = this.upvars.length, this.upvars.push(name)), index;
}
allocateNamed(name) {
let named = this.named[name];
return named || (named = this.named[name] = this.allocate(name)), named;
}
allocateBlock(name) {
"inverse" === name && (name = "else");
let block = this.blocks[name];
return block || (block = this.blocks[name] = this.allocate(`&${name}`)), block;
}
allocate(identifier) {
return this.symbols.push(identifier), this.size++;
}
}
class BlockSymbolTable extends SymbolTable {
constructor(parent, symbols, slots) {
super(), this.parent = parent, this.symbols = symbols, this.slots = slots;
}
root() {
return this.parent.root();
}
get locals() {
return this.symbols;
}
hasLexical(name) {
return this.parent.hasLexical(name);
}
getKeyword(name) {
return this.parent.getKeyword(name);
}
hasKeyword(name) {
return this.parent.hasKeyword(name);
}
has(name) {
return -1 !== this.symbols.indexOf(name) || this.parent.has(name);
}
get(name) {
let local = this.#get(name);
return local ? [ local, !1 ] : this.parent.get(name);
}
#get(name) {
let slot = this.symbols.indexOf(name);
return -1 === slot ? null : this.slots[slot];
}
getLocalsMap() {
let dict = this.parent.getLocalsMap();
return this.symbols.forEach((symbol => dict[symbol] = this.get(symbol)[0])), dict;
}
getDebugInfo() {
const locals = this.getLocalsMap(), root = this.root();
return [ {
...locals,
...root.named
}, Object.fromEntries(root.upvars.map(((s, i) => [ s, i ]))) ];
}
allocateFree(name, resolution) {
return this.parent.allocateFree(name, resolution);
}
allocateNamed(name) {
return this.parent.allocateNamed(name);
}
allocateBlock(name) {
return this.parent.allocateBlock(name);
}
allocate(identifier) {
return this.parent.allocate(identifier);
}
}
class Builder {
// TEMPLATE //
template(symbols, body, loc) {
return new Template({
table: symbols,
body: body,
loc: loc
});
}
// INTERNAL (these nodes cannot be reached when doing general-purpose visiting) //
block(symbols, body, loc) {
return new Block({
scope: symbols,
body: body,
loc: loc
});
}
namedBlock(name, block, loc) {
return new NamedBlock({
name: name,
block: block,
attrs: [],
componentArgs: [],
modifiers: [],
loc: loc
});
}
simpleNamedBlock(name, block, loc) {
return new BuildElement({
selfClosing: !1,
attrs: [],
componentArgs: [],
modifiers: [],
comments: []
}).named(name, block, loc);
}
slice(chars, loc) {
return new SourceSlice({
loc: loc,
chars: chars
});
}
args(positional, named, loc) {
return new Args({
loc: loc,
positional: positional,
named: named
});
}
positional(exprs, loc) {
return new PositionalArguments({
loc: loc,
exprs: exprs
});
}
namedArgument(key, value) {
return new NamedArgument({
name: key,
value: value
});
}
named(entries, loc) {
return new NamedArguments({
loc: loc,
entries: entries
});
}
attr({name: name, value: value, trusting: trusting}, loc) {
return new HtmlAttr({
loc: loc,
name: name,
value: value,
trusting: trusting
});
}
splatAttr(symbol, loc) {
return new SplatAttr({
symbol: symbol,
loc: loc
});
}
arg({name: name, value: value, trusting: trusting}, loc) {
return new ComponentArg({
name: name,
value: value,
trusting: trusting,
loc: loc
});
}
// EXPRESSIONS //
path(head, tail, loc) {
return new PathExpression({
loc: loc,
ref: head,
tail: tail
});
}
keyword(name, symbol, loc) {
return new KeywordExpression({
loc: loc,
name: name,
symbol: symbol
});
}
self(loc) {
return new ThisReference({
loc: loc
});
}
at(name, symbol, loc) {
return name[0], new ArgReference({
loc: loc,
name: new SourceSlice({
loc: loc,
chars: name
}),
symbol: symbol
});
}
freeVar({name: name, context: context, symbol: symbol, loc: loc}) {
return name[0], new FreeVarReference({
name: name,
resolution: context,
symbol: symbol,
loc: loc
});
}
localVar(name, symbol, isTemplateLocal, loc) {
return name[0], new LocalVarReference({
loc: loc,
name: name,
isTemplateLocal: isTemplateLocal,
symbol: symbol
});
}
sexp(parts, loc) {
return new CallExpression({
loc: loc,
callee: parts.callee,
args: parts.args
});
}
interpolate(parts, loc) {
return new InterpolateExpression({
loc: loc,
parts: parts
});
}
literal(value, loc) {
return new LiteralExpression({
loc: loc,
value: value
});
}
// STATEMENTS //
append({table: table, trusting: trusting, value: value}, loc) {
return new AppendContent({
table: table,
trusting: trusting,
value: value,
loc: loc
});
}
modifier({callee: callee, args: args}, loc) {
return new ElementModifier({
loc: loc,
callee: callee,
args: args
});
}
namedBlocks(blocks, loc) {
return new NamedBlocks({
loc: loc,
blocks: blocks
});
}
blockStatement({program: program, inverse: inverse = null, ...call}, loc) {
let blocksLoc = program.loc, blocks = [ this.namedBlock(SourceSlice.synthetic("default"), program, program.loc) ];
return inverse && (blocksLoc = blocksLoc.extend(inverse.loc), blocks.push(this.namedBlock(SourceSlice.synthetic("else"), inverse, inverse.loc))),
new InvokeBlock({
loc: loc,
blocks: this.namedBlocks(blocks, blocksLoc),
callee: call.callee,
args: call.args
});
}
element(options) {
return new BuildElement(options);
}
}
class BuildElement {
constructor(base) {
this.base = base, this.builder = new Builder;
}
simple(tag, body, loc) {
return new SimpleElement(assign({
tag: tag,
body: body,
componentArgs: [],
loc: loc
}, this.base));
}
named(name, block, loc) {
return new NamedBlock(assign({
name: name,
block: block,
componentArgs: [],
loc: loc
}, this.base));
}
selfClosingComponent(callee, loc) {
return new InvokeComponent(assign({
loc: loc,
callee: callee,
// point the empty named blocks at the `/` self-closing tag
blocks: new NamedBlocks({
blocks: [],
loc: loc.sliceEndChars({
skipEnd: 1,
chars: 1
})
})
}, this.base));
}
componentWithDefaultBlock(callee, children, symbols, loc) {
let block = this.builder.block(symbols, children, loc), namedBlock = this.builder.namedBlock(SourceSlice.synthetic("default"), block, loc);
// BUILDER.simpleNamedBlock('default', children, symbols, loc);
return new InvokeComponent(assign({
loc: loc,
callee: callee,
blocks: this.builder.namedBlocks([ namedBlock ], namedBlock.loc)
}, this.base));
}
componentWithNamedBlocks(callee, blocks, loc) {
return new InvokeComponent(assign({
loc: loc,
callee: callee,
blocks: this.builder.namedBlocks(blocks, SpanList.range(blocks))
}, this.base));
}
}
function SexpSyntaxContext(node) {
return isSimpleCallee(node) ? LooseModeResolution.namespaced("Helper") : null;
}
function ModifierSyntaxContext(node) {
return isSimpleCallee(node) ? LooseModeResolution.namespaced("Modifier") : null;
}
function BlockSyntaxContext(node) {
return isSimpleCallee(node) ? LooseModeResolution.namespaced("Component") : null;
}
function ComponentSyntaxContext(node) {
return isSimplePath(node) ? LooseModeResolution.namespaced("Component", !0) : null;
}
/**
* This corresponds to attribute curlies (<Foo bar={{...}}>).
* In strict mode, this also corresponds to arg curlies.
*/ function AttrValueSyntaxContext(node) {
return isSimpleCallee(node) ? LooseModeResolution.namespaced("Helper") : null;
}
/**
* This corresponds to append positions text curlies.
*/ function AppendSyntaxContext(node) {
let isSimple = isSimpleCallee(node), trusting = node.trusting;
return isSimple ? trusting ? LooseModeResolution.trustingAppend() : LooseModeResolution.append() : null;
}
// UTILITIES
/**
* A call node has a simple callee if its head is:
*
* - a `PathExpression`
* - the `PathExpression`'s head is a `VarHead`
* - it has no tail
*
* Simple heads:
*
* ```
* {{x}}
* {{x y}}
* ```
*
* Not simple heads:
*
* ```
* {{x.y}}
* {{x.y z}}
* {{@x}}
* {{@x a}}
* {{this}}
* {{this a}}
* ```
*/ function isSimpleCallee(node) {
return isSimplePath(node.path);
}
function isSimplePath(node) {
return "PathExpression" === node.type && "VarHead" === node.head.type && 0 === node.tail.length;
}
function normalize(source, options = {
lexicalScope: () => !1
}) {
let ast = preprocess(source, options), normalizeOptions = {
strictMode: !1,
...options,
locals: ast.blockParams,
keywords: options.keywords ?? []
}, top = SymbolTable.top(normalizeOptions.locals, normalizeOptions.keywords, {
customizeComponentName: options.customizeComponentName ?? (name => name),
lexicalScope: options.lexicalScope
}), block = new BlockContext(source, normalizeOptions, top), normalizer = new StatementNormalizer(block), astV2 = new TemplateChildren(block.loc(ast.loc), ast.body.map((b => normalizer.normalize(b))), block).assertTemplate(top);
return [ astV2, top.getUsedTemplateLocals() ];
}
/**
* A `BlockContext` represents the block that a particular AST node is contained inside of.
*
* `BlockContext` is aware of template-wide options (such as strict mode), as well as the bindings
* that are in-scope within that block.
*
* Concretely, it has the `PrecompileOptions` and current `SymbolTable`, and provides
* facilities for working with those options.
*
* `BlockContext` is stateless.
*/ class BlockContext {
constructor(source, options, table) {
this.source = source, this.options = options, this.table = table, this.builder = new Builder;
}
get strict() {
return this.options.strictMode || !1;
}
loc(loc) {
return this.source.spanFor(loc);
}
resolutionFor(node, resolution) {
if (this.strict) return {
result: STRICT_RESOLUTION
};
if (this.isFreeVar(node)) {
let r = resolution(node);
return null === r ? {
result: "error",
path: printPath(node),
head: printHead(node)
} : {
result: r
};
}
return {
result: STRICT_RESOLUTION
};
}
isLexicalVar(variable) {
return this.table.hasLexical(variable);
}
isKeyword(name) {
return this.strict && !this.table.hasLexical(name) && this.table.hasKeyword(name);
}
isFreeVar(callee) {
return "PathExpression" === callee.type ? "VarHead" === callee.head.type && !this.table.has(callee.head.name) : "PathExpression" === callee.path.type && this.isFreeVar(callee.path);
}
hasBinding(name) {
return this.table.has(name) || this.table.hasLexical(name);
}
child(blockParams) {
return new BlockContext(this.source, this.options, this.table.child(blockParams));
}
customizeComponentName(input) {
return this.options.customizeComponentName ? this.options.customizeComponentName(input) : input;
}
}
/**
* An `ExpressionNormalizer` normalizes expressions within a block.
*
* `ExpressionNormalizer` is stateless.
*/ class ExpressionNormalizer {
constructor(block) {
this.block = block;
}
normalize(expr, resolution) {
switch (expr.type) {
case "NullLiteral":
case "BooleanLiteral":
case "NumberLiteral":
case "StringLiteral":
case "UndefinedLiteral":
return this.block.builder.literal(expr.value, this.block.loc(expr.loc));
case "PathExpression":
return this.path(expr, resolution);
case "SubExpression":
{
// expr.path used to incorrectly have the type ASTv1.Expression
isLiteral(expr.path) && assertIllegalLiteral(expr.path, expr.loc);
let resolution = this.block.resolutionFor(expr, SexpSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to invoke a path (\`${resolution.path}\`) but ${resolution.head} was not in scope`, expr.loc);
return this.block.builder.sexp(this.callParts(expr, resolution.result), this.block.loc(expr.loc));
}
}
}
path(expr, resolution) {
let loc = this.block.loc(expr.loc);
if ("VarHead" === expr.head.type && 0 === expr.tail.length && this.block.isKeyword(expr.head.name)) return this.block.builder.keyword(expr.head.name, this.block.table.getKeyword(expr.head.name), loc);
let tail = [], offset = this.block.loc(expr.head.loc);
for (let part of expr.tail) offset = offset.sliceStartChars({
chars: part.length,
skipStart: 1
}), tail.push(new SourceSlice({
loc: offset,
chars: part
}));
return this.block.builder.path(this.ref(expr.head, resolution), tail, loc);
}
/**
* The `callParts` method takes ASTv1.CallParts as well as a syntax context and normalizes
* it to an ASTv2 CallParts.
*/ callParts(parts, context) {
let {path: path, params: params, hash: hash, loc: loc} = parts, callee = this.normalize(path, context), paramList = params.map((p => this.normalize(p, STRICT_RESOLUTION))), paramLoc = SpanList.range(paramList, callee.loc.collapse("end")), namedLoc = this.block.loc(hash.loc), argsLoc = SpanList.range([ paramLoc, namedLoc ]), positional = this.block.builder.positional(params.map((p => this.normalize(p, STRICT_RESOLUTION))), paramLoc), named = this.block.builder.named(hash.pairs.map((p => this.namedArgument(p))), this.block.loc(hash.loc));
switch (callee.type) {
case "Literal":
throw generateSyntaxError(`Invalid invocation of a literal value (\`${callee.value}\`)`, loc);
// This really shouldn't be possible, something has gone pretty wrong
case "Interpolate":
throw generateSyntaxError("Invalid invocation of a interpolated string", loc);
}
return {
callee: callee,
args: this.block.builder.args(positional, named, argsLoc)
};
}
namedArgument(pair) {
let keyOffsets = this.block.loc(pair.loc).sliceStartChars({
chars: pair.key.length
});
return this.block.builder.namedArgument(new SourceSlice({
chars: pair.key,
loc: keyOffsets
}), this.normalize(pair.value, STRICT_RESOLUTION));
}
/**
* The `ref` method normalizes an `ASTv1.PathHead` into an `ASTv2.VariableReference`.
* This method is extremely important, because it is responsible for normalizing free
* variables into an an ASTv2.PathHead *with appropriate context*.
*
* The syntax context is originally determined by the syntactic position that this `PathHead`
* came from, and is ultimately attached to the `ASTv2.VariableReference` here. In ASTv2,
* the `VariableReference` node bears full responsibility for loose mode rules that control
* the behavior of free variables.
*/ ref(head, resolution) {
let {block: block} = this, {builder: builder, table: table} = block, offsets = block.loc(head.loc);
switch (head.type) {
case "ThisHead":
if (block.hasBinding("this")) {
let [symbol, isRoot] = table.get("this");
return block.builder.localVar("this", symbol, isRoot, offsets);
}
return builder.self(offsets);
case "AtHead":
{
let symbol = table.allocateNamed(head.name);
return builder.at(head.name, symbol, offsets);
}
case "VarHead":
if (block.hasBinding(head.name)) {
let [symbol, isRoot] = table.get(head.name);
return block.builder.localVar(head.name, symbol, isRoot, offsets);
}
{
let context = block.strict ? STRICT_RESOLUTION : resolution, symbol = block.table.allocateFree(head.name, context);
return block.builder.freeVar({
name: head.name,
context: context,
symbol: symbol,
loc: offsets
});
}
}
}
}
/**
* `TemplateNormalizer` normalizes top-level ASTv1 statements to ASTv2.
*/ class StatementNormalizer {
constructor(block) {
this.block = block;
}
normalize(node) {
switch (node.type) {
case "BlockStatement":
return this.BlockStatement(node);
case "ElementNode":
return new ElementNormalizer(this.block).ElementNode(node);
case "MustacheStatement":
return this.MustacheStatement(node);
// These are the same in ASTv2
case "MustacheCommentStatement":
return this.MustacheCommentStatement(node);
case "CommentStatement":
{
let loc = this.block.loc(node.loc);
return new HtmlComment({
loc: loc,
text: loc.slice({
skipStart: 4,
skipEnd: 3
}).toSlice(node.value)
});
}
case "TextNode":
return new HtmlText({
loc: this.block.loc(node.loc),
chars: node.chars
});
}
}
MustacheCommentStatement(node) {
let textLoc, loc = this.block.loc(node.loc);
return textLoc = "{{!--" === loc.asString().slice(0, 5) ? loc.slice({
skipStart: 5,
skipEnd: 4
}) : loc.slice({
skipStart: 3,
skipEnd: 2
}), new GlimmerComment({
loc: loc,
text: textLoc.toSlice(node.value)
});
}
/**
* Normalizes an ASTv1.MustacheStatement to an ASTv2.AppendStatement
*/ MustacheStatement(mustache) {
let value, {path: path, params: params, hash: hash, trusting: trusting} = mustache, loc = this.block.loc(mustache.loc);
if (isLiteral(path)) 0 === params.length && 0 === hash.pairs.length ? value = this.expr.normalize(path) : assertIllegalLiteral(path, loc); else {
let resolution = this.block.resolutionFor(mustache, AppendSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to render a path (\`{{${resolution.path}}}\`), but ${resolution.head} was not in scope`, loc);
// Normalize the call parts in AppendSyntaxContext
let callParts = this.expr.callParts({
path: path,
params: params,
hash: hash,
loc: loc
}, resolution.result);
value = callParts.args.isEmpty() ? callParts.callee : this.block.builder.sexp(callParts, loc);
}
return this.block.builder.append({
table: this.block.table,
trusting: trusting,
value: value
}, loc);
}
/**
* Normalizes a ASTv1.BlockStatement to an ASTv2.BlockStatement
*/ BlockStatement(block) {
let {program: program, inverse: inverse} = block, loc = this.block.loc(block.loc);
// block.path used to incorrectly have the type ASTv1.Expression
isLiteral(block.path) && assertIllegalLiteral(block.path, loc);
let resolution = this.block.resolutionFor(block, BlockSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to invoke a path (\`{{#${resolution.path}}}\`) but ${resolution.head} was not in scope`, loc);
let callParts = this.expr.callParts(block, resolution.result);
return this.block.builder.blockStatement(assign({
symbols: this.block.table,
program: this.Block(program),
inverse: inverse ? this.Block(inverse) : null
}, callParts), loc);
}
Block({body: body, loc: loc, blockParams: blockParams}) {
let child = this.block.child(blockParams), normalizer = new StatementNormalizer(child);
return new BlockChildren(this.block.loc(loc), body.map((b => normalizer.normalize(b))), this.block).assertBlock(child.table);
}
get expr() {
return new ExpressionNormalizer(this.block);
}
}
class ElementNormalizer {
constructor(ctx) {
this.ctx = ctx;
}
/**
* Normalizes an ASTv1.ElementNode to:
*
* - ASTv2.NamedBlock if the tag name begins with `:`
* - ASTv2.Component if the tag name matches the component heuristics
* - ASTv2.SimpleElement if the tag name doesn't match the component heuristics
*
* A tag name represents a component if:
*
* - it begins with `@`
* - it is exactly `this` or begins with `this.`
* - the part before the first `.` is a reference to an in-scope variable binding
* - it begins with an uppercase character
*/ ElementNode(element) {
let {tag: tag, selfClosing: selfClosing, comments: comments} = element, loc = this.ctx.loc(element.loc), [tagHead, ...rest] = tag.split("."), path = this.classifyTag(tagHead, rest, element.loc), attrs = element.attributes.filter((a => "@" !== a.name[0])).map((a => this.attr(a))), args = element.attributes.filter((a => "@" === a.name[0])).map((a => this.arg(a))), modifiers = element.modifiers.map((m => this.modifier(m))), child = this.ctx.child(element.blockParams), normalizer = new StatementNormalizer(child), childNodes = element.children.map((s => normalizer.normalize(s))), el = this.ctx.builder.element({
selfClosing: selfClosing,
attrs: attrs,
componentArgs: args,
modifiers: modifiers,
comments: comments.map((c => new StatementNormalizer(this.ctx).MustacheCommentStatement(c)))
}), children = new ElementChildren(el, loc, childNodes, this.ctx), tagOffsets = this.ctx.loc(element.loc).sliceStartChars({
chars: tag.length,
skipStart: 1
});
if ("ElementHead" === path) return ":" === tag[0] ? children.assertNamedBlock(tagOffsets.slice({
skipStart: 1
}).toSlice(tag.slice(1)), child.table) : children.assertElement(tagOffsets.toSlice(tag), element.blockParams.length > 0);
if (element.selfClosing) return el.selfClosingComponent(path, loc);
{
let blocks = children.assertComponent(tag, child.table, element.blockParams.length > 0);
return el.componentWithNamedBlocks(path, blocks, loc);
}
}
modifier(m) {
// modifier.path used to incorrectly have the type ASTv1.Expression
isLiteral(m.path) && assertIllegalLiteral(m.path, m.loc);
let resolution = this.ctx.resolutionFor(m, ModifierSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to invoke a path (\`{{${resolution.path}}}\`) as a modifier, but ${resolution.head} was not in scope`, m.loc);
let callParts = this.expr.callParts(m, resolution.result);
return this.ctx.builder.modifier(callParts, this.ctx.loc(m.loc));
}
/**
* This method handles attribute values that are curlies, as well as curlies nested inside of
* interpolations:
*
* ```hbs
* <a href={{url}} />
* <a href="{{url}}.html" />
* ```
*/ mustacheAttr(mustache) {
let {path: path, params: params, hash: hash, loc: loc} = mustache;
if (isLiteral(path)) {
if (0 === params.length && 0 === hash.pairs.length) return this.expr.normalize(path);
assertIllegalLiteral(path, loc);
}
// Normalize the call parts in AttrValueSyntaxContext
let resolution = this.ctx.resolutionFor(mustache, AttrValueSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to render a path (\`{{${resolution.path}}}\`), but ${resolution.head} was not in scope`, mustache.loc);
let sexp = this.ctx.builder.sexp(this.expr.callParts(mustache, resolution.result), this.ctx.loc(mustache.loc));
// If there are no params or hash, just return the function part as its own expression
return sexp.args.isEmpty() ? sexp.callee : sexp;
}
/**
* attrPart is the narrowed down list of valid attribute values that are also
* allowed as a concat part (you can't nest concats).
*/ attrPart(part) {
switch (part.type) {
case "MustacheStatement":
return {
expr: this.mustacheAttr(part),
trusting: part.trusting
};
case "TextNode":
return {
expr: this.ctx.builder.literal(part.chars, this.ctx.loc(part.loc)),
trusting: !0
};
}
}
attrValue(part) {
if ("ConcatStatement" === part.type) {
let parts = part.parts.map((p => this.attrPart(p).expr));
return {
expr: this.ctx.builder.interpolate(parts, this.ctx.loc(part.loc)),
trusting: !1
};
}
return this.attrPart(part);
}
attr(m) {
if (m.name[0], "...attributes" === m.name) return this.ctx.builder.splatAttr(this.ctx.table.allocateBlock("attrs"), this.ctx.loc(m.loc));
let offsets = this.ctx.loc(m.loc), nameSlice = offsets.sliceStartChars({
chars: m.name.length
}).toSlice(m.name), value = this.attrValue(m.value);
return this.ctx.builder.attr({
name: nameSlice,
value: value.expr,
trusting: value.trusting
}, offsets);
}
// An arg curly <Foo @bar={{...}} /> is the same as an attribute curly for
// our purposes, except that in loose mode <Foo @bar={{baz}} /> is an error:
checkArgCall(arg) {
let {value: value} = arg;
if ("MustacheStatement" !== value.type) return;
if (0 !== value.params.length || 0 !== value.hash.pairs.length) return;
let {path: path} = value;
if ("PathExpression" !== path.type) return;
if (path.tail.length > 0) return;
let resolution = this.ctx.resolutionFor(path, (() => null));
if ("error" === resolution.result && "has-block" !== resolution.path) throw generateSyntaxError(`You attempted to pass a path as argument (\`${arg.name}={{${resolution.path}}}\`) but ${resolution.head} was not in scope. Try:\n* \`${arg.name}={{this.${resolution.path}}}\` if this is meant to be a property lookup, or\n* \`${arg.name}={{(${resolution.path})}}\` if this is meant to invoke the resolved helper, or\n* \`${arg.name}={{helper "${resolution.path}"}}\` if this is meant to pass the resolved helper by value`, arg.loc);
}
arg(arg) {
arg.name[0], this.checkArgCall(arg);
let offsets = this.ctx.loc(arg.loc), nameSlice = offsets.sliceStartChars({
chars: arg.name.length
}).toSlice(arg.name), value = this.attrValue(arg.value);
return this.ctx.builder.arg({
name: nameSlice,
value: value.expr,
trusting: value.trusting
}, offsets);
}
/**
* This function classifies the head of an ASTv1.Element into an ASTv2.PathHead (if the
* element is a component) or `'ElementHead'` (if the element is a simple element).
*
* Rules:
*
* 1. If the variable is an `@arg`, return an `AtHead`
* 2. If the variable is `this`, return a `ThisHead`
* 3. If the variable is in the current scope:
* a. If the scope is the root scope, then return a Free `LocalVarHead`
* b. Else, return a standard `LocalVarHead`
* 4. If the tag name is a path and the variable is not in the current scope, Syntax Error
* 5. If the variable is uppercase return a FreeVar(ResolveAsComponentHead)
* 6. Otherwise, return `'ElementHead'`
*/ classifyTag(variable, tail, loc) {
let uppercase = (tag = variable)[0] === tag[0]?.toUpperCase() && tag[0] !== tag[0]?.toLowerCase();
var tag;
let inScope = "@" === variable[0] || "this" === variable || this.ctx.hasBinding(variable);
if (this.ctx.strict && !inScope) {
if (uppercase) throw generateSyntaxError(`Attempted to invoke a component that was not in scope in a strict mode template, \`<${variable}>\`. If you wanted to create an element with that name, convert it to lowercase - \`<${variable.toLowerCase()}>\``, loc);
// In strict mode, values are always elements unless they are in scope
return "ElementHead";
}
// Since the parser handed us the HTML element name as a string, we need
// to convert it into an ASTv1 path so it can be processed using the
// expression normalizer.
let isComponent = inScope || uppercase, variableLoc = loc.sliceStartChars({
skipStart: 1,
chars: variable.length
}), tailLength = tail.reduce(((accum, part) => accum + 1 + part.length), 0), pathEnd = variableLoc.getEnd().move(tailLength), pathLoc = variableLoc.withEnd(pathEnd);
if (isComponent) {
let path = b.path({
head: b.head({
original: variable,
loc: variableLoc
}),
tail: tail,
loc: pathLoc
}), resolution = this.ctx.isLexicalVar(variable) ? {
result: STRICT_RESOLUTION
} : this.ctx.resolutionFor(path, ComponentSyntaxContext);
if ("error" === resolution.result) throw generateSyntaxError(`You attempted to invoke a path (\`<${resolution.path}>\`) but ${resolution.head} was not in scope`, loc);
return new ExpressionNormalizer(this.ctx).normalize(path, resolution.result);
}
// If the tag name wasn't a valid component but contained a `.`, it's
// a syntax error.
if (this.ctx.table.allocateFree(variable, STRICT_RESOLUTION), tail.length > 0) throw generateSyntaxError(`You used ${variable}.${tail.join(".")} as a tag name, but ${variable} is not in scope`, loc);
return "ElementHead";
}
get expr() {
return new ExpressionNormalizer(this.ctx);
}
}
class Children {
constructor(loc, children, block) {
this.loc = loc, this.children = children, this.block = block, this.namedBlocks = children.filter((c => c instanceof NamedBlock)),
this.hasSemanticContent = Boolean(children.filter((c => {
if (c instanceof NamedBlock) return !1;
switch (c.type) {
case "GlimmerComment":
case "HtmlComment":
return !1;
case "HtmlText":
return !/^\s*$/u.test(c.chars);
default:
return !0;
}
})).length), this.nonBlockChildren = children.filter((c => !(c instanceof NamedBlock)));
}
}
class TemplateChildren extends Children {
assertTemplate(table) {
if (isPresentArray(this.namedBlocks)) throw generateSyntaxError("Unexpected named block at the top-level of a template", this.loc);
return this.block.builder.template(table, this.nonBlockChildren, this.block.loc(this.loc));
}
}
class BlockChildren extends Children {
assertBlock(table) {
if (isPresentArray(this.namedBlocks)) throw generateSyntaxError("Unexpected named block nested in a normal block", this.loc);
return this.block.builder.block(table, this.nonBlockChildren, this.loc);
}
}
class ElementChildren extends Children {
constructor(el, loc, children, block) {
super(loc, children, block), this.el = el;
}
assertNamedBlock(name, table) {
if (this.el.base.selfClosing) throw generateSyntaxError(`<:${name.chars}/> is not a valid named block: named blocks cannot be self-closing`, this.loc);
if (isPresentArray(this.namedBlocks)) throw generateSyntaxError(`Unexpected named block inside <:${name.chars}> named block: named blocks cannot contain nested named blocks`, this.loc);
if ((tag = name.chars)[0] !== tag[0]?.toLowerCase() || tag[0] === tag[0]?.toUpperCase()) throw generateSyntaxError(`<:${name.chars}> is not a valid named block, and named blocks must begin with a lowercase letter`, this.loc);
var tag;
if (this.el.base.attrs.length > 0 || this.el.base.componentArgs.length > 0 || this.el.base.modifiers.length > 0) throw generateSyntaxError(`named block <:${name.chars}> cannot have attributes, arguments, or modifiers`, this.loc);
let offsets = SpanList.range(this.nonBlockChildren, this.loc);
return this.block.builder.namedBlock(name, this.block.builder.block(table, this.nonBlockChildren, offsets), this.loc);
}
assertElement(name, hasBlockParams) {
if (hasBlockParams) throw generateSyntaxError(`Unexpected block params in <${name.chars}>: simple elements cannot have block params`, this.loc);
if (isPresentArray(this.namedBlocks)) {
let names = this.namedBlocks.map((b => b.name));
if (1 === names.length) throw generateSyntaxError(`Unexpected named block <:foo> inside <${name.chars}> HTML element`, this.loc);
{
let printedNames = names.map((n => `<:${n.chars}>`)).join(", ");
throw generateSyntaxError(`Unexpected named blocks inside <${name.chars}> HTML element (${printedNames})`, this.loc);
}
}
return this.el.simple(name, this.nonBlockChildren, this.loc);
}
assertComponent(name, table, hasBlockParams) {
if (isPresentArray(this.namedBlocks) && this.hasSemanticContent) throw generateSyntaxError(`Unexpected content inside <${name}> component invocation: when using named blocks, the tag cannot contain other content`, this.loc);
if (isPresentArray(this.namedBlocks)) {
if (hasBlockParams) throw generateSyntaxError(`Unexpected block params list on <${name}> component invocation: when passing named blocks, the invocation tag cannot take block params`, this.loc);
let seenNames = new Set;
for (let block of this.namedBlocks) {
let name = block.name.chars;
if (seenNames.has(name)) throw generateSyntaxError(`Component had two named blocks with the same name, \`<:${name}>\`. Only one block with a given name may be passed`, this.loc);
if ("inverse" === name && seenNames.has("else") || "else" === name && seenNames.has("inverse")) throw generateSyntaxError("Component has both <:else> and <:inverse> block. <:inverse> is an alias for <:else>", this.loc);
seenNames.add(name);
}
return this.namedBlocks;
}
return [ this.block.builder.namedBlock(SourceSlice.synthetic("default"), this.block.builder.block(table, this.nonBlockChildren, this.loc), this.loc) ];
}
}
function isLiteral(node) {
switch (node.type) {
case "StringLiteral":
case "BooleanLiteral":
case "NumberLiteral":
case "UndefinedLiteral":
case "NullLiteral":
return !0;
default:
return !1;
}
}
function assertIllegalLiteral(node, loc) {
throw generateSyntaxError(`Unexpected literal \`${"StringLiteral" === node.type ? JSON.stringify(node.value) : String(node.value)}\``, loc);
}
function printPath(node) {
return "PathExpression" !== node.type && "PathExpression" === node.path.type ? printPath(node.path) : new Printer({
entityEncoding: "raw"
}).print(node);
}
function printHead(node) {
return "PathExpression" === node.type ? node.head.original : "PathExpression" === node.path.type ? printHead(node.path) : new Printer({
entityEncoding: "raw"
}).print(node);
}
export { api as ASTv2, BlockSymbolTable, KEYWORDS_TYPES, Walker as Path, ProgramSymbolTable, SourceSlice, SpanList, SymbolTable, Walker, WalkerPath, publicBuilder as builders, cannotRemoveNode, cannotReplaceNode, generateSyntaxError, getTemplateLocals, getVoidTags, hasSpan, isKeyword, isVoidTag, loc, maybeLoc, node, normalize, preprocess, build as print, sortByLoc, api$1 as src, traverse, visitorKeys };
//# sourceMappingURL=index.js.map