ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,804 lines (1,731 loc) • 360 kB
JavaScript
import { a as isPresentArray, g as getFirst, b as getLast, u as unwrap, f as asPresentArray, e as expect, d as dict, m as mapPresentArray, h as exhausted } from './collections-C3Y8z_9v.js';
import { a as assert } from './assert-Zqc4wiAV.js';
import { assertNever } from '../@glimmer/util/index.js';
import { s as setLocalDebugType } from './debug-brand-B1TWjOCH.js';
import { a as assign } from './object-utils-AijlD-JH.js';
import { o as opcodes } from './opcodes-CplRyHl_.js';
import { C as CURRIED_MODIFIER, b as CURRIED_HELPER, a as CURRIED_COMPONENT } from './curried-BZnYakIg.js';
import { a as WellKnownTagNames, W as WellKnownAttrNames } from './well-known-_EVO9RaV.js';
const Char = {
NBSP: 0xa0,
QUOT: 0x22,
LT: 0x3c,
GT: 0x3e,
AMP: 0x26
};
// \x26 is ampersand, \xa0 is non-breaking space
const ATTR_VALUE_REGEX_TEST = /["\x26\xa0]/u;
const ATTR_VALUE_REGEX_REPLACE = new RegExp(ATTR_VALUE_REGEX_TEST.source, 'gu');
const TEXT_REGEX_TEST = /[&<>\xa0]/u;
const TEXT_REGEX_REPLACE = new RegExp(TEXT_REGEX_TEST.source, 'gu');
function attrValueReplacer(char) {
switch (char.charCodeAt(0)) {
case Char.NBSP:
return ' ';
case Char.QUOT:
return '"';
case Char.AMP:
return '&';
default:
return char;
}
}
function textReplacer(char) {
switch (char.charCodeAt(0)) {
case Char.NBSP:
return ' ';
case Char.AMP:
return '&';
case Char.LT:
return '<';
case Char.GT:
return '>';
default:
return char;
}
}
function escapeAttrValue(attrValue) {
if (ATTR_VALUE_REGEX_TEST.test(attrValue)) {
return attrValue.replace(ATTR_VALUE_REGEX_REPLACE, attrValueReplacer);
}
return attrValue;
}
function escapeText(text) {
if (TEXT_REGEX_TEST.test(text)) {
return text.replace(TEXT_REGEX_REPLACE, textReplacer);
}
return text;
}
function sortByLoc(a, b) {
// If either is invisible, don't try to order them
if (a.loc.isInvisible || b.loc.isInvisible) {
return 0;
}
if (a.loc.startPosition.line < b.loc.startPosition.line) {
return -1;
}
if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column < b.loc.startPosition.column) {
return -1;
}
if (a.loc.startPosition.line === b.loc.startPosition.line && a.loc.startPosition.column === b.loc.startPosition.column) {
return 0;
}
return 1;
}
const voidMap = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
const NON_WHITESPACE = /^\S/u;
/**
* Examples when true:
* - link
* - liNK
*
* Examples when false:
* - Link (component)
*/
function isVoidTag(tag) {
return voidMap.has(tag.toLowerCase()) && tag[0]?.toLowerCase() === tag[0];
}
class Printer {
buffer = '';
options;
constructor(options) {
this.options = options;
}
/*
This is used by _all_ methods on this Printer class that add to `this.buffer`,
it allows consumers of the printer to use alternate string representations for
a given node.
The primary use case for this are things like source -> source codemod utilities.
For example, ember-template-recast attempts to always preserve the original string
formatting in each AST node if no modifications are made to it.
*/
handledByOverride(node, ensureLeadingWhitespace = false) {
if (this.options.override !== undefined) {
let result = this.options.override(node, this.options);
if (typeof result === 'string') {
if (ensureLeadingWhitespace && NON_WHITESPACE.test(result)) {
result = ` ${result}`;
}
this.buffer += result;
return true;
}
}
return false;
}
Node(node) {
switch (node.type) {
case 'MustacheStatement':
case 'BlockStatement':
case 'MustacheCommentStatement':
case 'CommentStatement':
case 'TextNode':
case 'ElementNode':
case 'AttrNode':
case 'Block':
case 'Template':
return this.TopLevelStatement(node);
case 'StringLiteral':
case 'BooleanLiteral':
case 'NumberLiteral':
case 'UndefinedLiteral':
case 'NullLiteral':
case 'PathExpression':
case 'SubExpression':
return this.Expression(node);
case 'ConcatStatement':
// should have an AttrNode parent
return this.ConcatStatement(node);
case 'Hash':
return this.Hash(node);
case 'HashPair':
return this.HashPair(node);
case 'ElementModifierStatement':
return this.ElementModifierStatement(node);
}
}
Expression(expression) {
switch (expression.type) {
case 'StringLiteral':
case 'BooleanLiteral':
case 'NumberLiteral':
case 'UndefinedLiteral':
case 'NullLiteral':
return this.Literal(expression);
case 'PathExpression':
return this.PathExpression(expression);
case 'SubExpression':
return this.SubExpression(expression);
}
}
Literal(literal) {
switch (literal.type) {
case 'StringLiteral':
return this.StringLiteral(literal);
case 'BooleanLiteral':
return this.BooleanLiteral(literal);
case 'NumberLiteral':
return this.NumberLiteral(literal);
case 'UndefinedLiteral':
return this.UndefinedLiteral(literal);
case 'NullLiteral':
return this.NullLiteral(literal);
}
}
TopLevelStatement(statement) {
switch (statement.type) {
case 'MustacheStatement':
return this.MustacheStatement(statement);
case 'BlockStatement':
return this.BlockStatement(statement);
case 'MustacheCommentStatement':
return this.MustacheCommentStatement(statement);
case 'CommentStatement':
return this.CommentStatement(statement);
case 'TextNode':
return this.TextNode(statement);
case 'ElementNode':
return this.ElementNode(statement);
case 'Block':
return this.Block(statement);
case 'Template':
return this.Template(statement);
case 'AttrNode':
// should have element
return this.AttrNode(statement);
}
}
Template(template) {
this.TopLevelStatements(template.body);
}
Block(block) {
/*
When processing a template like:
```hbs
{{#if whatever}}
whatever
{{else if somethingElse}}
something else
{{else}}
fallback
{{/if}}
```
The AST still _effectively_ looks like:
```hbs
{{#if whatever}}
whatever
{{else}}{{#if somethingElse}}
something else
{{else}}
fallback
{{/if}}{{/if}}
```
The only way we can tell if that is the case is by checking for
`block.chained`, but unfortunately when the actual statements are
processed the `block.body[0]` node (which will always be a
`BlockStatement`) has no clue that its ancestor `Block` node was
chained.
This "forwards" the `chained` setting so that we can check
it later when processing the `BlockStatement`.
*/
if (block.chained) {
let firstChild = block.body[0];
firstChild.chained = true;
}
if (this.handledByOverride(block)) {
return;
}
this.TopLevelStatements(block.body);
}
TopLevelStatements(statements) {
statements.forEach(statement => this.TopLevelStatement(statement));
}
ElementNode(el) {
if (this.handledByOverride(el)) {
return;
}
this.OpenElementNode(el);
this.TopLevelStatements(el.children);
this.CloseElementNode(el);
}
OpenElementNode(el) {
this.buffer += `<${el.tag}`;
const parts = [...el.attributes, ...el.modifiers, ...el.comments].sort(sortByLoc);
for (const part of parts) {
this.buffer += ' ';
switch (part.type) {
case 'AttrNode':
this.AttrNode(part);
break;
case 'ElementModifierStatement':
this.ElementModifierStatement(part);
break;
case 'MustacheCommentStatement':
this.MustacheCommentStatement(part);
break;
}
}
if (el.blockParams.length) {
this.BlockParams(el.blockParams);
}
if (el.selfClosing) {
this.buffer += ' /';
}
this.buffer += '>';
}
CloseElementNode(el) {
if (el.selfClosing || isVoidTag(el.tag)) {
return;
}
this.buffer += `</${el.tag}>`;
}
AttrNode(attr) {
if (this.handledByOverride(attr)) {
return;
}
let {
name,
value
} = attr;
this.buffer += name;
const isAttribute = !name.startsWith('@');
const shouldElideValue = isAttribute && value.type == 'TextNode' && value.chars.length === 0;
if (!shouldElideValue) {
this.buffer += '=';
this.AttrNodeValue(value);
}
}
AttrNodeValue(value) {
if (value.type === 'TextNode') {
let quote = '"';
if (this.options.entityEncoding === 'raw') {
if (value.chars.includes('"') && !value.chars.includes("'")) {
quote = "'";
}
}
this.buffer += quote;
this.TextNode(value, quote);
this.buffer += quote;
} else {
this.Node(value);
}
}
TextNode(text, isInAttr) {
if (this.handledByOverride(text)) {
return;
}
if (this.options.entityEncoding === 'raw') {
if (isInAttr && text.chars.includes(isInAttr)) {
this.buffer += escapeAttrValue(text.chars);
} else {
this.buffer += text.chars;
}
} else if (isInAttr) {
this.buffer += escapeAttrValue(text.chars);
} else {
this.buffer += escapeText(text.chars);
}
}
MustacheStatement(mustache) {
if (this.handledByOverride(mustache)) {
return;
}
this.buffer += mustache.trusting ? '{{{' : '{{';
if (mustache.strip.open) {
this.buffer += '~';
}
this.Expression(mustache.path);
this.Params(mustache.params);
this.Hash(mustache.hash);
if (mustache.strip.close) {
this.buffer += '~';
}
this.buffer += mustache.trusting ? '}}}' : '}}';
}
BlockStatement(block) {
if (this.handledByOverride(block)) {
return;
}
if (block.chained) {
this.buffer += block.inverseStrip.open ? '{{~' : '{{';
this.buffer += 'else ';
} else {
this.buffer += block.openStrip.open ? '{{~#' : '{{#';
}
this.Expression(block.path);
this.Params(block.params);
this.Hash(block.hash);
if (block.program.blockParams.length) {
this.BlockParams(block.program.blockParams);
}
if (block.chained) {
this.buffer += block.inverseStrip.close ? '~}}' : '}}';
} else {
this.buffer += block.openStrip.close ? '~}}' : '}}';
}
this.Block(block.program);
if (block.inverse) {
if (!block.inverse.chained) {
this.buffer += block.inverseStrip.open ? '{{~' : '{{';
this.buffer += 'else';
this.buffer += block.inverseStrip.close ? '~}}' : '}}';
}
this.Block(block.inverse);
}
if (!block.chained) {
this.buffer += block.closeStrip.open ? '{{~/' : '{{/';
this.Expression(block.path);
this.buffer += block.closeStrip.close ? '~}}' : '}}';
}
}
BlockParams(blockParams) {
this.buffer += ` as |${blockParams.join(' ')}|`;
}
ConcatStatement(concat) {
if (this.handledByOverride(concat)) {
return;
}
this.buffer += '"';
concat.parts.forEach(part => {
if (part.type === 'TextNode') {
this.TextNode(part, '"');
} else {
this.Node(part);
}
});
this.buffer += '"';
}
MustacheCommentStatement(comment) {
if (this.handledByOverride(comment)) {
return;
}
this.buffer += `{{!--${comment.value}--}}`;
}
ElementModifierStatement(mod) {
if (this.handledByOverride(mod)) {
return;
}
this.buffer += '{{';
this.Expression(mod.path);
this.Params(mod.params);
this.Hash(mod.hash);
this.buffer += '}}';
}
CommentStatement(comment) {
if (this.handledByOverride(comment)) {
return;
}
this.buffer += `<!--${comment.value}-->`;
}
PathExpression(path) {
if (this.handledByOverride(path)) {
return;
}
this.buffer += path.original;
}
SubExpression(sexp) {
if (this.handledByOverride(sexp)) {
return;
}
this.buffer += '(';
this.Expression(sexp.path);
this.Params(sexp.params);
this.Hash(sexp.hash);
this.buffer += ')';
}
Params(params) {
// TODO: implement a top level Params AST node (just like the Hash object)
// so that this can also be overridden
if (params.length) {
params.forEach(param => {
this.buffer += ' ';
this.Expression(param);
});
}
}
Hash(hash) {
if (this.handledByOverride(hash, true)) {
return;
}
hash.pairs.forEach(pair => {
this.buffer += ' ';
this.HashPair(pair);
});
}
HashPair(pair) {
if (this.handledByOverride(pair)) {
return;
}
this.buffer += pair.key;
this.buffer += '=';
this.Node(pair.value);
}
StringLiteral(str) {
if (this.handledByOverride(str)) {
return;
}
this.buffer += JSON.stringify(str.value);
}
BooleanLiteral(bool) {
if (this.handledByOverride(bool)) {
return;
}
this.buffer += String(bool.value);
}
NumberLiteral(number) {
if (this.handledByOverride(number)) {
return;
}
this.buffer += String(number.value);
}
UndefinedLiteral(node) {
if (this.handledByOverride(node)) {
return;
}
this.buffer += 'undefined';
}
NullLiteral(node) {
if (this.handledByOverride(node)) {
return;
}
this.buffer += 'null';
}
print(node) {
let {
options
} = this;
if (options.override) {
let result = options.override(node, options);
if (result !== undefined) {
return result;
}
}
this.buffer = '';
this.Node(node);
return this.buffer;
}
}
function build(ast, options = {
entityEncoding: 'transformed'
}) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS users
if (!ast) {
return '';
}
let printer = new Printer(options);
return printer.print(ast);
}
const UNKNOWN_POSITION = Object.freeze({
line: 1,
column: 0
});
const SYNTHETIC_LOCATION = Object.freeze({
source: '(synthetic)',
start: UNKNOWN_POSITION,
end: UNKNOWN_POSITION
});
const NON_EXISTENT_LOCATION = Object.freeze({
source: '(nonexistent)',
start: UNKNOWN_POSITION,
end: UNKNOWN_POSITION
});
const BROKEN_LOCATION = Object.freeze({
source: '(broken)',
start: UNKNOWN_POSITION,
end: UNKNOWN_POSITION
});
/**
* We have already computed the character position of this offset or span.
*/
const CHAR_OFFSET_KIND = 'CharPosition';
/**
* This offset or span was instantiated with a Handlebars SourcePosition or SourceLocation. Its
* character position will be computed on demand.
*/
const HBS_POSITION_KIND = 'HbsPosition';
/**
* for (rare) situations where a node is created but there was no source location (e.g. the name
* "default" in default blocks when the word "default" never appeared in source). This is used
* by the internals when there is a legitimate reason for the internals to synthesize a node
* with no location.
*/
const INTERNAL_SYNTHETIC_KIND = 'InternalsSynthetic';
/**
* For situations where a node represents zero parts of the source (for example, empty arguments).
* In general, we attempt to assign these nodes *some* position (empty arguments can be
* positioned immediately after the callee), but it's not always possible
*/
const NON_EXISTENT_KIND = 'NonExistent';
/**
* For situations where a source location was expected, but it didn't correspond to the node in
* the source. This happens if a plugin creates broken locations.
*/
const BROKEN_KIND = 'Broken';
/**
* These kinds describe spans that don't have a concrete location in the original source.
*/
function isInvisible(kind) {
return kind !== CHAR_OFFSET_KIND && kind !== HBS_POSITION_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.
*/
const MatchAny = 'MATCH_ANY';
const IsInvisible = 'IS_INVISIBLE';
class WhenList {
_whens;
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 {
_map = new Map();
get(pattern, or) {
let value = this._map.get(pattern);
if (value) {
return value;
}
value = or();
this._map.set(pattern, value);
return value;
}
add(pattern, out) {
this._map.set(pattern, out);
}
match(kind) {
const pattern = patternFor(kind);
const out = [];
const exact = this._map.get(pattern);
const fallback = this._map.get(MatchAny);
if (exact) {
out.push(exact);
}
if (fallback) {
out.push(fallback);
}
return out;
}
}
function match(callback) {
return callback(new Matcher()).validate();
}
class Matcher {
_whens = new When();
/**
* 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);
assert(isPresentArray(nesteds));
const callback = new WhenList(nesteds).first(right);
return callback;
}
// This big block is the bulk of the heavy lifting in this file. It facilitates exhaustiveness
// checking so that matchers can ensure they've actually covered all the cases (and TypeScript
// will treat it as an exhaustive match).
when(left, right,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback) {
this._whens.get(left, () => new When()).add(right, callback);
return this;
}
}
function patternFor(kind) {
switch (kind) {
case BROKEN_KIND:
case INTERNAL_SYNTHETIC_KIND:
case NON_EXISTENT_KIND:
return IsInvisible;
default:
return kind;
}
}
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]
});
}
chars;
loc;
constructor(options) {
this.loc = options.loc;
this.chars = options.chars;
}
getString() {
return this.chars;
}
serialize() {
return [this.chars, this.loc.serialize()];
}
}
/**
* All spans have these details in common.
*/
/**
* 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(NON_EXISTENT_KIND, NON_EXISTENT_LOCATION).wrap();
}
static load(source, serialized) {
if (typeof serialized === 'number') {
return SourceSpan.forCharPositions(source, serialized, serialized);
} else if (typeof serialized === 'string') {
return SourceSpan.synthetic(serialized);
} else if (Array.isArray(serialized)) {
return SourceSpan.forCharPositions(source, serialized[0], serialized[1]);
} else if (serialized === NON_EXISTENT_KIND) {
return SourceSpan.NON_EXISTENT;
} else if (serialized === BROKEN_KIND) {
return SourceSpan.broken(BROKEN_LOCATION);
}
assertNever(serialized);
}
static forHbsLoc(source, loc) {
const start = new HbsPosition(source, loc.start);
const end = new HbsPosition(source, loc.end);
return new HbsSpan(source, {
start,
end
}, loc).wrap();
}
static forCharPositions(source, startPos, endPos) {
const start = new CharPosition(source, startPos);
const end = new CharPosition(source, endPos);
return new CharPositionSpan(source, {
start,
end
}).wrap();
}
static synthetic(chars) {
return new InvisibleSpan(INTERNAL_SYNTHETIC_KIND, NON_EXISTENT_LOCATION, chars).wrap();
}
static broken(pos = BROKEN_LOCATION) {
return new InvisibleSpan(BROKEN_KIND, pos).wrap();
}
isInvisible;
constructor(data) {
this.data = data;
this.isInvisible = isInvisible(data.kind);
}
getStart() {
return this.data.getStart().wrap();
}
getEnd() {
return this.data.getEnd().wrap();
}
get loc() {
const span = this.data.toHbsSpan();
return span === null ? 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`.
*/
toSlice(expected) {
const chars = this.data.asString();
assert(expected === undefined || expected === chars, `unexpectedly found ${JSON.stringify(chars)} when slicing source, ` + `but expected ${JSON.stringify(expected)}`);
return 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 = 0,
skipEnd = 0
}) {
return span(this.getStart().move(skipStart).data, this.getEnd().move(-skipEnd).data);
}
sliceStartChars({
skipStart = 0,
chars
}) {
return span(this.getStart().move(skipStart).data, this.getStart().move(skipStart + chars).data);
}
sliceEndChars({
skipEnd = 0,
chars
}) {
return span(this.getEnd().move(skipEnd - chars).data, this.getStart().move(-skipEnd).data);
}
}
class CharPositionSpan {
kind = CHAR_OFFSET_KIND;
#locPosSpan = null;
constructor(source, charPositions) {
this.source = source;
this.charPositions = charPositions;
}
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 (locPosSpan === null) {
const start = this.charPositions.start.toHbsPos();
const end = this.charPositions.end.toHbsPos();
if (start === null || end === null) {
locPosSpan = this.#locPosSpan = BROKEN;
} else {
locPosSpan = this.#locPosSpan = new HbsSpan(this.source, {
start,
end
});
}
}
return locPosSpan === BROKEN ? null : locPosSpan;
}
serialize() {
const {
start: {
charPos: start
},
end: {
charPos: end
}
} = this.charPositions;
if (start === end) {
return start;
} else {
return [start, end];
}
}
toCharPosSpan() {
return this;
}
}
class HbsSpan {
kind = HBS_POSITION_KIND;
#charPosSpan = null;
// the source location from Handlebars + AST Plugins -- could be wrong
#providedHbsLoc;
constructor(source, hbsPositions, providedHbsLoc = null) {
this.source = source;
this.hbsPositions = hbsPositions;
this.#providedHbsLoc = providedHbsLoc;
}
serialize() {
const charPos = this.toCharPosSpan();
return charPos === null ? BROKEN_KIND : charPos.wrap().serialize();
}
wrap() {
return new SourceSpan(this);
}
updateProvided(pos, edge) {
if (this.#providedHbsLoc) {
this.#providedHbsLoc[edge] = pos;
}
// invalidate computed character offsets
this.#charPosSpan = null;
this.#providedHbsLoc = {
start: pos,
end: pos
};
}
locDidUpdate({
start,
end
}) {
if (start !== undefined) {
this.updateProvided(start, 'start');
this.hbsPositions.start = new HbsPosition(this.source, start, null);
}
if (end !== undefined) {
this.updateProvided(end, 'end');
this.hbsPositions.end = new HbsPosition(this.source, end, null);
}
}
asString() {
const span = this.toCharPosSpan();
return span === null ? '' : 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 (charPosSpan === null) {
const start = this.hbsPositions.start.toCharPos();
const end = this.hbsPositions.end.toCharPos();
if (start && end) {
charPosSpan = this.#charPosSpan = new CharPositionSpan(this.source, {
start,
end
});
} else {
charPosSpan = this.#charPosSpan = BROKEN;
return null;
}
}
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_KIND:
case NON_EXISTENT_KIND:
return this.kind;
case INTERNAL_SYNTHETIC_KIND:
return this.string || '';
}
}
wrap() {
return new SourceSpan(this);
}
asString() {
return this.string || '';
}
locDidUpdate({
start,
end
}) {
if (start !== undefined) {
this.loc.start = start;
}
if (end !== undefined) {
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(HBS_POSITION_KIND, HBS_POSITION_KIND, (left, right) => new HbsSpan(left.source, {
start: left,
end: right
}).wrap()).when(CHAR_OFFSET_KIND, CHAR_OFFSET_KIND, (left, right) => new CharPositionSpan(left.source, {
start: left,
end: right
}).wrap()).when(CHAR_OFFSET_KIND, HBS_POSITION_KIND, (left, right) => {
const rightCharPos = right.toCharPos();
if (rightCharPos === null) {
return new InvisibleSpan(BROKEN_KIND, BROKEN_LOCATION).wrap();
} else {
return span(left, rightCharPos);
}
}).when(HBS_POSITION_KIND, CHAR_OFFSET_KIND, (left, right) => {
const leftCharPos = left.toCharPos();
if (leftCharPos === null) {
return new InvisibleSpan(BROKEN_KIND, BROKEN_LOCATION).wrap();
} else {
return span(leftCharPos, right);
}
}).when(IsInvisible, MatchAny, left => new InvisibleSpan(left.kind, BROKEN_LOCATION).wrap()).when(MatchAny, IsInvisible, (_, right) => new InvisibleSpan(right.kind, BROKEN_LOCATION).wrap()));
// `string` includes NON_EXISTENT_KIND and BROKEN_KIND
/**
* All positions have these details in common. Most notably, all three kinds of positions can
* must be able to attempt to convert themselves into {@see CharPosition}.
*/
/**
* 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)
*/
const BROKEN = 'BROKEN';
/**
* 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_KIND, pos).wrap();
}
constructor(data) {
this.data = data;
}
/**
* Get the character offset for this `SourceOffset`, if possible.
*/
get offset() {
const charPos = this.data.toCharPos();
return charPos === null ? 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 (charPos === null) {
return SourceOffset.broken();
} else {
const result = charPos.offset + by;
if (charPos.source.validate(result)) {
return new CharPosition(charPos.source, result).wrap();
} else {
return 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 {
kind = CHAR_OFFSET_KIND;
/** Computed from char offset */
_locPos = null;
constructor(source, charPos) {
this.source = source;
this.charPos = charPos;
}
/**
* 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 hbs === null ? 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 (locPos === null) {
const hbsPos = this.source.hbsPosFor(this.charPos);
if (hbsPos === null) {
this._locPos = locPos = BROKEN;
} else {
this._locPos = locPos = new HbsPosition(this.source, hbsPos, this.charPos);
}
}
return locPos === BROKEN ? null : locPos;
}
}
class HbsPosition {
kind = HBS_POSITION_KIND;
_charPos;
constructor(source, hbsPos, charPos = null) {
this.source = source;
this.hbsPos = hbsPos;
this._charPos = charPos === null ? 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 (charPos === null) {
const charPosNumber = this.source.charPosFor(this.hbsPos);
if (charPosNumber === null) {
this._charPos = charPos = BROKEN;
} else {
this._charPos = charPos = 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(HBS_POSITION_KIND, HBS_POSITION_KIND, ({
hbsPos: left
}, {
hbsPos: right
}) => left.column === right.column && left.line === right.line).when(CHAR_OFFSET_KIND, CHAR_OFFSET_KIND, ({
charPos: left
}, {
charPos: right
}) => left === right).when(CHAR_OFFSET_KIND, HBS_POSITION_KIND, ({
offset: left
}, right) => left === right.toCharPos()?.offset).when(HBS_POSITION_KIND, CHAR_OFFSET_KIND, (left, {
offset: right
}) => left.toCharPos()?.offset === right).when(MatchAny, MatchAny, () => false));
class Source {
static from(source, options = {}) {
return new Source(source, options.meta?.moduleName);
}
/** Char offset of each `\n` in the source. */
#newlineOffsets;
constructor(source, module = 'an unknown module') {
this.source = source;
this.module = module;
setLocalDebugType('syntax:source', this);
this.#newlineOffsets = computeNewlineOffsets(source);
}
/**
* Validate that the character offset represents a position in the source string.
*/
validate(offset) {
return offset >= 0 && offset <= this.source.length;
}
slice(start, end) {
return this.source.slice(start, end);
}
offsetFor(line, column) {
return SourceOffset.forHbsPos(this, {
line,
column
});
}
spanFor({
start,
end
}) {
return SourceSpan.forHbsLoc(this, {
start: {
line: start.line,
column: start.column
},
end: {
line: end.line,
column: end.column
}
});
}
hbsPosFor(offset) {
if (offset < 0 || offset > this.source.length) return null;
const lineIdx = lowerBound(this.#newlineOffsets, offset);
return {
line: lineIdx + 1,
column: offset - this.#lineStartFor(lineIdx)
};
}
charPosFor({
line,
column
}) {
const lineIdx = line - 1;
// Valid lines are [0, newlineOffsets.length]. Anything else has no offset.
if (lineIdx < 0 || lineIdx > this.#newlineOffsets.length || column < 0) return null;
const lineStart = lineIdx === 0 ? 0 : this.#newlineOffsets[lineIdx - 1] + 1;
const lineEnd = this.#newlineOffsets[lineIdx] ?? this.source.length;
const target = lineStart + column;
if (target <= lineEnd) {
return target;
}
return lineEnd;
}
#lineStartFor(lineIdx) {
if (lineIdx === 0) return 0;
const prevNl = this.#newlineOffsets[lineIdx - 1];
return prevNl === undefined ? 0 : prevNl + 1;
}
}
function computeNewlineOffsets(source) {
const offsets = [];
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) {
offsets.push(i);
}
return offsets;
}
/** Lower-bound binary search: smallest i with arr[i] >= target, else arr.length. */
function lowerBound(arr, target) {
let lo = 0;
let hi = arr.length;
while (lo < hi) {
const mid = lo + hi >>> 1;
// mid is in [lo, hi) so always a valid index.
if (arr[mid] < target) lo = mid + 1;else hi = mid;
}
return lo;
}
class SpanList {
static range(span, fallback = SourceSpan.NON_EXISTENT) {
return new SpanList(span.map(loc)).getRangeOffset(fallback);
}
_span;
constructor(span = []) {
this._span = span;
}
add(offset) {
this._span.push(offset);
}
getRangeOffset(fallback) {
if (isPresentArray(this._span)) {
let first = getFirst(this._span);
let last = getLast(this._span);
return first.extend(last);
} else {
return fallback;
}
}
}
function loc(span) {
if (Array.isArray(span)) {
let first = getFirst(span);
let last = getLast(span);
return loc(first).extend(loc(last));
} else if (span instanceof SourceSpan) {
return span;
} else {
return span.loc;
}
}
function hasSpan(span) {
if (Array.isArray(span) && span.length === 0) {
return false;
}
return true;
}
function maybeLoc(location, fallback) {
if (hasSpan(location)) {
return loc(location);
} else {
return fallback;
}
}
const errorProps = ['description', 'fileName', 'lineNumber', 'endLineNumber', 'message', 'name', 'number', 'stack'];
function Exception(message, node) {
let loc = node && node.loc,
line,
endLineNumber,
column,
endColumn;
if (loc) {
line = loc.start.line;
endLineNumber = loc.end.line;
column = loc.start.column;
endColumn = loc.end.column;
message += ' - ' + line + ':' + column;
}
let tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (let idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
/* istanbul ignore else */
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Exception);
}
try {
if (loc) {
this.lineNumber = line;
this.endLineNumber = endLineNumber;
// Work around issue under safari where we can't directly set the column value
/* istanbul ignore next */
if (Object.defineProperty) {
Object.defineProperty(this, 'column', {
value: column,
enumerable: true
});
Object.defineProperty(this, 'endColumn', {
value: endColumn,
enumerable: true
});
} else {
this.column = column;
this.endColumn = endColumn;
}
}
} catch (nop) {
/* Ignore if the browser is very particular */
}
}
Exception.prototype = new Error();
function Visitor() {
this.parents = [];
}
Visitor.prototype = {
constructor: Visitor,
mutating: false,
// Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function (node, name) {
let value = this.accept(node[name]);
if (this.mutating) {
// Hacky sanity check: This may have a few false positives for type for the helper
// methods but will generally do the right thing without a lot of overhead.
if (value && !Visitor.prototype[value.type]) {
throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
}
node[name] = value;
}
},
// Performs an accept operation with added sanity check to ensure
// required keys are not removed.
acceptRequired: function (node, name) {
this.acceptKey(node, name);
if (!node[name]) {
throw new Exception(node.type + ' requires ' + name);
}
},
// Traverses a given array. If mutating, empty responses will be removed
// for child elements.
acceptArray: function (array) {
for (let i = 0, l = array.length; i < l; i++) {
this.acceptKey(array, i);
if (!array[i]) {
array.splice(i, 1);
i--;
l--;
}
}
},
accept: function (object) {
if (!object) {
return;
}
/* istanbul ignore next: Sanity code */
if (!this[object.type]) {
throw new Exception('Unknown type: ' + object.type, object);
}
if (this.current) {
this.parents.unshift(this.current);
}
this.current = object;
let ret = this[object.type](object);
this.current = this.parents.shift();
if (!this.mutating || ret) {
return ret;
} else if (ret !== false) {
return object;
}
},
Program: function (program) {
this.acceptArray(program.body);
},
MustacheStatement: visitSubExpression,
Decorator: visitSubExpression,
BlockStatement: visitBlock,
DecoratorBlock: visitBlock,
PartialStatement: visitPartial,
PartialBlockStatement: function (partial) {
visitPartial.call(this, partial);
this.acceptKey(partial, 'program');
},
ContentStatement: function /* content */ () {},
CommentStatement: function /* comment */ () {},
SubExpression: visitSubExpression,
PathExpression: function /* path */ () {},
StringLiteral: function /* string */ () {},
NumberLiteral: function /* number */ () {},
BooleanLiteral: function /* bool */ () {},
UndefinedLiteral: function /* literal */ () {},
NullLiteral: function /* literal */ () {},
Hash: function (hash) {
this.acceptArray(hash.pairs);
},
HashPair: function (pair) {
this.acceptRequired(pair, 'value');
}
};
function visitSubExpression(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
}
function visitBlock(block) {
visitSubExpression.call(this, block);
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
}
function visitPartial(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
}
function WhitespaceControl(options = {}) {
this.options = options;
}
WhitespaceControl.prototype = new Visitor();
WhitespaceControl.prototype.Program = function (program) {
const doStandalone = !this.options.ignoreStandalone;
let isRoot = !this.isRootSeen;
this.isRootSeen = true;
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
strip = this.accept(current);
if (!strip) {
continue;
}
let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
if (strip.close) {
omitRight(body, i, true);
}
if (strip.open) {
omitLeft(body, i, true);
}
if (doStandalone && inlineStandalone) {
omitRight(body, i);
if (omitLeft(body, i)) {
// If we are on a standalone node, save the indent info for partials
if (current.type === 'PartialStatement') {
// Pull out the whitespace from the final line
current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1];
}
}
}
if (doStandalone && openStandalone) {
omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only
omitLeft(body, i);
}
if (doStandalone && closeStandalone) {
// Always strip the next node
omitRight(body, i);
omitLeft((current.inverse || current.program).body);
}
}
return program;
};
WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function (block) {
this.accept(block.program);
this.accept(block.inverse);
// Find the inverse program that is involved with whitespace stripping.
let program = block.program || block.inverse,
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
if (inverse && inverse.chained) {
firstInverse = inverse.body[0].program;
// Walk the inverse chain to find the last inverse that is actually in the chain.
while (lastInverse.chained) {
lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
}
}
let strip = {
open: block.openStrip.open,
close: block.closeStrip.close,
// Determine the standalone candidacy. Basically flag our content as being possibly standalone
// so our parent can determine if we actually are standalone
openStandalone: isNextWhitespace(program.body),
closeStandalone: isPrevWhitespace((firstInverse || program).body)
};
if (block.openStrip.close) {
omitRight(program.body, null, true);
}
if (inverse) {
let inverseStrip = block.inverseStrip;
if (inverseStrip.open) {
omitLeft(program.body, null, true);
}
if (