ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,722 lines (1,638 loc) • 347 kB
JavaScript
import { i as isPresentArray, a as getFirst, g as getLast, b as asPresentArray, m as mapPresentArray } from './present-B1rrjAVM.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 { i as isVoidTag, b as build, v as voidMap, P as Printer } from './transform-resolutions-h1ik8gqW.js';
import { u as unwrap, e as expect, d as dict, b as exhausted } from './collections-DPkjqeA3.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 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) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
{
const roundTrip = this.hbsPosFor(target);
assert(roundTrip.line === line);
assert(roundTrip.column === column);
}
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 (inverseStrip.close) {
omitRight(firstInverse.body, null, true);
}
if (block.closeStrip.open) {
omitLeft(lastInverse.body, null, true);
}
// Find standalone else statements
if (!this.options.ignoreStandalone && isPrevWhitespace(program.body) && isNextWhitespace(firstInverse.body)) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
} else if (block.closeStrip.open) {
omitLeft(program.body, null, true);
}
return strip;
};
WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function (mustache) {
return mustache.strip;
};
WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function (node) {
/* istanbul ignore next */
let strip = node.strip || {};
return {
inlineStandalone: true,
open: strip.open,
close: strip.close
};
};
function isPrevWhitespace(body, i, isRoot) {
if (i === undefined) {
i = body.length;
}
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
let prev = body[i - 1],
sibling = body[i - 2];
if (!prev) {
return isRoot;
}
if (prev.type === 'ContentStatement') {
return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(prev.original);
}
}
function isNextWhitespace(body, i, isRoot) {
if (i === undefined) {
i = -1;
}
let next = body[i + 1],
sibling = body[i + 2];
if (!next) {
return isRoot;
}
if (next.type === 'ContentStatement') {
return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(next.original);
}
}
// Marks the node to the right of the position as omitted.
// I.e. {{foo}}' ' will mark the ' ' node as omitted.
//
// If i is undefined, then the first child will be marked as such.
//
// If multiple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(body, i, multiple) {
let current = body[i == null ? 0 : i + 1];
if (!current || current.type !== 'ContentStatement' || !multiple && current.rightStripped) {
return;
}
let original = current.value;
current.value = current.value.replace(multiple ? /^\s+/ : /^[ \t]*\r?\n?/, '');
current.rightStripped = current.value !== original;
}
// Marks the node to the left of the position as omitted.
// I.e. ' '{{foo}} will mark the ' ' node as omitted.
//
// If i is undefined then the last child will be marked as such.
//
// If multiple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitLeft(body, i, multiple) {
let current = body[i == null ? body.length - 1 : i - 1];
if (!current || current.type !== 'ContentStatement' || !multiple && current.leftStripped) {
return;
}
// We omit the last node if it's whitespace only and not preceded by a non-content node.
let original = current.value;
current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, '');
current.leftStripped = current.value !== original;
return current.leftStripped;
}
// @ts-nocheck
/* parser generated by jison 0.4.18 */
/*
Returns a Parser object of the following structure:
Parser: {
yy: {}
}
Parser.prototype: {
yy: {},
trace: function(),
symbols_: {associative list: name ==> number},
terminals_: {associative list: number ==> name},
productions_: [...],
performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$),
table: [...],
defaultActions: {...},
parseError: function(str, hash),
parse: function(input),
lexer: {
EOF: 1,
parseError: function(str, hash),
setInput: function(input),
input: function(),
unput: function(str),
more: function(),
less: function(n),
pastInput: function(),
upcomingInput: function(),
showPosition: function(),
test_match: function(regex_match_array, rule_index),
next: function(),
lex: function(),
begin: function(condition),
popState: function(),
_currentRules: function(),
topState: function(),
pushState: function(condition),
options: {
ranges: boolean (optional: true ==> token location info will include a .range[] member)
flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match)
backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code)
},
performAction: function(yy, yy_, $avoiding_name_collisions, YY_START),
rules: [...],
conditions: {associative list: name ==> set},
}
}
token location info (@$, _$, etc.): {
first_line: n,
last_line: n,
first_column: n,
last_column: n,
range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based)
}
the parseError function receives a 'hash' object with these members for lexer and parser errors: {
text: (matched text)
token: (the produced terminal token, if any)
line: (yylineno)
}
while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: {
loc: (yylloc)
expected: (string describing the set of expected tokens)
recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error)
}
*/
var parser = function () {
var o = function (k, v, o, l) {
for (o = o || {}, l = k.length; l--; o[k[l]] = v);
return o;
},
$V0 = [2, 52],
$V1 = [1, 20],
$V2 = [5, 14, 15, 19, 29, 34, 39, 44, 47, 48, 53, 57, 61],
$V3 = [1, 44],
$V4 = [1, 40],
$V5 = [1, 43],
$V6 = [1, 33],
$V7 = [1, 34],
$V8 = [1, 35],
$V9 = [1, 36],
$Va = [1, 37],
$Vb = [1, 42],
$Vc = [1, 46],
$Vd = [14, 15, 19, 29, 34, 39, 44, 47, 48, 53, 57, 61],
$Ve = [14, 15, 19, 29, 34, 44, 47, 48, 53, 57, 61],
$Vf = [15, 18],
$Vg = [14, 15, 19, 29, 34, 47, 48, 53, 57, 61],
$Vh = [33, 67, 73, 75, 84, 85, 86, 87, 88, 89],
$Vi = [23, 33, 56, 67, 68, 73, 75, 77, 79, 84, 85, 86, 87, 88, 89],
$Vj = [1, 62],
$Vk = [1, 63],
$Vl = [23, 33, 56, 68, 73, 79],
$Vm = [23, 33, 56, 67, 68, 73, 75, 77, 79, 84, 85, 86, 87, 88, 89, 92, 93],
$Vn = [2, 51],
$Vo = [1, 64],
$Vp = [67, 73, 75, 77, 84, 85, 86, 87, 88, 89],
$Vq = [56, 67, 73, 75, 84, 85, 86, 87, 88, 89],
$Vr = [1, 75],
$Vs = [1, 76],
$Vt = [1, 83],
$Vu = [33, 67, 73, 75, 79, 84, 85, 86, 87, 88, 89],
$Vv = [23, 67, 73, 75, 84, 85, 86, 87, 88, 89],
$Vw = [67, 68, 73, 75, 84, 85, 86, 87, 88, 89],
$Vx = [33, 79],
$Vy = [1, 134],
$Vz = [73, 81];
var parser = {
trace: function trace() {},
yy: {},
symbols_: {
error: 2,
root: 3,
program: 4,
EOF: 5,
program_repetition0: 6,
statement: 7,
mustache: 8,
block: 9,
rawBlock: 10,
partial: 11,
partialBlock: 12,
content: 13,
COMMENT: 14,
CONTENT: 15,
openRawBlock: 16,
rawBlock_repetition0: 17,
END_RAW_BLOCK: 18,
OPEN_RAW_BLOCK: 19,
helperName: 20,
openRawBlock_repetition0: 21,
openRawBlock_option0: 22,
CLOSE_RAW_BLOCK: 23,
openBlock: 24,
block_option0: 25,
closeBlock: 26,
openInverse: 27,
block_option1: 28,
OPEN_BLOCK: 29,
openBlock_repetition0: 30,
openBlock_option0: 31,
openBlock_option1: 32,
CLOSE: 33,
OPEN_INVERSE: 34,
openInverse_repetition0: 35,
openInverse_option0: 36,
openInverse_option1: 37,
openInverseChain: 38,
OPEN_INVERSE_CHAIN: 39,
openInverseChain_repetition0: 40,
openInverseChain_option0: 41,
openInverseChain_option1: 42,
inverseAndProgram: 43,
INVERSE: 44,
inverseChain: 45,
inverseChain_option0: 46,
OPEN_ENDBLOCK: 47,
OPEN: 48,
hash: 49,
expr: 50,
mustache_repetition0: 51,
mustache_option0: 52,
OPEN_UNESCAPED: 53,
mustache_repetition1: 54,
mustache_option1: 55,
CLOSE_UNESCAPED: 56,
OPEN_PARTIAL: 57,
partial_repetition0: 58,
partial_option0: 59,
openPartialBlock: 60,
OPEN_PARTIAL_BLOCK: 61,
openPartialBlock_repetition0: 62,
openPartialBlock_option0: 63,
exprHead: 64,
arrayLiteral: 65,
sexpr: 66,
OPEN_SEXPR: 67,
CLOSE_SEXPR: 68,
sexpr_repetition0: 69,
sexpr_option0: 70,
hash_repetition_plus0: 71,
hashSegment: 72,
ID: 73,
EQUALS: 74,
OPEN_ARRAY: 75,
arrayLiteral_repetition0: 76,
CLOSE_ARRAY: 77,
blockParams: 78,
OPEN_BLOCK_PARAMS: 79,
blockParams_repetition_plus0: 80,
CLOSE_BLOCK_PARAMS: 81,
path: 82,
dataName: 83,
STRING: 84,
NUMBER: 85,
BOOLEAN: 86,
UNDEFINED: 87,
NULL: 88,
DATA: 89,
pathSegments: 90,
sep: 91,
SEP: 92,
PRIVATE_SEP: 93,
$accept: 0,
$end: 1
},
terminals_: {
2: 'error',
5: 'EOF',
14: 'COMMENT',
15: 'CONTENT',
18: 'END_RAW_BLOCK',
19: 'OPEN_RAW_BLOCK',
23: 'CLOSE_RAW_BLOCK',
29: 'OPEN_BLOCK',
33: 'CLOSE',
34: 'OPEN_INVERSE',
39: 'OPEN_INVERSE_CHAIN',
44: 'INVERSE',
47: 'OPEN_ENDBLOCK',
48: 'OPEN',
53: 'OPEN_UNESCAPED',
56: 'CLOSE_UNESCAPED',
57: 'OPEN_PARTIAL',
61: 'OPEN_PARTIAL_BLOCK',
67: 'OPEN_SEXPR',
68: 'CLOSE_SEXPR',
73: 'ID',
74: 'EQUALS',
75: 'OPEN_ARRAY',
77: 'CLOSE_ARRAY',
79: 'OPEN_BLOCK_PARAMS',
81: 'CLOSE_BLOCK_PARAMS',
84: 'STRING',
85: 'NUMBER',
86: 'BOOLEAN',
87: 'UNDEFINED',
88: 'NULL',
89: 'DATA',
92: 'SEP',
93: 'PRIVATE_SEP'
},
productions_: [0, [3, 2], [4, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [13, 1], [10, 3], [16, 5], [9, 4], [9, 4], [24, 6], [27, 6], [38, 6], [43, 2], [45, 3], [45, 1], [26, 3], [8, 3], [8, 5], [8, 5], [11, 5], [12, 3], [60, 5], [50, 1], [50, 1], [64, 1], [64, 1], [66, 3], [66, 5], [49, 1], [72, 3], [65, 3], [78, 3], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [83, 2], [91, 1], [91, 1], [82, 3], [82, 1], [90, 3], [90, 1], [6, 0], [6, 2], [17, 0], [17, 2], [21, 0], [21, 2], [22, 0], [22, 1], [25, 0], [25, 1], [28, 0], [28, 1], [30, 0], [30, 2], [31, 0], [31, 1], [32, 0], [32, 1], [35, 0], [35, 2], [36, 0], [36, 1], [37, 0], [37, 1], [40, 0], [40, 2], [41, 0], [41, 1], [42, 0], [42, 1], [46, 0], [46, 1], [51, 0], [51, 2], [52, 0], [52, 1], [54, 0], [54, 2], [55, 0], [55, 1], [58, 0], [58, 2], [59, 0], [59, 1], [62, 0], [62, 2], [63, 0], [63, 1], [69, 0], [69, 2], [70, 0], [70, 1], [71, 1], [71, 2], [76, 0], [76, 2], [80, 1], [80, 2]],
performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) {
/* this == yyval */
var $0 = $$.length - 1;
switch (yystate) {
case 1:
return $$[$0 - 1];
case 2:
this.$ = yy.prepareProgram($$[$0]);
break;
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 20:
case 28:
case 29:
case 30:
case 31:
case 38:
case 39:
case 46:
case 47:
this.$ = $$[$0];
break;
case 9:
this.$ = {
type: 'CommentStatement',
value: yy.stripComment($$[$0]),
strip: yy.stripFlags($$[$0], $$[$0]),
loc: yy.locInfo(this._$)
};
break;
case 10:
this.$ = {
type: 'ContentStatement',
original: $$[$0],
value: $$[$0],
loc: yy.locInfo(this._$)
};
break;
case 11:
this.$ = yy.prepareRawBlock($$[$0 - 2], $$[$0 - 1], $$[$0], this._$);
break;
case 12:
this.$ = {
path: $$[$0 - 3],
params: $$[$0 - 2],
hash: $$[$0 - 1]
};
break;
case 13:
this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], false, this._$);
break;
case 14:
this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], true, this._$);
break;
case 15:
this.$ = {
open: $$[$0 - 5],
path: $$[$0 - 4],
params: $$[$0 - 3],
hash: $$[$0 - 2],
blockParams: $$[$0 - 1],
strip: yy.stripFlags($$[$0 - 5], $$[$0])
};
break;
case 16:
case 17:
this.$ = {
path: $$[$0 - 4],
params: $$[$0 - 3],
hash: $$[$0 - 2],
blockParams: $$[$0 - 1],
strip: y