*/
function deduplicateTextBindings(job) {
const seen = new Map();
for (const unit of job.units) {
for (const op of unit.update.reversed()) {
if (op.kind === OpKind.Binding && op.isTextAttribute) {
const seenForElement = seen.get(op.target) || new Set();
if (seenForElement.has(op.name)) {
if (job.compatibility === CompatibilityMode.TemplateDefinitionBuilder) {
// For most duplicated attributes, TemplateDefinitionBuilder lists all of the values in
// the consts array. However, for style and class attributes it only keeps the last one.
// We replicate that behavior here since it has actual consequences for apps with
// duplicate class or style attrs.
if (op.name === 'style' || op.name === 'class') {
OpList.remove(op);
}
}
else {
// TODO: Determine the correct behavior. It would probably make sense to merge multiple
// style and class attributes. Alternatively we could just throw an error, as HTML
// doesn't permit duplicate attributes.
}
}
seenForElement.add(op.name);
seen.set(op.target, seenForElement);
}
}
}
}
/**
* Defer instructions take a configuration array, which should be collected into the component
* consts. This phase finds the config options, and creates the corresponding const array.
*/
function configureDeferInstructions(job) {
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind !== OpKind.Defer) {
continue;
}
if (op.placeholderMinimumTime !== null) {
op.placeholderConfig =
new ConstCollectedExpr(literalOrArrayLiteral([op.placeholderMinimumTime]));
}
if (op.loadingMinimumTime !== null || op.loadingAfterTime !== null) {
op.loadingConfig = new ConstCollectedExpr(literalOrArrayLiteral([op.loadingMinimumTime, op.loadingAfterTime]));
}
}
}
}
/**
* Some `defer` conditions can reference other elements in the template, using their local reference
* names. However, the semantics are quite different from the normal local reference system: in
* particular, we need to look at local reference names in enclosing views. This phase resolves
* all such references to actual xrefs.
*/
function resolveDeferTargetNames(job) {
const scopes = new Map();
function getScopeForView(view) {
if (scopes.has(view.xref)) {
return scopes.get(view.xref);
}
const scope = new Scope$1();
for (const op of view.create) {
// add everything that can be referenced.
if (!isElementOrContainerOp(op) || op.localRefs === null) {
continue;
}
if (!Array.isArray(op.localRefs)) {
throw new Error('LocalRefs were already processed, but were needed to resolve defer targets.');
}
for (const ref of op.localRefs) {
if (ref.target !== '') {
continue;
}
scope.targets.set(ref.name, { xref: op.xref, slot: op.handle });
}
}
scopes.set(view.xref, scope);
return scope;
}
function resolveTrigger(deferOwnerView, op, placeholderView) {
switch (op.trigger.kind) {
case DeferTriggerKind.Idle:
case DeferTriggerKind.Immediate:
case DeferTriggerKind.Timer:
return;
case DeferTriggerKind.Hover:
case DeferTriggerKind.Interaction:
case DeferTriggerKind.Viewport:
if (op.trigger.targetName === null) {
// A `null` target name indicates we should default to the first element in the
// placeholder block.
if (placeholderView === null) {
throw new Error('defer on trigger with no target name must have a placeholder block');
}
const placeholder = job.views.get(placeholderView);
if (placeholder == undefined) {
throw new Error('AssertionError: could not find placeholder view for defer on trigger');
}
for (const placeholderOp of placeholder.create) {
if (hasConsumesSlotTrait(placeholderOp) &&
(isElementOrContainerOp(placeholderOp) ||
placeholderOp.kind === OpKind.Projection)) {
op.trigger.targetXref = placeholderOp.xref;
op.trigger.targetView = placeholderView;
op.trigger.targetSlotViewSteps = -1;
op.trigger.targetSlot = placeholderOp.handle;
return;
}
}
return;
}
let view = placeholderView !== null ? job.views.get(placeholderView) : deferOwnerView;
let step = placeholderView !== null ? -1 : 0;
while (view !== null) {
const scope = getScopeForView(view);
if (scope.targets.has(op.trigger.targetName)) {
const { xref, slot } = scope.targets.get(op.trigger.targetName);
op.trigger.targetXref = xref;
op.trigger.targetView = view.xref;
op.trigger.targetSlotViewSteps = step;
op.trigger.targetSlot = slot;
return;
}
view = view.parent !== null ? job.views.get(view.parent) : null;
step++;
}
break;
default:
throw new Error(`Trigger kind ${op.trigger.kind} not handled`);
}
}
// Find the defer ops, and assign the data about their targets.
for (const unit of job.units) {
const defers = new Map();
for (const op of unit.create) {
switch (op.kind) {
case OpKind.Defer:
defers.set(op.xref, op);
break;
case OpKind.DeferOn:
const deferOp = defers.get(op.defer);
resolveTrigger(unit, op, deferOp.placeholderView);
break;
}
}
}
}
class Scope$1 {
constructor() {
this.targets = new Map();
}
}
const REPLACEMENTS = new Map([
[OpKind.ElementEnd, [OpKind.ElementStart, OpKind.Element]],
[OpKind.ContainerEnd, [OpKind.ContainerStart, OpKind.Container]],
[OpKind.I18nEnd, [OpKind.I18nStart, OpKind.I18n]],
]);
/**
* Op kinds that should not prevent merging of start/end ops.
*/
const IGNORED_OP_KINDS = new Set([OpKind.Pipe]);
/**
* Replace sequences of mergable instructions (e.g. `ElementStart` and `ElementEnd`) with a
* consolidated instruction (e.g. `Element`).
*/
function collapseEmptyInstructions(job) {
for (const unit of job.units) {
for (const op of unit.create) {
// Find end ops that may be able to be merged.
const opReplacements = REPLACEMENTS.get(op.kind);
if (opReplacements === undefined) {
continue;
}
const [startKind, mergedKind] = opReplacements;
// Locate the previous (non-ignored) op.
let prevOp = op.prev;
while (prevOp !== null && IGNORED_OP_KINDS.has(prevOp.kind)) {
prevOp = prevOp.prev;
}
// If the previous op is the corresponding start op, we can megre.
if (prevOp !== null && prevOp.kind === startKind) {
// Transmute the start instruction to the merged version. This is safe as they're designed
// to be identical apart from the `kind`.
prevOp.kind = mergedKind;
// Remove the end instruction.
OpList.remove(op);
}
}
}
}
/**
* Safe read expressions such as `a?.b` have different semantics in Angular templates as
* compared to JavaScript. In particular, they default to `null` instead of `undefined`. This phase
* finds all unresolved safe read expressions, and converts them into the appropriate output AST
* reads, guarded by null checks. We generate temporaries as needed, to avoid re-evaluating the same
* sub-expression multiple times.
*/
function expandSafeReads(job) {
for (const unit of job.units) {
for (const op of unit.ops()) {
transformExpressionsInOp(op, e => safeTransform(e, { job }), VisitorContextFlag.None);
transformExpressionsInOp(op, ternaryTransform, VisitorContextFlag.None);
}
}
}
// A lookup set of all the expression kinds that require a temporary variable to be generated.
const requiresTemporary = [
InvokeFunctionExpr, LiteralArrayExpr, LiteralMapExpr, SafeInvokeFunctionExpr,
PipeBindingExpr
].map(e => e.constructor.name);
function needsTemporaryInSafeAccess(e) {
// TODO: We probably want to use an expression visitor to recursively visit all descendents.
// However, that would potentially do a lot of extra work (because it cannot short circuit), so we
// implement the logic ourselves for now.
if (e instanceof UnaryOperatorExpr) {
return needsTemporaryInSafeAccess(e.expr);
}
else if (e instanceof BinaryOperatorExpr) {
return needsTemporaryInSafeAccess(e.lhs) || needsTemporaryInSafeAccess(e.rhs);
}
else if (e instanceof ConditionalExpr) {
if (e.falseCase && needsTemporaryInSafeAccess(e.falseCase))
return true;
return needsTemporaryInSafeAccess(e.condition) || needsTemporaryInSafeAccess(e.trueCase);
}
else if (e instanceof NotExpr) {
return needsTemporaryInSafeAccess(e.condition);
}
else if (e instanceof AssignTemporaryExpr) {
return needsTemporaryInSafeAccess(e.expr);
}
else if (e instanceof ReadPropExpr) {
return needsTemporaryInSafeAccess(e.receiver);
}
else if (e instanceof ReadKeyExpr) {
return needsTemporaryInSafeAccess(e.receiver) || needsTemporaryInSafeAccess(e.index);
}
// TODO: Switch to a method which is exhaustive of newly added expression subtypes.
return e instanceof InvokeFunctionExpr || e instanceof LiteralArrayExpr ||
e instanceof LiteralMapExpr || e instanceof SafeInvokeFunctionExpr ||
e instanceof PipeBindingExpr;
}
function temporariesIn(e) {
const temporaries = new Set();
// TODO: Although it's not currently supported by the transform helper, we should be able to
// short-circuit exploring the tree to do less work. In particular, we don't have to penetrate
// into the subexpressions of temporary assignments.
transformExpressionsInExpression(e, e => {
if (e instanceof AssignTemporaryExpr) {
temporaries.add(e.xref);
}
return e;
}, VisitorContextFlag.None);
return temporaries;
}
function eliminateTemporaryAssignments(e, tmps, ctx) {
// TODO: We can be more efficient than the transform helper here. We don't need to visit any
// descendents of temporary assignments.
transformExpressionsInExpression(e, e => {
if (e instanceof AssignTemporaryExpr && tmps.has(e.xref)) {
const read = new ReadTemporaryExpr(e.xref);
// `TemplateDefinitionBuilder` has the (accidental?) behavior of generating assignments of
// temporary variables to themselves. This happens because some subexpression that the
// temporary refers to, possibly through nested temporaries, has a function call. We copy that
// behavior here.
return ctx.job.compatibility === CompatibilityMode.TemplateDefinitionBuilder ?
new AssignTemporaryExpr(read, read.xref) :
read;
}
return e;
}, VisitorContextFlag.None);
return e;
}
/**
* Creates a safe ternary guarded by the input expression, and with a body generated by the provided
* callback on the input expression. Generates a temporary variable assignment if needed, and
* deduplicates nested temporary assignments if needed.
*/
function safeTernaryWithTemporary(guard, body, ctx) {
let result;
if (needsTemporaryInSafeAccess(guard)) {
const xref = ctx.job.allocateXrefId();
result = [new AssignTemporaryExpr(guard, xref), new ReadTemporaryExpr(xref)];
}
else {
result = [guard, guard.clone()];
// Consider an expression like `a?.[b?.c()]?.d`. The `b?.c()` will be transformed first,
// introducing a temporary assignment into the key. Then, as part of expanding the `?.d`. That
// assignment will be duplicated into both the guard and expression sides. We de-duplicate it,
// by transforming it from an assignment into a read on the expression side.
eliminateTemporaryAssignments(result[1], temporariesIn(result[0]), ctx);
}
return new SafeTernaryExpr(result[0], body(result[1]));
}
function isSafeAccessExpression(e) {
return e instanceof SafePropertyReadExpr || e instanceof SafeKeyedReadExpr ||
e instanceof SafeInvokeFunctionExpr;
}
function isUnsafeAccessExpression(e) {
return e instanceof ReadPropExpr || e instanceof ReadKeyExpr ||
e instanceof InvokeFunctionExpr;
}
function isAccessExpression(e) {
return isSafeAccessExpression(e) || isUnsafeAccessExpression(e);
}
function deepestSafeTernary(e) {
if (isAccessExpression(e) && e.receiver instanceof SafeTernaryExpr) {
let st = e.receiver;
while (st.expr instanceof SafeTernaryExpr) {
st = st.expr;
}
return st;
}
return null;
}
// TODO: When strict compatibility with TemplateDefinitionBuilder is not required, we can use `&&`
// instead to save some code size.
function safeTransform(e, ctx) {
if (!isAccessExpression(e)) {
return e;
}
const dst = deepestSafeTernary(e);
if (dst) {
if (e instanceof InvokeFunctionExpr) {
dst.expr = dst.expr.callFn(e.args);
return e.receiver;
}
if (e instanceof ReadPropExpr) {
dst.expr = dst.expr.prop(e.name);
return e.receiver;
}
if (e instanceof ReadKeyExpr) {
dst.expr = dst.expr.key(e.index);
return e.receiver;
}
if (e instanceof SafeInvokeFunctionExpr) {
dst.expr = safeTernaryWithTemporary(dst.expr, (r) => r.callFn(e.args), ctx);
return e.receiver;
}
if (e instanceof SafePropertyReadExpr) {
dst.expr = safeTernaryWithTemporary(dst.expr, (r) => r.prop(e.name), ctx);
return e.receiver;
}
if (e instanceof SafeKeyedReadExpr) {
dst.expr = safeTernaryWithTemporary(dst.expr, (r) => r.key(e.index), ctx);
return e.receiver;
}
}
else {
if (e instanceof SafeInvokeFunctionExpr) {
return safeTernaryWithTemporary(e.receiver, (r) => r.callFn(e.args), ctx);
}
if (e instanceof SafePropertyReadExpr) {
return safeTernaryWithTemporary(e.receiver, (r) => r.prop(e.name), ctx);
}
if (e instanceof SafeKeyedReadExpr) {
return safeTernaryWithTemporary(e.receiver, (r) => r.key(e.index), ctx);
}
}
return e;
}
function ternaryTransform(e) {
if (!(e instanceof SafeTernaryExpr)) {
return e;
}
return new ConditionalExpr(new BinaryOperatorExpr(BinaryOperator.Equals, e.guard, NULL_EXPR), NULL_EXPR, e.expr);
}
/**
* The escape sequence used indicate message param values.
*/
const ESCAPE$1 = '\uFFFD';
/**
* Marker used to indicate an element tag.
*/
const ELEMENT_MARKER = '#';
/**
* Marker used to indicate a template tag.
*/
const TEMPLATE_MARKER = '*';
/**
* Marker used to indicate closing of an element or template tag.
*/
const TAG_CLOSE_MARKER = '/';
/**
* Marker used to indicate the sub-template context.
*/
const CONTEXT_MARKER = ':';
/**
* Marker used to indicate the start of a list of values.
*/
const LIST_START_MARKER = '[';
/**
* Marker used to indicate the end of a list of values.
*/
const LIST_END_MARKER = ']';
/**
* Delimiter used to separate multiple values in a list.
*/
const LIST_DELIMITER = '|';
/**
* Formats the param maps on extracted message ops into a maps of `Expression` objects that can be
* used in the final output.
*/
function extractI18nMessages(job) {
// Create an i18n message for each context.
// TODO: Merge the context op with the message op since they're 1:1 anyways.
const i18nMessagesByContext = new Map();
const i18nBlocks = new Map();
const i18nContexts = new Map();
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nContext:
const i18nMessageOp = createI18nMessage(job, op);
unit.create.push(i18nMessageOp);
i18nMessagesByContext.set(op.xref, i18nMessageOp);
i18nContexts.set(op.xref, op);
break;
case OpKind.I18nStart:
i18nBlocks.set(op.xref, op);
break;
}
}
}
// Associate sub-messages for ICUs with their root message. At this point we can also remove the
// ICU start/end ops, as they are no longer needed.
let currentIcu = null;
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.IcuStart:
currentIcu = op;
OpList.remove(op);
// Skip any contexts not associated with an ICU.
const icuContext = i18nContexts.get(op.context);
if (icuContext.contextKind !== I18nContextKind.Icu) {
continue;
}
// Skip ICUs that share a context with their i18n message. These represent root-level
// ICUs, not sub-messages.
const i18nBlock = i18nBlocks.get(icuContext.i18nBlock);
if (i18nBlock.context === icuContext.xref) {
continue;
}
// Find the root message and push this ICUs message as a sub-message.
const rootI18nBlock = i18nBlocks.get(i18nBlock.root);
const rootMessage = i18nMessagesByContext.get(rootI18nBlock.context);
if (rootMessage === undefined) {
throw Error('AssertionError: ICU sub-message should belong to a root message.');
}
const subMessage = i18nMessagesByContext.get(icuContext.xref);
subMessage.messagePlaceholder = op.messagePlaceholder;
rootMessage.subMessages.push(subMessage.xref);
break;
case OpKind.IcuEnd:
currentIcu = null;
OpList.remove(op);
break;
case OpKind.IcuPlaceholder:
// Add ICU placeholders to the message, then remove the ICU placeholder ops.
if (currentIcu === null || currentIcu.context == null) {
throw Error('AssertionError: Unexpected ICU placeholder outside of i18n context');
}
const msg = i18nMessagesByContext.get(currentIcu.context);
msg.postprocessingParams.set(op.name, literal(formatIcuPlaceholder(op)));
OpList.remove(op);
break;
}
}
}
}
/**
* Create an i18n message op from an i18n context op.
*/
function createI18nMessage(job, context, messagePlaceholder) {
let formattedParams = formatParams(context.params);
const formattedPostprocessingParams = formatParams(context.postprocessingParams);
let needsPostprocessing = [...context.params.values()].some(v => v.length > 1);
return createI18nMessageOp(job.allocateXrefId(), context.xref, context.i18nBlock, context.message, messagePlaceholder ?? null, formattedParams, formattedPostprocessingParams, needsPostprocessing);
}
/**
* Formats an ICU placeholder into a single string with expression placeholders.
*/
function formatIcuPlaceholder(op) {
if (op.strings.length !== op.expressionPlaceholders.length + 1) {
throw Error(`AsserionError: Invalid ICU placeholder with ${op.strings.length} strings and ${op.expressionPlaceholders.length} expressions`);
}
const values = op.expressionPlaceholders.map(formatValue);
return op.strings.flatMap((str, i) => [str, values[i] || '']).join('');
}
/**
* Formats a map of `I18nParamValue[]` values into a map of `Expression` values.
*/
function formatParams(params) {
const formattedParams = new Map();
for (const [placeholder, placeholderValues] of params) {
const serializedValues = formatParamValues(placeholderValues);
if (serializedValues !== null) {
formattedParams.set(placeholder, literal(serializedValues));
}
}
return formattedParams;
}
/**
* Formats an `I18nParamValue[]` into a string (or null for empty array).
*/
function formatParamValues(values) {
if (values.length === 0) {
return null;
}
const serializedValues = values.map(value => formatValue(value));
return serializedValues.length === 1 ?
serializedValues[0] :
`${LIST_START_MARKER}${serializedValues.join(LIST_DELIMITER)}${LIST_END_MARKER}`;
}
/**
* Formats a single `I18nParamValue` into a string
*/
function formatValue(value) {
// Element tags with a structural directive use a special form that concatenates the element and
// template values.
if ((value.flags & I18nParamValueFlags.ElementTag) &&
(value.flags & I18nParamValueFlags.TemplateTag)) {
if (typeof value.value !== 'object') {
throw Error('AssertionError: Expected i18n param value to have an element and template slot');
}
const elementValue = formatValue({
...value,
value: value.value.element,
flags: value.flags & ~I18nParamValueFlags.TemplateTag
});
const templateValue = formatValue({
...value,
value: value.value.template,
flags: value.flags & ~I18nParamValueFlags.ElementTag
});
// TODO(mmalerba): This is likely a bug in TemplateDefinitionBuilder, we should not need to
// record the template value twice. For now I'm re-implementing the behavior here to keep the
// output consistent with TemplateDefinitionBuilder.
if ((value.flags & I18nParamValueFlags.OpenTag) &&
(value.flags & I18nParamValueFlags.CloseTag)) {
return `${templateValue}${elementValue}${templateValue}`;
}
// To match the TemplateDefinitionBuilder output, flip the order depending on whether the
// values represent a closing or opening tag (or both).
// TODO(mmalerba): Figure out if this makes a difference in terms of either functionality,
// or the resulting message ID. If not, we can remove the special-casing in the future.
return value.flags & I18nParamValueFlags.CloseTag ? `${elementValue}${templateValue}` :
`${templateValue}${elementValue}`;
}
// Self-closing tags use a special form that concatenates the start and close tag values.
if ((value.flags & I18nParamValueFlags.OpenTag) &&
(value.flags & I18nParamValueFlags.CloseTag)) {
return `${formatValue({ ...value, flags: value.flags & ~I18nParamValueFlags.CloseTag })}${formatValue({ ...value, flags: value.flags & ~I18nParamValueFlags.OpenTag })}`;
}
// If there are no special flags, just return the raw value.
if (value.flags === I18nParamValueFlags.None) {
return `${value.value}`;
}
// Encode the remaining flags as part of the value.
let tagMarker = '';
let closeMarker = '';
if (value.flags & I18nParamValueFlags.ElementTag) {
tagMarker = ELEMENT_MARKER;
}
else if (value.flags & I18nParamValueFlags.TemplateTag) {
tagMarker = TEMPLATE_MARKER;
}
if (tagMarker !== '') {
closeMarker = value.flags & I18nParamValueFlags.CloseTag ? TAG_CLOSE_MARKER : '';
}
const context = value.subTemplateIndex === null ? '' : `${CONTEXT_MARKER}${value.subTemplateIndex}`;
return `${ESCAPE$1}${closeMarker}${tagMarker}${value.value}${context}${ESCAPE$1}`;
}
/**
* Generate `ir.AdvanceOp`s in between `ir.UpdateOp`s that ensure the runtime's implicit slot
* context will be advanced correctly.
*/
function generateAdvance(job) {
for (const unit of job.units) {
// First build a map of all of the declarations in the view that have assigned slots.
const slotMap = new Map();
for (const op of unit.create) {
if (!hasConsumesSlotTrait(op)) {
continue;
}
else if (op.handle.slot === null) {
throw new Error(`AssertionError: expected slots to have been allocated before generating advance() calls`);
}
slotMap.set(op.xref, op.handle.slot);
}
// Next, step through the update operations and generate `ir.AdvanceOp`s as required to ensure
// the runtime's implicit slot counter will be set to the correct slot before executing each
// update operation which depends on it.
//
// To do that, we track what the runtime's slot counter will be through the update operations.
let slotContext = 0;
for (const op of unit.update) {
if (!hasDependsOnSlotContextTrait(op)) {
// `op` doesn't depend on the slot counter, so it can be skipped.
continue;
}
else if (!slotMap.has(op.target)) {
// We expect ops that _do_ depend on the slot counter to point at declarations that exist in
// the `slotMap`.
throw new Error(`AssertionError: reference to unknown slot for target ${op.target}`);
}
const slot = slotMap.get(op.target);
// Does the slot counter need to be adjusted?
if (slotContext !== slot) {
// If so, generate an `ir.AdvanceOp` to advance the counter.
const delta = slot - slotContext;
if (delta < 0) {
throw new Error(`AssertionError: slot counter should never need to move backwards`);
}
OpList.insertBefore(createAdvanceOp(delta, op.sourceSpan), op);
slotContext = slot;
}
}
}
}
/**
* Locate projection slots, populate the each component's `ngContentSelectors` literal field,
* populate `project` arguments, and generate the required `projectionDef` instruction for the job's
* root view.
*/
function generateProjectionDefs(job) {
// TODO: Why does TemplateDefinitionBuilder force a shared constant?
const share = job.compatibility === CompatibilityMode.TemplateDefinitionBuilder;
// Collect all selectors from this component, and its nested views. Also, assign each projection a
// unique ascending projection slot index.
const selectors = [];
let projectionSlotIndex = 0;
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind === OpKind.Projection) {
selectors.push(op.selector);
op.projectionSlotIndex = projectionSlotIndex++;
}
}
}
if (selectors.length > 0) {
// Create the projectionDef array. If we only found a single wildcard selector, then we use the
// default behavior with no arguments instead.
let defExpr = null;
if (selectors.length > 1 || selectors[0] !== '*') {
const def = selectors.map(s => s === '*' ? s : parseSelectorToR3Selector(s));
defExpr = job.pool.getConstLiteral(literalOrArrayLiteral(def), share);
}
// Create the ngContentSelectors constant.
job.contentSelectors = job.pool.getConstLiteral(literalOrArrayLiteral(selectors), share);
// The projection def instruction goes at the beginning of the root view, before any
// `projection` instructions.
job.root.create.prepend([createProjectionDefOp(defExpr)]);
}
}
/**
* Generate a preamble sequence for each view creation block and listener function which declares
* any variables that be referenced in other operations in the block.
*
* Variables generated include:
* * a saved view context to be used to restore the current view in event listeners.
* * the context of the restored view within event listener handlers.
* * context variables from the current view as well as all parent views (including the root
* context if needed).
* * local references from elements within the current view and any lexical parents.
*
* Variables are generated here unconditionally, and may optimized away in future operations if it
* turns out their values (and any side effects) are unused.
*/
function generateVariables(job) {
recursivelyProcessView(job.root, /* there is no parent scope for the root view */ null);
}
/**
* Process the given `ViewCompilation` and generate preambles for it and any listeners that it
* declares.
*
* @param `parentScope` a scope extracted from the parent view which captures any variables which
* should be inherited by this view. `null` if the current view is the root view.
*/
function recursivelyProcessView(view, parentScope) {
// Extract a `Scope` from this view.
const scope = getScopeForView(view, parentScope);
for (const op of view.create) {
switch (op.kind) {
case OpKind.Template:
// Descend into child embedded views.
recursivelyProcessView(view.job.views.get(op.xref), scope);
break;
case OpKind.RepeaterCreate:
// Descend into child embedded views.
recursivelyProcessView(view.job.views.get(op.xref), scope);
if (op.emptyView) {
recursivelyProcessView(view.job.views.get(op.emptyView), scope);
}
break;
case OpKind.Listener:
// Prepend variables to listener handler functions.
op.handlerOps.prepend(generateVariablesInScopeForView(view, scope));
break;
}
}
// Prepend the declarations for all available variables in scope to the `update` block.
const preambleOps = generateVariablesInScopeForView(view, scope);
view.update.prepend(preambleOps);
}
/**
* Process a view and generate a `Scope` representing the variables available for reference within
* that view.
*/
function getScopeForView(view, parent) {
const scope = {
view: view.xref,
viewContextVariable: {
kind: SemanticVariableKind.Context,
name: null,
view: view.xref,
},
contextVariables: new Map(),
aliases: view.aliases,
references: [],
parent,
};
for (const identifier of view.contextVariables.keys()) {
scope.contextVariables.set(identifier, {
kind: SemanticVariableKind.Identifier,
name: null,
identifier,
});
}
for (const op of view.create) {
switch (op.kind) {
case OpKind.ElementStart:
case OpKind.Template:
if (!Array.isArray(op.localRefs)) {
throw new Error(`AssertionError: expected localRefs to be an array`);
}
// Record available local references from this element.
for (let offset = 0; offset < op.localRefs.length; offset++) {
scope.references.push({
name: op.localRefs[offset].name,
targetId: op.xref,
targetSlot: op.handle,
offset,
variable: {
kind: SemanticVariableKind.Identifier,
name: null,
identifier: op.localRefs[offset].name,
},
});
}
break;
}
}
return scope;
}
/**
* Generate declarations for all variables that are in scope for a given view.
*
* This is a recursive process, as views inherit variables available from their parent view, which
* itself may have inherited variables, etc.
*/
function generateVariablesInScopeForView(view, scope) {
const newOps = [];
if (scope.view !== view.xref) {
// Before generating variables for a parent view, we need to switch to the context of the parent
// view with a `nextContext` expression. This context switching operation itself declares a
// variable, because the context of the view may be referenced directly.
newOps.push(createVariableOp(view.job.allocateXrefId(), scope.viewContextVariable, new NextContextExpr(), VariableFlags.None));
}
// Add variables for all context variables available in this scope's view.
const scopeView = view.job.views.get(scope.view);
for (const [name, value] of scopeView.contextVariables) {
const context = new ContextExpr(scope.view);
// We either read the context, or, if the variable is CTX_REF, use the context directly.
const variable = value === CTX_REF ? context : new ReadPropExpr(context, value);
// Add the variable declaration.
newOps.push(createVariableOp(view.job.allocateXrefId(), scope.contextVariables.get(name), variable, VariableFlags.None));
}
for (const alias of scopeView.aliases) {
newOps.push(createVariableOp(view.job.allocateXrefId(), alias, alias.expression.clone(), VariableFlags.AlwaysInline));
}
// Add variables for all local references declared for elements in this scope.
for (const ref of scope.references) {
newOps.push(createVariableOp(view.job.allocateXrefId(), ref.variable, new ReferenceExpr(ref.targetId, ref.targetSlot, ref.offset), VariableFlags.None));
}
if (scope.parent !== null) {
// Recursively add variables from the parent scope.
newOps.push(...generateVariablesInScopeForView(view, scope.parent));
}
return newOps;
}
/**
* `ir.ConstCollectedExpr` may be present in any IR expression. This means that expression needs to
* be lifted into the component const array, and replaced with a reference to the const array at its
*
* usage site. This phase walks the IR and performs this transformation.
*/
function collectConstExpressions(job) {
for (const unit of job.units) {
for (const op of unit.ops()) {
transformExpressionsInOp(op, expr => {
if (!(expr instanceof ConstCollectedExpr)) {
return expr;
}
return literal(job.addConst(expr.expr));
}, VisitorContextFlag.None);
}
}
}
const STYLE_DOT = 'style.';
const CLASS_DOT = 'class.';
const STYLE_BANG = 'style!';
const CLASS_BANG = 'class!';
const BANG_IMPORTANT = '!important';
/**
* Host bindings are compiled using a different parser entrypoint, and are parsed quite differently
* as a result. Therefore, we need to do some extra parsing for host style properties, as compared
* to non-host style properties.
* TODO: Unify host bindings and non-host bindings in the parser.
*/
function parseHostStyleProperties(job) {
for (const op of job.root.update) {
if (!(op.kind === OpKind.Binding && op.bindingKind === BindingKind.Property)) {
continue;
}
if (op.name.endsWith(BANG_IMPORTANT)) {
// Delete any `!important` suffixes from the binding name.
op.name = op.name.substring(0, op.name.length - BANG_IMPORTANT.length);
}
if (op.name.startsWith(STYLE_DOT)) {
op.bindingKind = BindingKind.StyleProperty;
op.name = op.name.substring(STYLE_DOT.length);
if (!isCssCustomProperty$1(op.name)) {
op.name = hyphenate$1(op.name);
}
const { property, suffix } = parseProperty$1(op.name);
op.name = property;
op.unit = suffix;
}
else if (op.name.startsWith(STYLE_BANG)) {
op.bindingKind = BindingKind.StyleProperty;
op.name = 'style';
}
else if (op.name.startsWith(CLASS_DOT)) {
op.bindingKind = BindingKind.ClassName;
op.name = parseProperty$1(op.name.substring(CLASS_DOT.length)).property;
}
else if (op.name.startsWith(CLASS_BANG)) {
op.bindingKind = BindingKind.ClassName;
op.name = parseProperty$1(op.name.substring(CLASS_BANG.length)).property;
}
}
}
/**
* Checks whether property name is a custom CSS property.
* See: https://www.w3.org/TR/css-variables-1
*/
function isCssCustomProperty$1(name) {
return name.startsWith('--');
}
function hyphenate$1(value) {
return value
.replace(/[a-z][A-Z]/g, v => {
return v.charAt(0) + '-' + v.charAt(1);
})
.toLowerCase();
}
function parseProperty$1(name) {
const overrideIndex = name.indexOf('!important');
if (overrideIndex !== -1) {
name = overrideIndex > 0 ? name.substring(0, overrideIndex) : '';
}
let suffix = null;
let property = name;
const unitIndex = name.lastIndexOf('.');
if (unitIndex > 0) {
suffix = name.slice(unitIndex + 1);
property = name.substring(0, unitIndex);
}
return { property, suffix };
}
function mapEntry(key, value) {
return { key, value, quoted: false };
}
function mapLiteral(obj, quoted = false) {
return literalMap(Object.keys(obj).map(key => ({
key,
quoted,
value: obj[key],
})));
}
class IcuSerializerVisitor {
visitText(text) {
return text.value;
}
visitContainer(container) {
return container.children.map(child => child.visit(this)).join('');
}
visitIcu(icu) {
const strCases = Object.keys(icu.cases).map((k) => `${k} {${icu.cases[k].visit(this)}}`);
const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
return result;
}
visitTagPlaceholder(ph) {
return ph.isVoid ?
this.formatPh(ph.startName) :
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
}
visitPlaceholder(ph) {
return this.formatPh(ph.name);
}
visitBlockPlaceholder(ph) {
return `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
}
visitIcuPlaceholder(ph, context) {
return this.formatPh(ph.name);
}
formatPh(value) {
return `{${formatI18nPlaceholderName(value, /* useCamelCase */ false)}}`;
}
}
const serializer = new IcuSerializerVisitor();
function serializeIcuNode(icu) {
return icu.visit(serializer);
}
var TokenType;
(function (TokenType) {
TokenType[TokenType["Character"] = 0] = "Character";
TokenType[TokenType["Identifier"] = 1] = "Identifier";
TokenType[TokenType["PrivateIdentifier"] = 2] = "PrivateIdentifier";
TokenType[TokenType["Keyword"] = 3] = "Keyword";
TokenType[TokenType["String"] = 4] = "String";
TokenType[TokenType["Operator"] = 5] = "Operator";
TokenType[TokenType["Number"] = 6] = "Number";
TokenType[TokenType["Error"] = 7] = "Error";
})(TokenType || (TokenType = {}));
const KEYWORDS = ['var', 'let', 'as', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this'];
class Lexer {
tokenize(text) {
const scanner = new _Scanner(text);
const tokens = [];
let token = scanner.scanToken();
while (token != null) {
tokens.push(token);
token = scanner.scanToken();
}
return tokens;
}
}
class Token {
constructor(index, end, type, numValue, strValue) {
this.index = index;
this.end = end;
this.type = type;
this.numValue = numValue;
this.strValue = strValue;
}
isCharacter(code) {
return this.type == TokenType.Character && this.numValue == code;
}
isNumber() {
return this.type == TokenType.Number;
}
isString() {
return this.type == TokenType.String;
}
isOperator(operator) {
return this.type == TokenType.Operator && this.strValue == operator;
}
isIdentifier() {
return this.type == TokenType.Identifier;
}
isPrivateIdentifier() {
return this.type == TokenType.PrivateIdentifier;
}
isKeyword() {
return this.type == TokenType.Keyword;
}
isKeywordLet() {
return this.type == TokenType.Keyword && this.strValue == 'let';
}
isKeywordAs() {
return this.type == TokenType.Keyword && this.strValue == 'as';
}
isKeywordNull() {
return this.type == TokenType.Keyword && this.strValue == 'null';
}
isKeywordUndefined() {
return this.type == TokenType.Keyword && this.strValue == 'undefined';
}
isKeywordTrue() {
return this.type == TokenType.Keyword && this.strValue == 'true';
}
isKeywordFalse() {
return this.type == TokenType.Keyword && this.strValue == 'false';
}
isKeywordThis() {
return this.type == TokenType.Keyword && this.strValue == 'this';
}
isError() {
return this.type == TokenType.Error;
}
toNumber() {
return this.type == TokenType.Number ? this.numValue : -1;
}
toString() {
switch (this.type) {
case TokenType.Character:
case TokenType.Identifier:
case TokenType.Keyword:
case TokenType.Operator:
case TokenType.PrivateIdentifier:
case TokenType.String:
case TokenType.Error:
return this.strValue;
case TokenType.Number:
return this.numValue.toString();
default:
return null;
}
}
}
function newCharacterToken(index, end, code) {
return new Token(index, end, TokenType.Character, code, String.fromCharCode(code));
}
function newIdentifierToken(index, end, text) {
return new Token(index, end, TokenType.Identifier, 0, text);
}
function newPrivateIdentifierToken(index, end, text) {
return new Token(index, end, TokenType.PrivateIdentifier, 0, text);
}
function newKeywordToken(index, end, text) {
return new Token(index, end, TokenType.Keyword, 0, text);
}
function newOperatorToken(index, end, text) {
return new Token(index, end, TokenType.Operator, 0, text);
}
function newStringToken(index, end, text) {
return new Token(index, end, TokenType.String, 0, text);
}
function newNumberToken(index, end, n) {
return new Token(index, end, TokenType.Number, n, '');
}
function newErrorToken(index, end, message) {
return new Token(index, end, TokenType.Error, 0, message);
}
const EOF = new Token(-1, -1, TokenType.Character, 0, '');
class _Scanner {
constructor(input) {
this.input = input;
this.peek = 0;
this.index = -1;
this.length = input.length;
this.advance();
}
advance() {
this.peek = ++this.index >= this.length ? $EOF : this.input.charCodeAt(this.index);
}
scanToken() {
const input = this.input, length = this.length;
let peek = this.peek, index = this.index;
// Skip whitespace.
while (peek <= $SPACE) {
if (++index >= length) {
peek = $EOF;
break;
}
else {
peek = input.charCodeAt(index);
}
}
this.peek = peek;
this.index = index;
if (index >= length) {
return null;
}
// Handle identifiers and numbers.
if (isIdentifierStart(peek))
return this.scanIdentifier();
if (isDigit(peek))
return this.scanNumber(index);
const start = index;
switch (peek) {
case $PERIOD:
this.advance();
return isDigit(this.peek) ? this.scanNumber(start) :
newCharacterToken(start, this.index, $PERIOD);
case $LPAREN:
case $RPAREN:
case $LBRACE:
case $RBRACE:
case $LBRACKET:
case $RBRACKET:
case $COMMA:
case $COLON:
case $SEMICOLON:
return this.scanCharacter(start, peek);
case $SQ:
case $DQ:
return this.scanString();
case $HASH:
return this.scanPrivateIdentifier();
case $PLUS:
case $MINUS:
case $STAR:
case $SLASH:
case $PERCENT:
case $CARET:
return this.scanOperator(start, String.fromCharCode(peek));
case $QUESTION:
return this.scanQuestion(start);
case $LT:
case $GT:
return this.scanComplexOperator(start, String.fromCharCode(peek), $EQ, '=');
case $BANG:
case $EQ:
return this.scanComplexOperator(start, String.fromCharCode(peek), $EQ, '=', $EQ, '=');
case $AMPERSAND:
return this.scanComplexOperator(start, '&', $AMPERSAND, '&');
case $BAR:
return this.scanComplexOperator(start, '|', $BAR, '|');
case $NBSP:
while (isWhitespace(this.peek))
this.advance();
return this.scanToken();
}
this.advance();
return this.error(`Unexpected character [${String.fromCharCode(peek)}]`, 0);
}
scanCharacter(start, code) {
this.advance();
return newCharacterToken(start, this.index, code);
}
scanOperator(start, str) {
this.advance();
return newOperatorToken(start, this.index, str);
}
/**
* Tokenize a 2/3 char long operator
*
* @param start start index in the expression
* @param one first symbol (always part of the operator)
* @param twoCode code point for the second symbol
* @param two second symbol (part of the operator when the second code point matches)
* @param threeCode code point for the third symbol
* @param three third symbol (part of the operator when provided and matches source expression)
*/
scanComplexOperator(start, one, twoCode, two, threeCode, three) {
this.advance();
let str = one;
if (this.peek == twoCode) {
this.advance();
str += two;
}
if (threeCode != null && this.peek == threeCode) {
this.advance();
str += three;
}
return newOperatorToken(start, this.index, str);
}
scanIdentifier() {
const start = this.index;
this.advance();
while (isIdentifierPart(this.peek))
this.advance();
const str = this.input.substring(start, this.index);
return KEYWORDS.indexOf(str) > -1 ? newKeywordToken(start, this.index, str) :
newIdentifierToken(start, this.index, str);
}
/** Scans an ECMAScript private identifier. */
scanPrivateIdentifier() {
const start = this.index;
this.advance();
if (!isIdentifierStart(this.peek)) {
return this.error('Invalid character [#]', -1);
}
while (isIdentifierPart(this.peek))
this.advance();
const identifierName = this.input.substring(start, this.index);
return newPrivateIdentifierToken(start, this.index, identifierName);
}
scanNumber(start) {
let simple = (this.index === start);
let hasSeparators = false;
this.advance(); // Skip initial digit.
while (true) {
if (isDigit(this.peek)) {
// Do nothing.
}
else if (this.peek === $_) {
// Separators are only valid when they're surrounded by digits. E.g. `1_0_1` is
// valid while `_101` and `101_` are not. The separator can't be next to the decimal
// point or another separator either. Note that it's unlikely that we'll hit a case where
// the underscore is at the start, because that's a valid identifier and it will be picked
// up earlier in the parsing. We validate for it anyway just in case.
if (!isDigit(this.input.charCodeAt(this.index - 1)) ||
!isDigit(this.input.charCodeAt(this.index + 1))) {
return this.error('Invalid numeric separator', 0);
}
hasSeparators = true;
}
else if (this.peek === $PERIOD) {
simple = false;
}
else if (isExponentStart(this.peek)) {
this.advance();
if (isExponentSign(this.peek))
this.advance();
if (!isDigit(this.peek))
return this.error('Invalid exponent', -1);
simple = false;
}
else {
break;
}
this.advance();
}
let str = this.input.substring(start, this.index);
if (hasSeparators) {
str = str.replace(/_/g, '');
}
const value = simple ? parseIntAutoRadix(str) : parseFloat(str);
return newNumberToken(start, this.index, value);
}
scanString() {
const start = this.index;
const quote = this.peek;
this.advance(); // Skip initial quote.
let buffer = '';
let marker = this.index;
const input = this.input;
while (this.peek != quote) {
if (this.peek == $BACKSLASH) {
buffer += input.substring(marker, this.index);
let unescapedCode;
this.advance(); // mutates this.peek
// @ts-expect-error see microsoft/TypeScript#9998
if (this.peek == $u) {
// 4 character hex code for unicode character.
const hex = input.substring(this.index + 1, this.index + 5);
if (/^[0-9a-f]+$/i.test(hex)) {
unescapedCode = parseInt(hex, 16);
}
else {
return this.error(`Invalid unicode escape [\\u${hex}]`, 0);
}
for (let i = 0; i < 5; i++) {
this.advance();
}
}
else {
unescapedCode = unescape(this.peek);
this.advance();
}
buffer += String.fromCharCode(unescapedCode);
marker = this.index;
}
else if (this.peek == $EOF) {
return this.error('Unterminated quote', 0);
}
else {
this.advance();
}
}
const last = input.substring(marker, this.index);
this.advance(); // Skip terminating quote.
return newStringToken(start, this.index, buffer + last);
}
scanQuestion(start) {
this.advance();
let str = '?';
// Either `a ?? b` or 'a?.b'.
if (this.peek === $QUESTION || this.peek === $PERIOD) {
str += this.peek === $PERIOD ? '.' : '?';
this.advance();
}
return newOperatorToken(start, this.index, str);
}
error(message, offset) {
const position = this.index + offset;
return newErrorToken(position, this.index, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}
function isIdentifierStart(code) {
return ($a <= code && code <= $z) || ($A <= code && code <= $Z) ||
(code == $_) || (code == $$);
}
function isIdentifier(input) {
if (input.length == 0)
return false;
const scanner = new _Scanner(input);
if (!isIdentifierStart(scanner.peek))
return false;
scanner.advance();
while (scanner.peek !== $EOF) {
if (!isIdentifierPart(scanner.peek))
return false;
scanner.advance();
}
return true;
}
function isIdentifierPart(code) {
return isAsciiLetter(code) || isDigit(code) || (code == $_) ||
(code == $$);
}
function isExponentStart(code) {
return code == $e || code == $E;
}
function isExponentSign(code) {
return code == $MINUS || code == $PLUS;
}
function unescape(code) {
switch (code) {
case $n:
return $LF;
case $f:
return $FF;
case $r:
return $CR;
case $t:
return $TAB;
case $v:
return $VTAB;
default:
return code;
}
}
function parseIntAutoRadix(text) {
const result = parseInt(text);
if (isNaN(result)) {
throw new Error('Invalid integer literal when parsing ' + text);
}
return result;
}
class SplitInterpolation {
constructor(strings, expressions, offsets) {
this.strings = strings;
this.expressions = expressions;
this.offsets = offsets;
}
}
class TemplateBindingParseResult {
constructor(templateBindings, warnings, errors) {
this.templateBindings = templateBindings;
this.warnings = warnings;
this.errors = errors;
}
}
class Parser$1 {
constructor(_lexer) {
this._lexer = _lexer;
this.errors = [];
}
parseAction(input, isAssignmentEvent, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(sourceToLex);
let flags = 1 /* ParseFlags.Action */;
if (isAssignmentEvent) {
flags |= 2 /* ParseFlags.AssignmentEvent */;
}
const ast = new _ParseAST(input, location, absoluteOffset, tokens, flags, this.errors, 0).parseChain();
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
parseBinding(input, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
checkSimpleExpression(ast) {
const checker = new SimpleExpressionChecker();
ast.visit(checker);
return checker.errors;
}
// Host bindings parsed here
parseSimpleBinding(input, location, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const ast = this._parseBindingAst(input, location, absoluteOffset, interpolationConfig);
const errors = this.checkSimpleExpression(ast);
if (errors.length > 0) {
this._reportError(`Host binding expression cannot contain ${errors.join(' ')}`, input, location);
}
return new ASTWithSource(ast, input, location, absoluteOffset, this.errors);
}
_reportError(message, input, errLocation, ctxLocation) {
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
}
_parseBindingAst(input, location, absoluteOffset, interpolationConfig) {
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(sourceToLex);
return new _ParseAST(input, location, absoluteOffset, tokens, 0 /* ParseFlags.None */, this.errors, 0)
.parseChain();
}
/**
* Parse microsyntax template expression and return a list of bindings or
* parsing errors in case the given expression is invalid.
*
* For example,
* ```
*
* ^ ^ absoluteValueOffset for `templateValue`
* absoluteKeyOffset for `templateKey`
* ```
* contains three bindings:
* 1. ngFor -> null
* 2. item -> NgForOfContext.$implicit
* 3. ngForOf -> items
*
* This is apparent from the de-sugared template:
* ```
*
* ```
*
* @param templateKey name of directive, without the * prefix. For example: ngIf, ngFor
* @param templateValue RHS of the microsyntax attribute
* @param templateUrl template filename if it's external, component filename if it's inline
* @param absoluteKeyOffset start of the `templateKey`
* @param absoluteValueOffset start of the `templateValue`
*/
parseTemplateBindings(templateKey, templateValue, templateUrl, absoluteKeyOffset, absoluteValueOffset) {
const tokens = this._lexer.tokenize(templateValue);
const parser = new _ParseAST(templateValue, templateUrl, absoluteValueOffset, tokens, 0 /* ParseFlags.None */, this.errors, 0 /* relative offset */);
return parser.parseTemplateBindings({
source: templateKey,
span: new AbsoluteSourceSpan(absoluteKeyOffset, absoluteKeyOffset + templateKey.length),
});
}
parseInterpolation(input, location, absoluteOffset, interpolatedTokens, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const { strings, expressions, offsets } = this.splitInterpolation(input, location, interpolatedTokens, interpolationConfig);
if (expressions.length === 0)
return null;
const expressionNodes = [];
for (let i = 0; i < expressions.length; ++i) {
const expressionText = expressions[i].text;
const sourceToLex = this._stripComments(expressionText);
const tokens = this._lexer.tokenize(sourceToLex);
const ast = new _ParseAST(input, location, absoluteOffset, tokens, 0 /* ParseFlags.None */, this.errors, offsets[i])
.parseChain();
expressionNodes.push(ast);
}
return this.createInterpolationAst(strings.map(s => s.text), expressionNodes, input, location, absoluteOffset);
}
/**
* Similar to `parseInterpolation`, but treats the provided string as a single expression
* element that would normally appear within the interpolation prefix and suffix (`{{` and `}}`).
* This is used for parsing the switch expression in ICUs.
*/
parseInterpolationExpression(expression, location, absoluteOffset) {
const sourceToLex = this._stripComments(expression);
const tokens = this._lexer.tokenize(sourceToLex);
const ast = new _ParseAST(expression, location, absoluteOffset, tokens, 0 /* ParseFlags.None */, this.errors, 0)
.parseChain();
const strings = ['', '']; // The prefix and suffix strings are both empty
return this.createInterpolationAst(strings, [ast], expression, location, absoluteOffset);
}
createInterpolationAst(strings, expressions, input, location, absoluteOffset) {
const span = new ParseSpan(0, input.length);
const interpolation = new Interpolation$1(span, span.toAbsolute(absoluteOffset), strings, expressions);
return new ASTWithSource(interpolation, input, location, absoluteOffset, this.errors);
}
/**
* Splits a string of text into "raw" text segments and expressions present in interpolations in
* the string.
* Returns `null` if there are no interpolations, otherwise a
* `SplitInterpolation` with splits that look like
* ...
*/
splitInterpolation(input, location, interpolatedTokens, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
const strings = [];
const expressions = [];
const offsets = [];
const inputToTemplateIndexMap = interpolatedTokens ? getIndexMapForOriginalTemplate(interpolatedTokens) : null;
let i = 0;
let atInterpolation = false;
let extendLastString = false;
let { start: interpStart, end: interpEnd } = interpolationConfig;
while (i < input.length) {
if (!atInterpolation) {
// parse until starting {{
const start = i;
i = input.indexOf(interpStart, i);
if (i === -1) {
i = input.length;
}
const text = input.substring(start, i);
strings.push({ text, start, end: i });
atInterpolation = true;
}
else {
// parse from starting {{ to ending }} while ignoring content inside quotes.
const fullStart = i;
const exprStart = fullStart + interpStart.length;
const exprEnd = this._getInterpolationEndIndex(input, interpEnd, exprStart);
if (exprEnd === -1) {
// Could not find the end of the interpolation; do not parse an expression.
// Instead we should extend the content on the last raw string.
atInterpolation = false;
extendLastString = true;
break;
}
const fullEnd = exprEnd + interpEnd.length;
const text = input.substring(exprStart, exprEnd);
if (text.trim().length === 0) {
this._reportError('Blank expressions are not allowed in interpolated strings', input, `at column ${i} in`, location);
}
expressions.push({ text, start: fullStart, end: fullEnd });
const startInOriginalTemplate = inputToTemplateIndexMap?.get(fullStart) ?? fullStart;
const offset = startInOriginalTemplate + interpStart.length;
offsets.push(offset);
i = fullEnd;
atInterpolation = false;
}
}
if (!atInterpolation) {
// If we are now at a text section, add the remaining content as a raw string.
if (extendLastString) {
const piece = strings[strings.length - 1];
piece.text += input.substring(i);
piece.end = input.length;
}
else {
strings.push({ text: input.substring(i), start: i, end: input.length });
}
}
return new SplitInterpolation(strings, expressions, offsets);
}
wrapLiteralPrimitive(input, location, absoluteOffset) {
const span = new ParseSpan(0, input == null ? 0 : input.length);
return new ASTWithSource(new LiteralPrimitive(span, span.toAbsolute(absoluteOffset), input), input, location, absoluteOffset, this.errors);
}
_stripComments(input) {
const i = this._commentStart(input);
return i != null ? input.substring(0, i) : input;
}
_commentStart(input) {
let outerQuote = null;
for (let i = 0; i < input.length - 1; i++) {
const char = input.charCodeAt(i);
const nextChar = input.charCodeAt(i + 1);
if (char === $SLASH && nextChar == $SLASH && outerQuote == null)
return i;
if (outerQuote === char) {
outerQuote = null;
}
else if (outerQuote == null && isQuote(char)) {
outerQuote = char;
}
}
return null;
}
_checkNoInterpolation(input, location, { start, end }) {
let startIndex = -1;
let endIndex = -1;
for (const charIndex of this._forEachUnquotedChar(input, 0)) {
if (startIndex === -1) {
if (input.startsWith(start)) {
startIndex = charIndex;
}
}
else {
endIndex = this._getInterpolationEndIndex(input, end, charIndex);
if (endIndex > -1) {
break;
}
}
}
if (startIndex > -1 && endIndex > -1) {
this._reportError(`Got interpolation (${start}${end}) where expression was expected`, input, `at column ${startIndex} in`, location);
}
}
/**
* Finds the index of the end of an interpolation expression
* while ignoring comments and quoted content.
*/
_getInterpolationEndIndex(input, expressionEnd, start) {
for (const charIndex of this._forEachUnquotedChar(input, start)) {
if (input.startsWith(expressionEnd, charIndex)) {
return charIndex;
}
// Nothing else in the expression matters after we've
// hit a comment so look directly for the end token.
if (input.startsWith('//', charIndex)) {
return input.indexOf(expressionEnd, charIndex);
}
}
return -1;
}
/**
* Generator used to iterate over the character indexes of a string that are outside of quotes.
* @param input String to loop through.
* @param start Index within the string at which to start.
*/
*_forEachUnquotedChar(input, start) {
let currentQuote = null;
let escapeCount = 0;
for (let i = start; i < input.length; i++) {
const char = input[i];
// Skip the characters inside quotes. Note that we only care about the outer-most
// quotes matching up and we need to account for escape characters.
if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) &&
escapeCount % 2 === 0) {
currentQuote = currentQuote === null ? char : null;
}
else if (currentQuote === null) {
yield i;
}
escapeCount = char === '\\' ? escapeCount + 1 : 0;
}
}
}
/** Describes a stateful context an expression parser is in. */
var ParseContextFlags;
(function (ParseContextFlags) {
ParseContextFlags[ParseContextFlags["None"] = 0] = "None";
/**
* A Writable context is one in which a value may be written to an lvalue.
* For example, after we see a property access, we may expect a write to the
* property via the "=" operator.
* prop
* ^ possible "=" after
*/
ParseContextFlags[ParseContextFlags["Writable"] = 1] = "Writable";
})(ParseContextFlags || (ParseContextFlags = {}));
class _ParseAST {
constructor(input, location, absoluteOffset, tokens, parseFlags, errors, offset) {
this.input = input;
this.location = location;
this.absoluteOffset = absoluteOffset;
this.tokens = tokens;
this.parseFlags = parseFlags;
this.errors = errors;
this.offset = offset;
this.rparensExpected = 0;
this.rbracketsExpected = 0;
this.rbracesExpected = 0;
this.context = ParseContextFlags.None;
// Cache of expression start and input indeces to the absolute source span they map to, used to
// prevent creating superfluous source spans in `sourceSpan`.
// A serial of the expression start and input index is used for mapping because both are stateful
// and may change for subsequent expressions visited by the parser.
this.sourceSpanCache = new Map();
this.index = 0;
}
peek(offset) {
const i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next() {
return this.peek(0);
}
/** Whether all the parser input has been processed. */
get atEOF() {
return this.index >= this.tokens.length;
}
/**
* Index of the next token to be processed, or the end of the last token if all have been
* processed.
*/
get inputIndex() {
return this.atEOF ? this.currentEndIndex : this.next.index + this.offset;
}
/**
* End index of the last processed token, or the start of the first token if none have been
* processed.
*/
get currentEndIndex() {
if (this.index > 0) {
const curToken = this.peek(-1);
return curToken.end + this.offset;
}
// No tokens have been processed yet; return the next token's start or the length of the input
// if there is no token.
if (this.tokens.length === 0) {
return this.input.length + this.offset;
}
return this.next.index + this.offset;
}
/**
* Returns the absolute offset of the start of the current token.
*/
get currentAbsoluteOffset() {
return this.absoluteOffset + this.inputIndex;
}
/**
* Retrieve a `ParseSpan` from `start` to the current position (or to `artificialEndIndex` if
* provided).
*
* @param start Position from which the `ParseSpan` will start.
* @param artificialEndIndex Optional ending index to be used if provided (and if greater than the
* natural ending index)
*/
span(start, artificialEndIndex) {
let endIndex = this.currentEndIndex;
if (artificialEndIndex !== undefined && artificialEndIndex > this.currentEndIndex) {
endIndex = artificialEndIndex;
}
// In some unusual parsing scenarios (like when certain tokens are missing and an `EmptyExpr` is
// being created), the current token may already be advanced beyond the `currentEndIndex`. This
// appears to be a deep-seated parser bug.
//
// As a workaround for now, swap the start and end indices to ensure a valid `ParseSpan`.
// TODO(alxhub): fix the bug upstream in the parser state, and remove this workaround.
if (start > endIndex) {
const tmp = endIndex;
endIndex = start;
start = tmp;
}
return new ParseSpan(start, endIndex);
}
sourceSpan(start, artificialEndIndex) {
const serial = `${start}@${this.inputIndex}:${artificialEndIndex}`;
if (!this.sourceSpanCache.has(serial)) {
this.sourceSpanCache.set(serial, this.span(start, artificialEndIndex).toAbsolute(this.absoluteOffset));
}
return this.sourceSpanCache.get(serial);
}
advance() {
this.index++;
}
/**
* Executes a callback in the provided context.
*/
withContext(context, cb) {
this.context |= context;
const ret = cb();
this.context ^= context;
return ret;
}
consumeOptionalCharacter(code) {
if (this.next.isCharacter(code)) {
this.advance();
return true;
}
else {
return false;
}
}
peekKeywordLet() {
return this.next.isKeywordLet();
}
peekKeywordAs() {
return this.next.isKeywordAs();
}
/**
* Consumes an expected character, otherwise emits an error about the missing expected character
* and skips over the token stream until reaching a recoverable point.
*
* See `this.error` and `this.skip` for more details.
*/
expectCharacter(code) {
if (this.consumeOptionalCharacter(code))
return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
}
consumeOptionalOperator(op) {
if (this.next.isOperator(op)) {
this.advance();
return true;
}
else {
return false;
}
}
expectOperator(operator) {
if (this.consumeOptionalOperator(operator))
return;
this.error(`Missing expected operator ${operator}`);
}
prettyPrintToken(tok) {
return tok === EOF ? 'end of input' : `token ${tok}`;
}
expectIdentifierOrKeyword() {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
if (n.isPrivateIdentifier()) {
this._reportErrorForPrivateIdentifier(n, 'expected identifier or keyword');
}
else {
this.error(`Unexpected ${this.prettyPrintToken(n)}, expected identifier or keyword`);
}
return null;
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString() {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
if (n.isPrivateIdentifier()) {
this._reportErrorForPrivateIdentifier(n, 'expected identifier, keyword or string');
}
else {
this.error(`Unexpected ${this.prettyPrintToken(n)}, expected identifier, keyword, or string`);
}
return '';
}
this.advance();
return n.toString();
}
parseChain() {
const exprs = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
const expr = this.parsePipe();
exprs.push(expr);
if (this.consumeOptionalCharacter($SEMICOLON)) {
if (!(this.parseFlags & 1 /* ParseFlags.Action */)) {
this.error('Binding expression cannot contain chained expression');
}
while (this.consumeOptionalCharacter($SEMICOLON)) {
} // read all semicolons
}
else if (this.index < this.tokens.length) {
const errorIndex = this.index;
this.error(`Unexpected token '${this.next}'`);
// The `error` call above will skip ahead to the next recovery point in an attempt to
// recover part of the expression, but that might be the token we started from which will
// lead to an infinite loop. If that's the case, break the loop assuming that we can't
// parse further.
if (this.index === errorIndex) {
break;
}
}
}
if (exprs.length === 0) {
// We have no expressions so create an empty expression that spans the entire input length
const artificialStart = this.offset;
const artificialEnd = this.offset + this.input.length;
return new EmptyExpr$1(this.span(artificialStart, artificialEnd), this.sourceSpan(artificialStart, artificialEnd));
}
if (exprs.length == 1)
return exprs[0];
return new Chain(this.span(start), this.sourceSpan(start), exprs);
}
parsePipe() {
const start = this.inputIndex;
let result = this.parseExpression();
if (this.consumeOptionalOperator('|')) {
if (this.parseFlags & 1 /* ParseFlags.Action */) {
this.error('Cannot have a pipe in an action expression');
}
do {
const nameStart = this.inputIndex;
let nameId = this.expectIdentifierOrKeyword();
let nameSpan;
let fullSpanEnd = undefined;
if (nameId !== null) {
nameSpan = this.sourceSpan(nameStart);
}
else {
// No valid identifier was found, so we'll assume an empty pipe name ('').
nameId = '';
// However, there may have been whitespace present between the pipe character and the next
// token in the sequence (or the end of input). We want to track this whitespace so that
// the `BindingPipe` we produce covers not just the pipe character, but any trailing
// whitespace beyond it. Another way of thinking about this is that the zero-length name
// is assumed to be at the end of any whitespace beyond the pipe character.
//
// Therefore, we push the end of the `ParseSpan` for this pipe all the way up to the
// beginning of the next token, or until the end of input if the next token is EOF.
fullSpanEnd = this.next.index !== -1 ? this.next.index : this.input.length + this.offset;
// The `nameSpan` for an empty pipe name is zero-length at the end of any whitespace
// beyond the pipe character.
nameSpan = new ParseSpan(fullSpanEnd, fullSpanEnd).toAbsolute(this.absoluteOffset);
}
const args = [];
while (this.consumeOptionalCharacter($COLON)) {
args.push(this.parseExpression());
// If there are additional expressions beyond the name, then the artificial end for the
// name is no longer relevant.
}
result = new BindingPipe(this.span(start), this.sourceSpan(start, fullSpanEnd), result, nameId, args, nameSpan);
} while (this.consumeOptionalOperator('|'));
}
return result;
}
parseExpression() {
return this.parseConditional();
}
parseConditional() {
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.consumeOptionalOperator('?')) {
const yes = this.parsePipe();
let no;
if (!this.consumeOptionalCharacter($COLON)) {
const end = this.inputIndex;
const expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
no = new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
else {
no = this.parsePipe();
}
return new Conditional(this.span(start), this.sourceSpan(start), result, yes, no);
}
else {
return result;
}
}
parseLogicalOr() {
// '||'
const start = this.inputIndex;
let result = this.parseLogicalAnd();
while (this.consumeOptionalOperator('||')) {
const right = this.parseLogicalAnd();
result = new Binary(this.span(start), this.sourceSpan(start), '||', result, right);
}
return result;
}
parseLogicalAnd() {
// '&&'
const start = this.inputIndex;
let result = this.parseNullishCoalescing();
while (this.consumeOptionalOperator('&&')) {
const right = this.parseNullishCoalescing();
result = new Binary(this.span(start), this.sourceSpan(start), '&&', result, right);
}
return result;
}
parseNullishCoalescing() {
// '??'
const start = this.inputIndex;
let result = this.parseEquality();
while (this.consumeOptionalOperator('??')) {
const right = this.parseEquality();
result = new Binary(this.span(start), this.sourceSpan(start), '??', result, right);
}
return result;
}
parseEquality() {
// '==','!=','===','!=='
const start = this.inputIndex;
let result = this.parseRelational();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '==':
case '===':
case '!=':
case '!==':
this.advance();
const right = this.parseRelational();
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseRelational() {
// '<', '>', '<=', '>='
const start = this.inputIndex;
let result = this.parseAdditive();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '<':
case '>':
case '<=':
case '>=':
this.advance();
const right = this.parseAdditive();
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseAdditive() {
// '+', '-'
const start = this.inputIndex;
let result = this.parseMultiplicative();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '+':
case '-':
this.advance();
let right = this.parseMultiplicative();
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parseMultiplicative() {
// '*', '%', '/'
const start = this.inputIndex;
let result = this.parsePrefix();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '*':
case '%':
case '/':
this.advance();
let right = this.parsePrefix();
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
continue;
}
break;
}
return result;
}
parsePrefix() {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
let result;
switch (operator) {
case '+':
this.advance();
result = this.parsePrefix();
return Unary.createPlus(this.span(start), this.sourceSpan(start), result);
case '-':
this.advance();
result = this.parsePrefix();
return Unary.createMinus(this.span(start), this.sourceSpan(start), result);
case '!':
this.advance();
result = this.parsePrefix();
return new PrefixNot(this.span(start), this.sourceSpan(start), result);
}
}
return this.parseCallChain();
}
parseCallChain() {
const start = this.inputIndex;
let result = this.parsePrimary();
while (true) {
if (this.consumeOptionalCharacter($PERIOD)) {
result = this.parseAccessMember(result, start, false);
}
else if (this.consumeOptionalOperator('?.')) {
if (this.consumeOptionalCharacter($LPAREN)) {
result = this.parseCall(result, start, true);
}
else {
result = this.consumeOptionalCharacter($LBRACKET) ?
this.parseKeyedReadOrWrite(result, start, true) :
this.parseAccessMember(result, start, true);
}
}
else if (this.consumeOptionalCharacter($LBRACKET)) {
result = this.parseKeyedReadOrWrite(result, start, false);
}
else if (this.consumeOptionalCharacter($LPAREN)) {
result = this.parseCall(result, start, false);
}
else if (this.consumeOptionalOperator('!')) {
result = new NonNullAssert(this.span(start), this.sourceSpan(start), result);
}
else {
return result;
}
}
}
parsePrimary() {
const start = this.inputIndex;
if (this.consumeOptionalCharacter($LPAREN)) {
this.rparensExpected++;
const result = this.parsePipe();
this.rparensExpected--;
this.expectCharacter($RPAREN);
return result;
}
else if (this.next.isKeywordNull()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), null);
}
else if (this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), void 0);
}
else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), true);
}
else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), false);
}
else if (this.next.isKeywordThis()) {
this.advance();
return new ThisReceiver(this.span(start), this.sourceSpan(start));
}
else if (this.consumeOptionalCharacter($LBRACKET)) {
this.rbracketsExpected++;
const elements = this.parseExpressionList($RBRACKET);
this.rbracketsExpected--;
this.expectCharacter($RBRACKET);
return new LiteralArray(this.span(start), this.sourceSpan(start), elements);
}
else if (this.next.isCharacter($LBRACE)) {
return this.parseLiteralMap();
}
else if (this.next.isIdentifier()) {
return this.parseAccessMember(new ImplicitReceiver(this.span(start), this.sourceSpan(start)), start, false);
}
else if (this.next.isNumber()) {
const value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), value);
}
else if (this.next.isString()) {
const literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), literalValue);
}
else if (this.next.isPrivateIdentifier()) {
this._reportErrorForPrivateIdentifier(this.next, null);
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
else {
this.error(`Unexpected token ${this.next}`);
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
}
parseExpressionList(terminator) {
const result = [];
do {
if (!this.next.isCharacter(terminator)) {
result.push(this.parsePipe());
}
else {
break;
}
} while (this.consumeOptionalCharacter($COMMA));
return result;
}
parseLiteralMap() {
const keys = [];
const values = [];
const start = this.inputIndex;
this.expectCharacter($LBRACE);
if (!this.consumeOptionalCharacter($RBRACE)) {
this.rbracesExpected++;
do {
const keyStart = this.inputIndex;
const quoted = this.next.isString();
const key = this.expectIdentifierOrKeywordOrString();
keys.push({ key, quoted });
// Properties with quoted keys can't use the shorthand syntax.
if (quoted) {
this.expectCharacter($COLON);
values.push(this.parsePipe());
}
else if (this.consumeOptionalCharacter($COLON)) {
values.push(this.parsePipe());
}
else {
const span = this.span(keyStart);
const sourceSpan = this.sourceSpan(keyStart);
values.push(new PropertyRead(span, sourceSpan, sourceSpan, new ImplicitReceiver(span, sourceSpan), key));
}
} while (this.consumeOptionalCharacter($COMMA) &&
!this.next.isCharacter($RBRACE));
this.rbracesExpected--;
this.expectCharacter($RBRACE);
}
return new LiteralMap(this.span(start), this.sourceSpan(start), keys, values);
}
parseAccessMember(readReceiver, start, isSafe) {
const nameStart = this.inputIndex;
const id = this.withContext(ParseContextFlags.Writable, () => {
const id = this.expectIdentifierOrKeyword() ?? '';
if (id.length === 0) {
this.error(`Expected identifier for property access`, readReceiver.span.end);
}
return id;
});
const nameSpan = this.sourceSpan(nameStart);
let receiver;
if (isSafe) {
if (this.consumeOptionalAssignment()) {
this.error('The \'?.\' operator cannot be used in the assignment');
receiver = new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
else {
receiver = new SafePropertyRead(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id);
}
}
else {
if (this.consumeOptionalAssignment()) {
if (!(this.parseFlags & 1 /* ParseFlags.Action */)) {
this.error('Bindings cannot contain assignments');
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
}
const value = this.parseConditional();
receiver = new PropertyWrite(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id, value);
}
else {
receiver =
new PropertyRead(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id);
}
}
return receiver;
}
parseCall(receiver, start, isSafe) {
const argumentStart = this.inputIndex;
this.rparensExpected++;
const args = this.parseCallArguments();
const argumentSpan = this.span(argumentStart, this.inputIndex).toAbsolute(this.absoluteOffset);
this.expectCharacter($RPAREN);
this.rparensExpected--;
const span = this.span(start);
const sourceSpan = this.sourceSpan(start);
return isSafe ? new SafeCall(span, sourceSpan, receiver, args, argumentSpan) :
new Call(span, sourceSpan, receiver, args, argumentSpan);
}
consumeOptionalAssignment() {
// When parsing assignment events (originating from two-way-binding aka banana-in-a-box syntax),
// it is valid for the primary expression to be terminated by the non-null operator. This
// primary expression is substituted as LHS of the assignment operator to achieve
// two-way-binding, such that the LHS could be the non-null operator. The grammar doesn't
// naturally allow for this syntax, so assignment events are parsed specially.
if ((this.parseFlags & 2 /* ParseFlags.AssignmentEvent */) && this.next.isOperator('!') &&
this.peek(1).isOperator('=')) {
// First skip over the ! operator.
this.advance();
// Then skip over the = operator, to fully consume the optional assignment operator.
this.advance();
return true;
}
return this.consumeOptionalOperator('=');
}
parseCallArguments() {
if (this.next.isCharacter($RPAREN))
return [];
const positionals = [];
do {
positionals.push(this.parsePipe());
} while (this.consumeOptionalCharacter($COMMA));
return positionals;
}
/**
* Parses an identifier, a keyword, a string with an optional `-` in between,
* and returns the string along with its absolute source span.
*/
expectTemplateBindingKey() {
let result = '';
let operatorFound = false;
const start = this.currentAbsoluteOffset;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.consumeOptionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return {
source: result,
span: new AbsoluteSourceSpan(start, start + result.length),
};
}
/**
* Parse microsyntax template expression and return a list of bindings or
* parsing errors in case the given expression is invalid.
*
* For example,
* ```
*
* ```
* contains five bindings:
* 1. ngFor -> null
* 2. item -> NgForOfContext.$implicit
* 3. ngForOf -> items
* 4. i -> NgForOfContext.index
* 5. ngForTrackBy -> func
*
* For a full description of the microsyntax grammar, see
* https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855
*
* @param templateKey name of the microsyntax directive, like ngIf, ngFor,
* without the *, along with its absolute span.
*/
parseTemplateBindings(templateKey) {
const bindings = [];
// The first binding is for the template key itself
// In *ngFor="let item of items", key = "ngFor", value = null
// In *ngIf="cond | pipe", key = "ngIf", value = "cond | pipe"
bindings.push(...this.parseDirectiveKeywordBindings(templateKey));
while (this.index < this.tokens.length) {
// If it starts with 'let', then this must be variable declaration
const letBinding = this.parseLetBinding();
if (letBinding) {
bindings.push(letBinding);
}
else {
// Two possible cases here, either `value "as" key` or
// "directive-keyword expression". We don't know which case, but both
// "value" and "directive-keyword" are template binding key, so consume
// the key first.
const key = this.expectTemplateBindingKey();
// Peek at the next token, if it is "as" then this must be variable
// declaration.
const binding = this.parseAsBinding(key);
if (binding) {
bindings.push(binding);
}
else {
// Otherwise the key must be a directive keyword, like "of". Transform
// the key to actual key. Eg. of -> ngForOf, trackBy -> ngForTrackBy
key.source =
templateKey.source + key.source.charAt(0).toUpperCase() + key.source.substring(1);
bindings.push(...this.parseDirectiveKeywordBindings(key));
}
}
this.consumeStatementTerminator();
}
return new TemplateBindingParseResult(bindings, [] /* warnings */, this.errors);
}
parseKeyedReadOrWrite(receiver, start, isSafe) {
return this.withContext(ParseContextFlags.Writable, () => {
this.rbracketsExpected++;
const key = this.parsePipe();
if (key instanceof EmptyExpr$1) {
this.error(`Key access cannot be empty`);
}
this.rbracketsExpected--;
this.expectCharacter($RBRACKET);
if (this.consumeOptionalOperator('=')) {
if (isSafe) {
this.error('The \'?.\' operator cannot be used in the assignment');
}
else {
const value = this.parseConditional();
return new KeyedWrite(this.span(start), this.sourceSpan(start), receiver, key, value);
}
}
else {
return isSafe ? new SafeKeyedRead(this.span(start), this.sourceSpan(start), receiver, key) :
new KeyedRead(this.span(start), this.sourceSpan(start), receiver, key);
}
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
});
}
/**
* Parse a directive keyword, followed by a mandatory expression.
* For example, "of items", "trackBy: func".
* The bindings are: ngForOf -> items, ngForTrackBy -> func
* There could be an optional "as" binding that follows the expression.
* For example,
* ```
* *ngFor="let item of items | slice:0:1 as collection".
* ^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
* keyword bound target optional 'as' binding
* ```
*
* @param key binding key, for example, ngFor, ngIf, ngForOf, along with its
* absolute span.
*/
parseDirectiveKeywordBindings(key) {
const bindings = [];
this.consumeOptionalCharacter($COLON); // trackBy: trackByFunction
const value = this.getDirectiveBoundTarget();
let spanEnd = this.currentAbsoluteOffset;
// The binding could optionally be followed by "as". For example,
// *ngIf="cond | pipe as x". In this case, the key in the "as" binding
// is "x" and the value is the template key itself ("ngIf"). Note that the
// 'key' in the current context now becomes the "value" in the next binding.
const asBinding = this.parseAsBinding(key);
if (!asBinding) {
this.consumeStatementTerminator();
spanEnd = this.currentAbsoluteOffset;
}
const sourceSpan = new AbsoluteSourceSpan(key.span.start, spanEnd);
bindings.push(new ExpressionBinding(sourceSpan, key, value));
if (asBinding) {
bindings.push(asBinding);
}
return bindings;
}
/**
* Return the expression AST for the bound target of a directive keyword
* binding. For example,
* ```
* *ngIf="condition | pipe"
* ^^^^^^^^^^^^^^^^ bound target for "ngIf"
* *ngFor="let item of items"
* ^^^^^ bound target for "ngForOf"
* ```
*/
getDirectiveBoundTarget() {
if (this.next === EOF || this.peekKeywordAs() || this.peekKeywordLet()) {
return null;
}
const ast = this.parsePipe(); // example: "condition | async"
const { start, end } = ast.span;
const value = this.input.substring(start, end);
return new ASTWithSource(ast, value, this.location, this.absoluteOffset + start, this.errors);
}
/**
* Return the binding for a variable declared using `as`. Note that the order
* of the key-value pair in this declaration is reversed. For example,
* ```
* *ngFor="let item of items; index as i"
* ^^^^^ ^
* value key
* ```
*
* @param value name of the value in the declaration, "ngIf" in the example
* above, along with its absolute span.
*/
parseAsBinding(value) {
if (!this.peekKeywordAs()) {
return null;
}
this.advance(); // consume the 'as' keyword
const key = this.expectTemplateBindingKey();
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(value.span.start, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}
/**
* Return the binding for a variable declared using `let`. For example,
* ```
* *ngFor="let item of items; let i=index;"
* ^^^^^^^^ ^^^^^^^^^^^
* ```
* In the first binding, `item` is bound to `NgForOfContext.$implicit`.
* In the second binding, `i` is bound to `NgForOfContext.index`.
*/
parseLetBinding() {
if (!this.peekKeywordLet()) {
return null;
}
const spanStart = this.currentAbsoluteOffset;
this.advance(); // consume the 'let' keyword
const key = this.expectTemplateBindingKey();
let value = null;
if (this.consumeOptionalOperator('=')) {
value = this.expectTemplateBindingKey();
}
this.consumeStatementTerminator();
const sourceSpan = new AbsoluteSourceSpan(spanStart, this.currentAbsoluteOffset);
return new VariableBinding(sourceSpan, key, value);
}
/**
* Consume the optional statement terminator: semicolon or comma.
*/
consumeStatementTerminator() {
this.consumeOptionalCharacter($SEMICOLON) || this.consumeOptionalCharacter($COMMA);
}
/**
* Records an error and skips over the token stream until reaching a recoverable point. See
* `this.skip` for more details on token skipping.
*/
error(message, index = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
locationText(index = null) {
if (index == null)
index = this.index;
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`at the end of the expression`;
}
/**
* Records an error for an unexpected private identifier being discovered.
* @param token Token representing a private identifier.
* @param extraMessage Optional additional message being appended to the error.
*/
_reportErrorForPrivateIdentifier(token, extraMessage) {
let errorMessage = `Private identifiers are not supported. Unexpected private identifier: ${token}`;
if (extraMessage !== null) {
errorMessage += `, ${extraMessage}`;
}
this.error(errorMessage);
}
/**
* Error recovery should skip tokens until it encounters a recovery point.
*
* The following are treated as unconditional recovery points:
* - end of input
* - ';' (parseChain() is always the root production, and it expects a ';')
* - '|' (since pipes may be chained and each pipe expression may be treated independently)
*
* The following are conditional recovery points:
* - ')', '}', ']' if one of calling productions is expecting one of these symbols
* - This allows skip() to recover from errors such as '(a.) + 1' allowing more of the AST to
* be retained (it doesn't skip any tokens as the ')' is retained because of the '(' begins
* an '('
')' production).
* The recovery points of grouping symbols must be conditional as they must be skipped if
* none of the calling productions are not expecting the closing token else we will never
* make progress in the case of an extraneous group closing symbol (such as a stray ')').
* That is, we skip a closing symbol if we are not in a grouping production.
* - '=' in a `Writable` context
* - In this context, we are able to recover after seeing the `=` operator, which
* signals the presence of an independent rvalue expression following the `=` operator.
*
* If a production expects one of these token it increments the corresponding nesting count,
* and then decrements it just prior to checking if the token is in the input.
*/
skip() {
let n = this.next;
while (this.index < this.tokens.length && !n.isCharacter($SEMICOLON) &&
!n.isOperator('|') && (this.rparensExpected <= 0 || !n.isCharacter($RPAREN)) &&
(this.rbracesExpected <= 0 || !n.isCharacter($RBRACE)) &&
(this.rbracketsExpected <= 0 || !n.isCharacter($RBRACKET)) &&
(!(this.context & ParseContextFlags.Writable) || !n.isOperator('='))) {
if (this.next.isError()) {
this.errors.push(new ParserError(this.next.toString(), this.input, this.locationText(), this.location));
}
this.advance();
n = this.next;
}
}
}
class SimpleExpressionChecker extends RecursiveAstVisitor {
constructor() {
super(...arguments);
this.errors = [];
}
visitPipe() {
this.errors.push('pipes');
}
}
/**
* Computes the real offset in the original template for indexes in an interpolation.
*
* Because templates can have encoded HTML entities and the input passed to the parser at this stage
* of the compiler is the _decoded_ value, we need to compute the real offset using the original
* encoded values in the interpolated tokens. Note that this is only a special case handling for
* `MlParserTokenType.ENCODED_ENTITY` token types. All other interpolated tokens are expected to
* have parts which exactly match the input string for parsing the interpolation.
*
* @param interpolatedTokens The tokens for the interpolated value.
*
* @returns A map of index locations in the decoded template to indexes in the original template
*/
function getIndexMapForOriginalTemplate(interpolatedTokens) {
let offsetMap = new Map();
let consumedInOriginalTemplate = 0;
let consumedInInput = 0;
let tokenIndex = 0;
while (tokenIndex < interpolatedTokens.length) {
const currentToken = interpolatedTokens[tokenIndex];
if (currentToken.type === 9 /* MlParserTokenType.ENCODED_ENTITY */) {
const [decoded, encoded] = currentToken.parts;
consumedInOriginalTemplate += encoded.length;
consumedInInput += decoded.length;
}
else {
const lengthOfParts = currentToken.parts.reduce((sum, current) => sum + current.length, 0);
consumedInInput += lengthOfParts;
consumedInOriginalTemplate += lengthOfParts;
}
offsetMap.set(consumedInInput, consumedInOriginalTemplate);
tokenIndex++;
}
return offsetMap;
}
class NodeWithI18n {
constructor(sourceSpan, i18n) {
this.sourceSpan = sourceSpan;
this.i18n = i18n;
}
}
class Text extends NodeWithI18n {
constructor(value, sourceSpan, tokens, i18n) {
super(sourceSpan, i18n);
this.value = value;
this.tokens = tokens;
}
visit(visitor, context) {
return visitor.visitText(this, context);
}
}
class Expansion extends NodeWithI18n {
constructor(switchValue, type, cases, sourceSpan, switchValueSourceSpan, i18n) {
super(sourceSpan, i18n);
this.switchValue = switchValue;
this.type = type;
this.cases = cases;
this.switchValueSourceSpan = switchValueSourceSpan;
}
visit(visitor, context) {
return visitor.visitExpansion(this, context);
}
}
class ExpansionCase {
constructor(value, expression, sourceSpan, valueSourceSpan, expSourceSpan) {
this.value = value;
this.expression = expression;
this.sourceSpan = sourceSpan;
this.valueSourceSpan = valueSourceSpan;
this.expSourceSpan = expSourceSpan;
}
visit(visitor, context) {
return visitor.visitExpansionCase(this, context);
}
}
class Attribute extends NodeWithI18n {
constructor(name, value, sourceSpan, keySpan, valueSpan, valueTokens, i18n) {
super(sourceSpan, i18n);
this.name = name;
this.value = value;
this.keySpan = keySpan;
this.valueSpan = valueSpan;
this.valueTokens = valueTokens;
}
visit(visitor, context) {
return visitor.visitAttribute(this, context);
}
}
class Element extends NodeWithI18n {
constructor(name, attrs, children, sourceSpan, startSourceSpan, endSourceSpan = null, i18n) {
super(sourceSpan, i18n);
this.name = name;
this.attrs = attrs;
this.children = children;
this.startSourceSpan = startSourceSpan;
this.endSourceSpan = endSourceSpan;
}
visit(visitor, context) {
return visitor.visitElement(this, context);
}
}
class Comment {
constructor(value, sourceSpan) {
this.value = value;
this.sourceSpan = sourceSpan;
}
visit(visitor, context) {
return visitor.visitComment(this, context);
}
}
class Block extends NodeWithI18n {
constructor(name, parameters, children, sourceSpan, nameSpan, startSourceSpan, endSourceSpan = null, i18n) {
super(sourceSpan, i18n);
this.name = name;
this.parameters = parameters;
this.children = children;
this.nameSpan = nameSpan;
this.startSourceSpan = startSourceSpan;
this.endSourceSpan = endSourceSpan;
}
visit(visitor, context) {
return visitor.visitBlock(this, context);
}
}
class BlockParameter {
constructor(expression, sourceSpan) {
this.expression = expression;
this.sourceSpan = sourceSpan;
}
visit(visitor, context) {
return visitor.visitBlockParameter(this, context);
}
}
function visitAll(visitor, nodes, context = null) {
const result = [];
const visit = visitor.visit ?
(ast) => visitor.visit(ast, context) || ast.visit(visitor, context) :
(ast) => ast.visit(visitor, context);
nodes.forEach(ast => {
const astResult = visit(ast);
if (astResult) {
result.push(astResult);
}
});
return result;
}
class RecursiveVisitor {
constructor() { }
visitElement(ast, context) {
this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.children);
});
}
visitAttribute(ast, context) { }
visitText(ast, context) { }
visitComment(ast, context) { }
visitExpansion(ast, context) {
return this.visitChildren(context, visit => {
visit(ast.cases);
});
}
visitExpansionCase(ast, context) { }
visitBlock(block, context) {
this.visitChildren(context, visit => {
visit(block.parameters);
visit(block.children);
});
}
visitBlockParameter(ast, context) { }
visitChildren(context, cb) {
let results = [];
let t = this;
function visit(children) {
if (children)
results.push(visitAll(t, children, context));
}
cb(visit);
return Array.prototype.concat.apply([], results);
}
}
class ElementSchemaRegistry {
}
const BOOLEAN = 'boolean';
const NUMBER = 'number';
const STRING = 'string';
const OBJECT = 'object';
/**
* This array represents the DOM schema. It encodes inheritance, properties, and events.
*
* ## Overview
*
* Each line represents one kind of element. The `element_inheritance` and properties are joined
* using `element_inheritance|properties` syntax.
*
* ## Element Inheritance
*
* The `element_inheritance` can be further subdivided as `element1,element2,...^parentElement`.
* Here the individual elements are separated by `,` (commas). Every element in the list
* has identical properties.
*
* An `element` may inherit additional properties from `parentElement` If no `^parentElement` is
* specified then `""` (blank) element is assumed.
*
* NOTE: The blank element inherits from root `[Element]` element, the super element of all
* elements.
*
* NOTE an element prefix such as `:svg:` has no special meaning to the schema.
*
* ## Properties
*
* Each element has a set of properties separated by `,` (commas). Each property can be prefixed
* by a special character designating its type:
*
* - (no prefix): property is a string.
* - `*`: property represents an event.
* - `!`: property is a boolean.
* - `#`: property is a number.
* - `%`: property is an object.
*
* ## Query
*
* The class creates an internal squas representation which allows to easily answer the query of
* if a given property exist on a given element.
*
* NOTE: We don't yet support querying for types or events.
* NOTE: This schema is auto extracted from `schema_extractor.ts` located in the test folder,
* see dom_element_schema_registry_spec.ts
*/
// =================================================================================================
// =================================================================================================
// =========== S T O P - S T O P - S T O P - S T O P - S T O P - S T O P ===========
// =================================================================================================
// =================================================================================================
//
// DO NOT EDIT THIS DOM SCHEMA WITHOUT A SECURITY REVIEW!
//
// Newly added properties must be security reviewed and assigned an appropriate SecurityContext in
// dom_security_schema.ts. Reach out to mprobst & rjamet for details.
//
// =================================================================================================
const SCHEMA = [
'[Element]|textContent,%ariaAtomic,%ariaAutoComplete,%ariaBusy,%ariaChecked,%ariaColCount,%ariaColIndex,%ariaColSpan,%ariaCurrent,%ariaDescription,%ariaDisabled,%ariaExpanded,%ariaHasPopup,%ariaHidden,%ariaKeyShortcuts,%ariaLabel,%ariaLevel,%ariaLive,%ariaModal,%ariaMultiLine,%ariaMultiSelectable,%ariaOrientation,%ariaPlaceholder,%ariaPosInSet,%ariaPressed,%ariaReadOnly,%ariaRelevant,%ariaRequired,%ariaRoleDescription,%ariaRowCount,%ariaRowIndex,%ariaRowSpan,%ariaSelected,%ariaSetSize,%ariaSort,%ariaValueMax,%ariaValueMin,%ariaValueNow,%ariaValueText,%classList,className,elementTiming,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*fullscreenchange,*fullscreenerror,*search,*webkitfullscreenchange,*webkitfullscreenerror,outerHTML,%part,#scrollLeft,#scrollTop,slot' +
/* added manually to avoid breaking changes */
',*message,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored',
'[HTMLElement]^[Element]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',
'abbr,address,article,aside,b,bdi,bdo,cite,content,code,dd,dfn,dt,em,figcaption,figure,footer,header,hgroup,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy',
'media^[HTMLElement]|!autoplay,!controls,%controlsList,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,*waitingforkey,#playbackRate,preload,!preservesPitch,src,%srcObject,#volume',
':svg:^[HTMLElement]|!autofocus,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,%style,#tabIndex',
':svg:graphics^:svg:|',
':svg:animation^:svg:|*begin,*end,*repeat',
':svg:geometry^:svg:|',
':svg:componentTransferFunction^:svg:|',
':svg:gradient^:svg:|',
':svg:textContent^:svg:graphics|',
':svg:textPositioning^:svg:textContent|',
'a^[HTMLElement]|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerPolicy,rel,%relList,rev,search,shape,target,text,type,username',
'area^[HTMLElement]|alt,coords,download,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerPolicy,rel,%relList,search,shape,target,username',
'audio^media|',
'br^[HTMLElement]|clear',
'base^[HTMLElement]|href,target',
'body^[HTMLElement]|aLink,background,bgColor,link,*afterprint,*beforeprint,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*messageerror,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button^[HTMLElement]|!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value',
'canvas^[HTMLElement]|#height,#width',
'content^[HTMLElement]|select',
'dl^[HTMLElement]|!compact',
'data^[HTMLElement]|value',
'datalist^[HTMLElement]|',
'details^[HTMLElement]|!open',
'dialog^[HTMLElement]|!open,returnValue',
'dir^[HTMLElement]|!compact',
'div^[HTMLElement]|align',
'embed^[HTMLElement]|align,height,name,src,type,width',
'fieldset^[HTMLElement]|!disabled,name',
'font^[HTMLElement]|color,face,size',
'form^[HTMLElement]|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target',
'frame^[HTMLElement]|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src',
'frameset^[HTMLElement]|cols,*afterprint,*beforeprint,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*messageerror,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows',
'hr^[HTMLElement]|align,color,!noShade,size,width',
'head^[HTMLElement]|',
'h1,h2,h3,h4,h5,h6^[HTMLElement]|align',
'html^[HTMLElement]|version',
'iframe^[HTMLElement]|align,allow,!allowFullscreen,!allowPaymentRequest,csp,frameBorder,height,loading,longDesc,marginHeight,marginWidth,name,referrerPolicy,%sandbox,scrolling,src,srcdoc,width',
'img^[HTMLElement]|align,alt,border,%crossOrigin,decoding,#height,#hspace,!isMap,loading,longDesc,lowsrc,name,referrerPolicy,sizes,src,srcset,useMap,#vspace,#width',
'input^[HTMLElement]|accept,align,alt,autocomplete,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width',
'li^[HTMLElement]|type,#value',
'label^[HTMLElement]|htmlFor',
'legend^[HTMLElement]|align',
'link^[HTMLElement]|as,charset,%crossOrigin,!disabled,href,hreflang,imageSizes,imageSrcset,integrity,media,referrerPolicy,rel,%relList,rev,%sizes,target,type',
'map^[HTMLElement]|name',
'marquee^[HTMLElement]|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width',
'menu^[HTMLElement]|!compact',
'meta^[HTMLElement]|content,httpEquiv,media,name,scheme',
'meter^[HTMLElement]|#high,#low,#max,#min,#optimum,#value',
'ins,del^[HTMLElement]|cite,dateTime',
'ol^[HTMLElement]|!compact,!reversed,#start,type',
'object^[HTMLElement]|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width',
'optgroup^[HTMLElement]|!disabled,label',
'option^[HTMLElement]|!defaultSelected,!disabled,label,!selected,text,value',
'output^[HTMLElement]|defaultValue,%htmlFor,name,value',
'p^[HTMLElement]|align',
'param^[HTMLElement]|name,type,value,valueType',
'picture^[HTMLElement]|',
'pre^[HTMLElement]|#width',
'progress^[HTMLElement]|#max,#value',
'q,blockquote,cite^[HTMLElement]|',
'script^[HTMLElement]|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,!noModule,%referrerPolicy,src,text,type',
'select^[HTMLElement]|autocomplete,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value',
'slot^[HTMLElement]|name',
'source^[HTMLElement]|#height,media,sizes,src,srcset,type,#width',
'span^[HTMLElement]|',
'style^[HTMLElement]|!disabled,media,type',
'caption^[HTMLElement]|align',
'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width',
'col,colgroup^[HTMLElement]|align,ch,chOff,#span,vAlign,width',
'table^[HTMLElement]|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width',
'tr^[HTMLElement]|align,bgColor,ch,chOff,vAlign',
'tfoot,thead,tbody^[HTMLElement]|align,ch,chOff,vAlign',
'template^[HTMLElement]|',
'textarea^[HTMLElement]|autocomplete,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap',
'time^[HTMLElement]|dateTime',
'title^[HTMLElement]|text',
'track^[HTMLElement]|!default,kind,label,src,srclang',
'ul^[HTMLElement]|!compact,type',
'unknown^[HTMLElement]|',
'video^media|!disablePictureInPicture,#height,*enterpictureinpicture,*leavepictureinpicture,!playsInline,poster,#width',
':svg:a^:svg:graphics|',
':svg:animate^:svg:animation|',
':svg:animateMotion^:svg:animation|',
':svg:animateTransform^:svg:animation|',
':svg:circle^:svg:geometry|',
':svg:clipPath^:svg:graphics|',
':svg:defs^:svg:graphics|',
':svg:desc^:svg:|',
':svg:discard^:svg:|',
':svg:ellipse^:svg:geometry|',
':svg:feBlend^:svg:|',
':svg:feColorMatrix^:svg:|',
':svg:feComponentTransfer^:svg:|',
':svg:feComposite^:svg:|',
':svg:feConvolveMatrix^:svg:|',
':svg:feDiffuseLighting^:svg:|',
':svg:feDisplacementMap^:svg:|',
':svg:feDistantLight^:svg:|',
':svg:feDropShadow^:svg:|',
':svg:feFlood^:svg:|',
':svg:feFuncA^:svg:componentTransferFunction|',
':svg:feFuncB^:svg:componentTransferFunction|',
':svg:feFuncG^:svg:componentTransferFunction|',
':svg:feFuncR^:svg:componentTransferFunction|',
':svg:feGaussianBlur^:svg:|',
':svg:feImage^:svg:|',
':svg:feMerge^:svg:|',
':svg:feMergeNode^:svg:|',
':svg:feMorphology^:svg:|',
':svg:feOffset^:svg:|',
':svg:fePointLight^:svg:|',
':svg:feSpecularLighting^:svg:|',
':svg:feSpotLight^:svg:|',
':svg:feTile^:svg:|',
':svg:feTurbulence^:svg:|',
':svg:filter^:svg:|',
':svg:foreignObject^:svg:graphics|',
':svg:g^:svg:graphics|',
':svg:image^:svg:graphics|decoding',
':svg:line^:svg:geometry|',
':svg:linearGradient^:svg:gradient|',
':svg:mpath^:svg:|',
':svg:marker^:svg:|',
':svg:mask^:svg:|',
':svg:metadata^:svg:|',
':svg:path^:svg:geometry|',
':svg:pattern^:svg:|',
':svg:polygon^:svg:geometry|',
':svg:polyline^:svg:geometry|',
':svg:radialGradient^:svg:gradient|',
':svg:rect^:svg:geometry|',
':svg:svg^:svg:graphics|#currentScale,#zoomAndPan',
':svg:script^:svg:|type',
':svg:set^:svg:animation|',
':svg:stop^:svg:|',
':svg:style^:svg:|!disabled,media,title,type',
':svg:switch^:svg:graphics|',
':svg:symbol^:svg:|',
':svg:tspan^:svg:textPositioning|',
':svg:text^:svg:textPositioning|',
':svg:textPath^:svg:textContent|',
':svg:title^:svg:|',
':svg:use^:svg:graphics|',
':svg:view^:svg:|#zoomAndPan',
'data^[HTMLElement]|value',
'keygen^[HTMLElement]|!autofocus,challenge,!disabled,form,keytype,name',
'menuitem^[HTMLElement]|type,label,icon,!disabled,!checked,radiogroup,!default',
'summary^[HTMLElement]|',
'time^[HTMLElement]|dateTime',
':svg:cursor^:svg:|',
];
const _ATTR_TO_PROP = new Map(Object.entries({
'class': 'className',
'for': 'htmlFor',
'formaction': 'formAction',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
}));
// Invert _ATTR_TO_PROP.
const _PROP_TO_ATTR = Array.from(_ATTR_TO_PROP).reduce((inverted, [propertyName, attributeName]) => {
inverted.set(propertyName, attributeName);
return inverted;
}, new Map());
class DomElementSchemaRegistry extends ElementSchemaRegistry {
constructor() {
super();
this._schema = new Map();
// We don't allow binding to events for security reasons. Allowing event bindings would almost
// certainly introduce bad XSS vulnerabilities. Instead, we store events in a separate schema.
this._eventSchema = new Map;
SCHEMA.forEach(encodedType => {
const type = new Map();
const events = new Set();
const [strType, strProperties] = encodedType.split('|');
const properties = strProperties.split(',');
const [typeNames, superName] = strType.split('^');
typeNames.split(',').forEach(tag => {
this._schema.set(tag.toLowerCase(), type);
this._eventSchema.set(tag.toLowerCase(), events);
});
const superType = superName && this._schema.get(superName.toLowerCase());
if (superType) {
for (const [prop, value] of superType) {
type.set(prop, value);
}
for (const superEvent of this._eventSchema.get(superName.toLowerCase())) {
events.add(superEvent);
}
}
properties.forEach((property) => {
if (property.length > 0) {
switch (property[0]) {
case '*':
events.add(property.substring(1));
break;
case '!':
type.set(property.substring(1), BOOLEAN);
break;
case '#':
type.set(property.substring(1), NUMBER);
break;
case '%':
type.set(property.substring(1), OBJECT);
break;
default:
type.set(property, STRING);
}
}
});
});
}
hasProperty(tagName, propName, schemaMetas) {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) {
return true;
}
if (tagName.indexOf('-') > -1) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return false;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Can't tell now as we don't know which properties a custom element will get
// once it is instantiated
return true;
}
}
const elementProperties = this._schema.get(tagName.toLowerCase()) || this._schema.get('unknown');
return elementProperties.has(propName);
}
hasElement(tagName, schemaMetas) {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) {
return true;
}
if (tagName.indexOf('-') > -1) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return true;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Allow any custom elements
return true;
}
}
return this._schema.has(tagName.toLowerCase());
}
/**
* securityContext returns the security context for the given property on the given DOM tag.
*
* Tag and property name are statically known and cannot change at runtime, i.e. it is not
* possible to bind a value into a changing attribute or tag name.
*
* The filtering is based on a list of allowed tags|attributes. All attributes in the schema
* above are assumed to have the 'NONE' security context, i.e. that they are safe inert
* string values. Only specific well known attack vectors are assigned their appropriate context.
*/
securityContext(tagName, propName, isAttribute) {
if (isAttribute) {
// NB: For security purposes, use the mapped property name, not the attribute name.
propName = this.getMappedPropName(propName);
}
// Make sure comparisons are case insensitive, so that case differences between attribute and
// property names do not have a security impact.
tagName = tagName.toLowerCase();
propName = propName.toLowerCase();
let ctx = SECURITY_SCHEMA()[tagName + '|' + propName];
if (ctx) {
return ctx;
}
ctx = SECURITY_SCHEMA()['*|' + propName];
return ctx ? ctx : SecurityContext.NONE;
}
getMappedPropName(propName) {
return _ATTR_TO_PROP.get(propName) ?? propName;
}
getDefaultComponentElementName() {
return 'ng-component';
}
validateProperty(name) {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...` +
`\nIf '${name}' is a directive input, make sure the directive is imported by the` +
` current module.`;
return { error: true, msg: msg };
}
else {
return { error: false };
}
}
validateAttribute(name) {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...`;
return { error: true, msg: msg };
}
else {
return { error: false };
}
}
allKnownElementNames() {
return Array.from(this._schema.keys());
}
allKnownAttributesOfElement(tagName) {
const elementProperties = this._schema.get(tagName.toLowerCase()) || this._schema.get('unknown');
// Convert properties to attributes.
return Array.from(elementProperties.keys()).map(prop => _PROP_TO_ATTR.get(prop) ?? prop);
}
allKnownEventsOfElement(tagName) {
return Array.from(this._eventSchema.get(tagName.toLowerCase()) ?? []);
}
normalizeAnimationStyleProperty(propName) {
return dashCaseToCamelCase(propName);
}
normalizeAnimationStyleValue(camelCaseProp, userProvidedProp, val) {
let unit = '';
const strVal = val.toString().trim();
let errorMsg = null;
if (_isPixelDimensionStyle(camelCaseProp) && val !== 0 && val !== '0') {
if (typeof val === 'number') {
unit = 'px';
}
else {
const valAndSuffixMatch = val.match(/^[+-]?[\d\.]+([a-z]*)$/);
if (valAndSuffixMatch && valAndSuffixMatch[1].length == 0) {
errorMsg = `Please provide a CSS unit value for ${userProvidedProp}:${val}`;
}
}
}
return { error: errorMsg, value: strVal + unit };
}
}
function _isPixelDimensionStyle(prop) {
switch (prop) {
case 'width':
case 'height':
case 'minWidth':
case 'minHeight':
case 'maxWidth':
case 'maxHeight':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'fontSize':
case 'outlineWidth':
case 'outlineOffset':
case 'paddingTop':
case 'paddingLeft':
case 'paddingBottom':
case 'paddingRight':
case 'marginTop':
case 'marginLeft':
case 'marginBottom':
case 'marginRight':
case 'borderRadius':
case 'borderWidth':
case 'borderTopWidth':
case 'borderLeftWidth':
case 'borderRightWidth':
case 'borderBottomWidth':
case 'textIndent':
return true;
default:
return false;
}
}
class HtmlTagDefinition {
constructor({ closedByChildren, implicitNamespacePrefix, contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false, ignoreFirstLf = false, preventNamespaceInheritance = false, canSelfClose = false, } = {}) {
this.closedByChildren = {};
this.closedByParent = false;
if (closedByChildren && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.isVoid = isVoid;
this.closedByParent = closedByParent || isVoid;
this.implicitNamespacePrefix = implicitNamespacePrefix || null;
this.contentType = contentType;
this.ignoreFirstLf = ignoreFirstLf;
this.preventNamespaceInheritance = preventNamespaceInheritance;
this.canSelfClose = canSelfClose ?? isVoid;
}
isClosedByChild(name) {
return this.isVoid || name.toLowerCase() in this.closedByChildren;
}
getContentType(prefix) {
if (typeof this.contentType === 'object') {
const overrideType = prefix === undefined ? undefined : this.contentType[prefix];
return overrideType ?? this.contentType.default;
}
return this.contentType;
}
}
let DEFAULT_TAG_DEFINITION;
// see https://www.w3.org/TR/html51/syntax.html#optional-tags
// This implementation does not fully conform to the HTML5 spec.
let TAG_DEFINITIONS;
function getHtmlTagDefinition(tagName) {
if (!TAG_DEFINITIONS) {
DEFAULT_TAG_DEFINITION = new HtmlTagDefinition({ canSelfClose: true });
TAG_DEFINITIONS = Object.assign(Object.create(null), {
'base': new HtmlTagDefinition({ isVoid: true }),
'meta': new HtmlTagDefinition({ isVoid: true }),
'area': new HtmlTagDefinition({ isVoid: true }),
'embed': new HtmlTagDefinition({ isVoid: true }),
'link': new HtmlTagDefinition({ isVoid: true }),
'img': new HtmlTagDefinition({ isVoid: true }),
'input': new HtmlTagDefinition({ isVoid: true }),
'param': new HtmlTagDefinition({ isVoid: true }),
'hr': new HtmlTagDefinition({ isVoid: true }),
'br': new HtmlTagDefinition({ isVoid: true }),
'source': new HtmlTagDefinition({ isVoid: true }),
'track': new HtmlTagDefinition({ isVoid: true }),
'wbr': new HtmlTagDefinition({ isVoid: true }),
'p': new HtmlTagDefinition({
closedByChildren: [
'address', 'article', 'aside', 'blockquote', 'div', 'dl', 'fieldset',
'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5',
'h6', 'header', 'hgroup', 'hr', 'main', 'nav', 'ol',
'p', 'pre', 'section', 'table', 'ul'
],
closedByParent: true
}),
'thead': new HtmlTagDefinition({ closedByChildren: ['tbody', 'tfoot'] }),
'tbody': new HtmlTagDefinition({ closedByChildren: ['tbody', 'tfoot'], closedByParent: true }),
'tfoot': new HtmlTagDefinition({ closedByChildren: ['tbody'], closedByParent: true }),
'tr': new HtmlTagDefinition({ closedByChildren: ['tr'], closedByParent: true }),
'td': new HtmlTagDefinition({ closedByChildren: ['td', 'th'], closedByParent: true }),
'th': new HtmlTagDefinition({ closedByChildren: ['td', 'th'], closedByParent: true }),
'col': new HtmlTagDefinition({ isVoid: true }),
'svg': new HtmlTagDefinition({ implicitNamespacePrefix: 'svg' }),
'foreignObject': new HtmlTagDefinition({
// Usually the implicit namespace here would be redundant since it will be inherited from
// the parent `svg`, but we have to do it for `foreignObject`, because the way the parser
// works is that the parent node of an end tag is its own start tag which means that
// the `preventNamespaceInheritance` on `foreignObject` would have it default to the
// implicit namespace which is `html`, unless specified otherwise.
implicitNamespacePrefix: 'svg',
// We want to prevent children of foreignObject from inheriting its namespace, because
// the point of the element is to allow nodes from other namespaces to be inserted.
preventNamespaceInheritance: true,
}),
'math': new HtmlTagDefinition({ implicitNamespacePrefix: 'math' }),
'li': new HtmlTagDefinition({ closedByChildren: ['li'], closedByParent: true }),
'dt': new HtmlTagDefinition({ closedByChildren: ['dt', 'dd'] }),
'dd': new HtmlTagDefinition({ closedByChildren: ['dt', 'dd'], closedByParent: true }),
'rb': new HtmlTagDefinition({ closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true }),
'rt': new HtmlTagDefinition({ closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true }),
'rtc': new HtmlTagDefinition({ closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true }),
'rp': new HtmlTagDefinition({ closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true }),
'optgroup': new HtmlTagDefinition({ closedByChildren: ['optgroup'], closedByParent: true }),
'option': new HtmlTagDefinition({ closedByChildren: ['option', 'optgroup'], closedByParent: true }),
'pre': new HtmlTagDefinition({ ignoreFirstLf: true }),
'listing': new HtmlTagDefinition({ ignoreFirstLf: true }),
'style': new HtmlTagDefinition({ contentType: TagContentType.RAW_TEXT }),
'script': new HtmlTagDefinition({ contentType: TagContentType.RAW_TEXT }),
'title': new HtmlTagDefinition({
// The browser supports two separate `title` tags which have to use
// a different content type: `HTMLTitleElement` and `SVGTitleElement`
contentType: { default: TagContentType.ESCAPABLE_RAW_TEXT, svg: TagContentType.PARSABLE_DATA }
}),
'textarea': new HtmlTagDefinition({ contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true }),
});
new DomElementSchemaRegistry().allKnownElementNames().forEach(knownTagName => {
if (!TAG_DEFINITIONS[knownTagName] && getNsPrefix(knownTagName) === null) {
TAG_DEFINITIONS[knownTagName] = new HtmlTagDefinition({ canSelfClose: false });
}
});
}
// We have to make both a case-sensitive and a case-insensitive lookup, because
// HTML tag names are case insensitive, whereas some SVG tags are case sensitive.
return TAG_DEFINITIONS[tagName] ?? TAG_DEFINITIONS[tagName.toLowerCase()] ??
DEFAULT_TAG_DEFINITION;
}
const TAG_TO_PLACEHOLDER_NAMES = {
'A': 'LINK',
'B': 'BOLD_TEXT',
'BR': 'LINE_BREAK',
'EM': 'EMPHASISED_TEXT',
'H1': 'HEADING_LEVEL1',
'H2': 'HEADING_LEVEL2',
'H3': 'HEADING_LEVEL3',
'H4': 'HEADING_LEVEL4',
'H5': 'HEADING_LEVEL5',
'H6': 'HEADING_LEVEL6',
'HR': 'HORIZONTAL_RULE',
'I': 'ITALIC_TEXT',
'LI': 'LIST_ITEM',
'LINK': 'MEDIA_LINK',
'OL': 'ORDERED_LIST',
'P': 'PARAGRAPH',
'Q': 'QUOTATION',
'S': 'STRIKETHROUGH_TEXT',
'SMALL': 'SMALL_TEXT',
'SUB': 'SUBSTRIPT',
'SUP': 'SUPERSCRIPT',
'TBODY': 'TABLE_BODY',
'TD': 'TABLE_CELL',
'TFOOT': 'TABLE_FOOTER',
'TH': 'TABLE_HEADER_CELL',
'THEAD': 'TABLE_HEADER',
'TR': 'TABLE_ROW',
'TT': 'MONOSPACED_TEXT',
'U': 'UNDERLINED_TEXT',
'UL': 'UNORDERED_LIST',
};
/**
* Creates unique names for placeholder with different content.
*
* Returns the same placeholder name when the content is identical.
*/
class PlaceholderRegistry {
constructor() {
// Count the occurrence of the base name top generate a unique name
this._placeHolderNameCounts = {};
// Maps signature to placeholder names
this._signatureToName = {};
}
getStartTagPlaceholderName(tag, attrs, isVoid) {
const signature = this._hashTag(tag, attrs, isVoid);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const upperTag = tag.toUpperCase();
const baseName = TAG_TO_PLACEHOLDER_NAMES[upperTag] || `TAG_${upperTag}`;
const name = this._generateUniqueName(isVoid ? baseName : `START_${baseName}`);
this._signatureToName[signature] = name;
return name;
}
getCloseTagPlaceholderName(tag) {
const signature = this._hashClosingTag(tag);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const upperTag = tag.toUpperCase();
const baseName = TAG_TO_PLACEHOLDER_NAMES[upperTag] || `TAG_${upperTag}`;
const name = this._generateUniqueName(`CLOSE_${baseName}`);
this._signatureToName[signature] = name;
return name;
}
getPlaceholderName(name, content) {
const upperName = name.toUpperCase();
const signature = `PH: ${upperName}=${content}`;
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const uniqueName = this._generateUniqueName(upperName);
this._signatureToName[signature] = uniqueName;
return uniqueName;
}
getUniquePlaceholder(name) {
return this._generateUniqueName(name.toUpperCase());
}
getStartBlockPlaceholderName(name, parameters) {
const signature = this._hashBlock(name, parameters);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const placeholder = this._generateUniqueName(`START_BLOCK_${this._toSnakeCase(name)}`);
this._signatureToName[signature] = placeholder;
return placeholder;
}
getCloseBlockPlaceholderName(name) {
const signature = this._hashClosingBlock(name);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const placeholder = this._generateUniqueName(`CLOSE_BLOCK_${this._toSnakeCase(name)}`);
this._signatureToName[signature] = placeholder;
return placeholder;
}
// Generate a hash for a tag - does not take attribute order into account
_hashTag(tag, attrs, isVoid) {
const start = `<${tag}`;
const strAttrs = Object.keys(attrs).sort().map((name) => ` ${name}=${attrs[name]}`).join('');
const end = isVoid ? '/>' : `>${tag}>`;
return start + strAttrs + end;
}
_hashClosingTag(tag) {
return this._hashTag(`/${tag}`, {}, false);
}
_hashBlock(name, parameters) {
const params = parameters.length === 0 ? '' : ` (${parameters.sort().join('; ')})`;
return `@${name}${params} {}`;
}
_hashClosingBlock(name) {
return this._hashBlock(`close_${name}`, []);
}
_toSnakeCase(name) {
return name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
}
_generateUniqueName(base) {
const seen = this._placeHolderNameCounts.hasOwnProperty(base);
if (!seen) {
this._placeHolderNameCounts[base] = 1;
return base;
}
const id = this._placeHolderNameCounts[base];
this._placeHolderNameCounts[base] = id + 1;
return `${base}_${id}`;
}
}
const _expParser = new Parser$1(new Lexer());
/**
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
*/
function createI18nMessageFactory(interpolationConfig, containerBlocks) {
const visitor = new _I18nVisitor(_expParser, interpolationConfig, containerBlocks);
return (nodes, meaning, description, customId, visitNodeFn) => visitor.toI18nMessage(nodes, meaning, description, customId, visitNodeFn);
}
function noopVisitNodeFn(_html, i18n) {
return i18n;
}
class _I18nVisitor {
constructor(_expressionParser, _interpolationConfig, _containerBlocks) {
this._expressionParser = _expressionParser;
this._interpolationConfig = _interpolationConfig;
this._containerBlocks = _containerBlocks;
}
toI18nMessage(nodes, meaning = '', description = '', customId = '', visitNodeFn) {
const context = {
isIcu: nodes.length == 1 && nodes[0] instanceof Expansion,
icuDepth: 0,
placeholderRegistry: new PlaceholderRegistry(),
placeholderToContent: {},
placeholderToMessage: {},
visitNodeFn: visitNodeFn || noopVisitNodeFn,
};
const i18nodes = visitAll(this, nodes, context);
return new Message(i18nodes, context.placeholderToContent, context.placeholderToMessage, meaning, description, customId);
}
visitElement(el, context) {
const children = visitAll(this, el.children, context);
const attrs = {};
el.attrs.forEach(attr => {
// Do not visit the attributes, translatable ones are top-level ASTs
attrs[attr.name] = attr.value;
});
const isVoid = getHtmlTagDefinition(el.name).isVoid;
const startPhName = context.placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
context.placeholderToContent[startPhName] = {
text: el.startSourceSpan.toString(),
sourceSpan: el.startSourceSpan,
};
let closePhName = '';
if (!isVoid) {
closePhName = context.placeholderRegistry.getCloseTagPlaceholderName(el.name);
context.placeholderToContent[closePhName] = {
text: `${el.name}>`,
sourceSpan: el.endSourceSpan ?? el.sourceSpan,
};
}
const node = new TagPlaceholder(el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
return context.visitNodeFn(el, node);
}
visitAttribute(attribute, context) {
const node = attribute.valueTokens === undefined || attribute.valueTokens.length === 1 ?
new Text$2(attribute.value, attribute.valueSpan || attribute.sourceSpan) :
this._visitTextWithInterpolation(attribute.valueTokens, attribute.valueSpan || attribute.sourceSpan, context, attribute.i18n);
return context.visitNodeFn(attribute, node);
}
visitText(text, context) {
const node = text.tokens.length === 1 ?
new Text$2(text.value, text.sourceSpan) :
this._visitTextWithInterpolation(text.tokens, text.sourceSpan, context, text.i18n);
return context.visitNodeFn(text, node);
}
visitComment(comment, context) {
return null;
}
visitExpansion(icu, context) {
context.icuDepth++;
const i18nIcuCases = {};
const i18nIcu = new Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
icu.cases.forEach((caze) => {
i18nIcuCases[caze.value] = new Container(caze.expression.map((node) => node.visit(this, context)), caze.expSourceSpan);
});
context.icuDepth--;
if (context.isIcu || context.icuDepth > 0) {
// Returns an ICU node when:
// - the message (vs a part of the message) is an ICU message, or
// - the ICU message is nested.
const expPh = context.placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
i18nIcu.expressionPlaceholder = expPh;
context.placeholderToContent[expPh] = {
text: icu.switchValue,
sourceSpan: icu.switchValueSourceSpan,
};
return context.visitNodeFn(icu, i18nIcu);
}
// Else returns a placeholder
// ICU placeholders should not be replaced with their original content but with the their
// translations.
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = context.placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
context.placeholderToMessage[phName] = this.toI18nMessage([icu], '', '', '', undefined);
const node = new IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
return context.visitNodeFn(icu, node);
}
visitExpansionCase(_icuCase, _context) {
throw new Error('Unreachable code');
}
visitBlock(block, context) {
const children = visitAll(this, block.children, context);
if (this._containerBlocks.has(block.name)) {
return new Container(children, block.sourceSpan);
}
const parameters = block.parameters.map(param => param.expression);
const startPhName = context.placeholderRegistry.getStartBlockPlaceholderName(block.name, parameters);
const closePhName = context.placeholderRegistry.getCloseBlockPlaceholderName(block.name);
context.placeholderToContent[startPhName] = {
text: block.startSourceSpan.toString(),
sourceSpan: block.startSourceSpan,
};
context.placeholderToContent[closePhName] = {
text: block.endSourceSpan ? block.endSourceSpan.toString() : '}',
sourceSpan: block.endSourceSpan ?? block.sourceSpan,
};
const node = new BlockPlaceholder(block.name, parameters, startPhName, closePhName, children, block.sourceSpan, block.startSourceSpan, block.endSourceSpan);
return context.visitNodeFn(block, node);
}
visitBlockParameter(_parameter, _context) {
throw new Error('Unreachable code');
}
/**
* Convert, text and interpolated tokens up into text and placeholder pieces.
*
* @param tokens The text and interpolated tokens.
* @param sourceSpan The span of the whole of the `text` string.
* @param context The current context of the visitor, used to compute and store placeholders.
* @param previousI18n Any i18n metadata associated with this `text` from a previous pass.
*/
_visitTextWithInterpolation(tokens, sourceSpan, context, previousI18n) {
// Return a sequence of `Text` and `Placeholder` nodes grouped in a `Container`.
const nodes = [];
// We will only create a container if there are actually interpolations,
// so this flag tracks that.
let hasInterpolation = false;
for (const token of tokens) {
switch (token.type) {
case 8 /* TokenType.INTERPOLATION */:
case 17 /* TokenType.ATTR_VALUE_INTERPOLATION */:
hasInterpolation = true;
const expression = token.parts[1];
const baseName = extractPlaceholderName(expression) || 'INTERPOLATION';
const phName = context.placeholderRegistry.getPlaceholderName(baseName, expression);
context.placeholderToContent[phName] = {
text: token.parts.join(''),
sourceSpan: token.sourceSpan
};
nodes.push(new Placeholder(expression, phName, token.sourceSpan));
break;
default:
if (token.parts[0].length > 0) {
// This token is text or an encoded entity.
// If it is following on from a previous text node then merge it into that node
// Otherwise, if it is following an interpolation, then add a new node.
const previous = nodes[nodes.length - 1];
if (previous instanceof Text$2) {
previous.value += token.parts[0];
previous.sourceSpan = new ParseSourceSpan(previous.sourceSpan.start, token.sourceSpan.end, previous.sourceSpan.fullStart, previous.sourceSpan.details);
}
else {
nodes.push(new Text$2(token.parts[0], token.sourceSpan));
}
}
break;
}
}
if (hasInterpolation) {
// Whitespace removal may have invalidated the interpolation source-spans.
reusePreviousSourceSpans(nodes, previousI18n);
return new Container(nodes, sourceSpan);
}
else {
return nodes[0];
}
}
}
/**
* Re-use the source-spans from `previousI18n` metadata for the `nodes`.
*
* Whitespace removal can invalidate the source-spans of interpolation nodes, so we
* reuse the source-span stored from a previous pass before the whitespace was removed.
*
* @param nodes The `Text` and `Placeholder` nodes to be processed.
* @param previousI18n Any i18n metadata for these `nodes` stored from a previous pass.
*/
function reusePreviousSourceSpans(nodes, previousI18n) {
if (previousI18n instanceof Message) {
// The `previousI18n` is an i18n `Message`, so we are processing an `Attribute` with i18n
// metadata. The `Message` should consist only of a single `Container` that contains the
// parts (`Text` and `Placeholder`) to process.
assertSingleContainerMessage(previousI18n);
previousI18n = previousI18n.nodes[0];
}
if (previousI18n instanceof Container) {
// The `previousI18n` is a `Container`, which means that this is a second i18n extraction pass
// after whitespace has been removed from the AST nodes.
assertEquivalentNodes(previousI18n.children, nodes);
// Reuse the source-spans from the first pass.
for (let i = 0; i < nodes.length; i++) {
nodes[i].sourceSpan = previousI18n.children[i].sourceSpan;
}
}
}
/**
* Asserts that the `message` contains exactly one `Container` node.
*/
function assertSingleContainerMessage(message) {
const nodes = message.nodes;
if (nodes.length !== 1 || !(nodes[0] instanceof Container)) {
throw new Error('Unexpected previous i18n message - expected it to consist of only a single `Container` node.');
}
}
/**
* Asserts that the `previousNodes` and `node` collections have the same number of elements and
* corresponding elements have the same node type.
*/
function assertEquivalentNodes(previousNodes, nodes) {
if (previousNodes.length !== nodes.length) {
throw new Error('The number of i18n message children changed between first and second pass.');
}
if (previousNodes.some((node, i) => nodes[i].constructor !== node.constructor)) {
throw new Error('The types of the i18n message children changed between first and second pass.');
}
}
const _CUSTOM_PH_EXP = /\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*("|')([\s\S]*?)\1[\s\S]*\)/g;
function extractPlaceholderName(input) {
return input.split(_CUSTOM_PH_EXP)[2];
}
/**
* An i18n error.
*/
class I18nError extends ParseError {
constructor(span, msg) {
super(span, msg);
}
}
// Mapping between all HTML entity names and their unicode representation.
// Generated from https://html.spec.whatwg.org/multipage/entities.json by stripping
// the `&` and `;` from the keys and removing the duplicates.
// see https://www.w3.org/TR/html51/syntax.html#named-character-references
const NAMED_ENTITIES = {
'AElig': '\u00C6',
'AMP': '\u0026',
'amp': '\u0026',
'Aacute': '\u00C1',
'Abreve': '\u0102',
'Acirc': '\u00C2',
'Acy': '\u0410',
'Afr': '\uD835\uDD04',
'Agrave': '\u00C0',
'Alpha': '\u0391',
'Amacr': '\u0100',
'And': '\u2A53',
'Aogon': '\u0104',
'Aopf': '\uD835\uDD38',
'ApplyFunction': '\u2061',
'af': '\u2061',
'Aring': '\u00C5',
'angst': '\u00C5',
'Ascr': '\uD835\uDC9C',
'Assign': '\u2254',
'colone': '\u2254',
'coloneq': '\u2254',
'Atilde': '\u00C3',
'Auml': '\u00C4',
'Backslash': '\u2216',
'setminus': '\u2216',
'setmn': '\u2216',
'smallsetminus': '\u2216',
'ssetmn': '\u2216',
'Barv': '\u2AE7',
'Barwed': '\u2306',
'doublebarwedge': '\u2306',
'Bcy': '\u0411',
'Because': '\u2235',
'becaus': '\u2235',
'because': '\u2235',
'Bernoullis': '\u212C',
'Bscr': '\u212C',
'bernou': '\u212C',
'Beta': '\u0392',
'Bfr': '\uD835\uDD05',
'Bopf': '\uD835\uDD39',
'Breve': '\u02D8',
'breve': '\u02D8',
'Bumpeq': '\u224E',
'HumpDownHump': '\u224E',
'bump': '\u224E',
'CHcy': '\u0427',
'COPY': '\u00A9',
'copy': '\u00A9',
'Cacute': '\u0106',
'Cap': '\u22D2',
'CapitalDifferentialD': '\u2145',
'DD': '\u2145',
'Cayleys': '\u212D',
'Cfr': '\u212D',
'Ccaron': '\u010C',
'Ccedil': '\u00C7',
'Ccirc': '\u0108',
'Cconint': '\u2230',
'Cdot': '\u010A',
'Cedilla': '\u00B8',
'cedil': '\u00B8',
'CenterDot': '\u00B7',
'centerdot': '\u00B7',
'middot': '\u00B7',
'Chi': '\u03A7',
'CircleDot': '\u2299',
'odot': '\u2299',
'CircleMinus': '\u2296',
'ominus': '\u2296',
'CirclePlus': '\u2295',
'oplus': '\u2295',
'CircleTimes': '\u2297',
'otimes': '\u2297',
'ClockwiseContourIntegral': '\u2232',
'cwconint': '\u2232',
'CloseCurlyDoubleQuote': '\u201D',
'rdquo': '\u201D',
'rdquor': '\u201D',
'CloseCurlyQuote': '\u2019',
'rsquo': '\u2019',
'rsquor': '\u2019',
'Colon': '\u2237',
'Proportion': '\u2237',
'Colone': '\u2A74',
'Congruent': '\u2261',
'equiv': '\u2261',
'Conint': '\u222F',
'DoubleContourIntegral': '\u222F',
'ContourIntegral': '\u222E',
'conint': '\u222E',
'oint': '\u222E',
'Copf': '\u2102',
'complexes': '\u2102',
'Coproduct': '\u2210',
'coprod': '\u2210',
'CounterClockwiseContourIntegral': '\u2233',
'awconint': '\u2233',
'Cross': '\u2A2F',
'Cscr': '\uD835\uDC9E',
'Cup': '\u22D3',
'CupCap': '\u224D',
'asympeq': '\u224D',
'DDotrahd': '\u2911',
'DJcy': '\u0402',
'DScy': '\u0405',
'DZcy': '\u040F',
'Dagger': '\u2021',
'ddagger': '\u2021',
'Darr': '\u21A1',
'Dashv': '\u2AE4',
'DoubleLeftTee': '\u2AE4',
'Dcaron': '\u010E',
'Dcy': '\u0414',
'Del': '\u2207',
'nabla': '\u2207',
'Delta': '\u0394',
'Dfr': '\uD835\uDD07',
'DiacriticalAcute': '\u00B4',
'acute': '\u00B4',
'DiacriticalDot': '\u02D9',
'dot': '\u02D9',
'DiacriticalDoubleAcute': '\u02DD',
'dblac': '\u02DD',
'DiacriticalGrave': '\u0060',
'grave': '\u0060',
'DiacriticalTilde': '\u02DC',
'tilde': '\u02DC',
'Diamond': '\u22C4',
'diam': '\u22C4',
'diamond': '\u22C4',
'DifferentialD': '\u2146',
'dd': '\u2146',
'Dopf': '\uD835\uDD3B',
'Dot': '\u00A8',
'DoubleDot': '\u00A8',
'die': '\u00A8',
'uml': '\u00A8',
'DotDot': '\u20DC',
'DotEqual': '\u2250',
'doteq': '\u2250',
'esdot': '\u2250',
'DoubleDownArrow': '\u21D3',
'Downarrow': '\u21D3',
'dArr': '\u21D3',
'DoubleLeftArrow': '\u21D0',
'Leftarrow': '\u21D0',
'lArr': '\u21D0',
'DoubleLeftRightArrow': '\u21D4',
'Leftrightarrow': '\u21D4',
'hArr': '\u21D4',
'iff': '\u21D4',
'DoubleLongLeftArrow': '\u27F8',
'Longleftarrow': '\u27F8',
'xlArr': '\u27F8',
'DoubleLongLeftRightArrow': '\u27FA',
'Longleftrightarrow': '\u27FA',
'xhArr': '\u27FA',
'DoubleLongRightArrow': '\u27F9',
'Longrightarrow': '\u27F9',
'xrArr': '\u27F9',
'DoubleRightArrow': '\u21D2',
'Implies': '\u21D2',
'Rightarrow': '\u21D2',
'rArr': '\u21D2',
'DoubleRightTee': '\u22A8',
'vDash': '\u22A8',
'DoubleUpArrow': '\u21D1',
'Uparrow': '\u21D1',
'uArr': '\u21D1',
'DoubleUpDownArrow': '\u21D5',
'Updownarrow': '\u21D5',
'vArr': '\u21D5',
'DoubleVerticalBar': '\u2225',
'par': '\u2225',
'parallel': '\u2225',
'shortparallel': '\u2225',
'spar': '\u2225',
'DownArrow': '\u2193',
'ShortDownArrow': '\u2193',
'darr': '\u2193',
'downarrow': '\u2193',
'DownArrowBar': '\u2913',
'DownArrowUpArrow': '\u21F5',
'duarr': '\u21F5',
'DownBreve': '\u0311',
'DownLeftRightVector': '\u2950',
'DownLeftTeeVector': '\u295E',
'DownLeftVector': '\u21BD',
'leftharpoondown': '\u21BD',
'lhard': '\u21BD',
'DownLeftVectorBar': '\u2956',
'DownRightTeeVector': '\u295F',
'DownRightVector': '\u21C1',
'rhard': '\u21C1',
'rightharpoondown': '\u21C1',
'DownRightVectorBar': '\u2957',
'DownTee': '\u22A4',
'top': '\u22A4',
'DownTeeArrow': '\u21A7',
'mapstodown': '\u21A7',
'Dscr': '\uD835\uDC9F',
'Dstrok': '\u0110',
'ENG': '\u014A',
'ETH': '\u00D0',
'Eacute': '\u00C9',
'Ecaron': '\u011A',
'Ecirc': '\u00CA',
'Ecy': '\u042D',
'Edot': '\u0116',
'Efr': '\uD835\uDD08',
'Egrave': '\u00C8',
'Element': '\u2208',
'in': '\u2208',
'isin': '\u2208',
'isinv': '\u2208',
'Emacr': '\u0112',
'EmptySmallSquare': '\u25FB',
'EmptyVerySmallSquare': '\u25AB',
'Eogon': '\u0118',
'Eopf': '\uD835\uDD3C',
'Epsilon': '\u0395',
'Equal': '\u2A75',
'EqualTilde': '\u2242',
'eqsim': '\u2242',
'esim': '\u2242',
'Equilibrium': '\u21CC',
'rightleftharpoons': '\u21CC',
'rlhar': '\u21CC',
'Escr': '\u2130',
'expectation': '\u2130',
'Esim': '\u2A73',
'Eta': '\u0397',
'Euml': '\u00CB',
'Exists': '\u2203',
'exist': '\u2203',
'ExponentialE': '\u2147',
'ee': '\u2147',
'exponentiale': '\u2147',
'Fcy': '\u0424',
'Ffr': '\uD835\uDD09',
'FilledSmallSquare': '\u25FC',
'FilledVerySmallSquare': '\u25AA',
'blacksquare': '\u25AA',
'squarf': '\u25AA',
'squf': '\u25AA',
'Fopf': '\uD835\uDD3D',
'ForAll': '\u2200',
'forall': '\u2200',
'Fouriertrf': '\u2131',
'Fscr': '\u2131',
'GJcy': '\u0403',
'GT': '\u003E',
'gt': '\u003E',
'Gamma': '\u0393',
'Gammad': '\u03DC',
'Gbreve': '\u011E',
'Gcedil': '\u0122',
'Gcirc': '\u011C',
'Gcy': '\u0413',
'Gdot': '\u0120',
'Gfr': '\uD835\uDD0A',
'Gg': '\u22D9',
'ggg': '\u22D9',
'Gopf': '\uD835\uDD3E',
'GreaterEqual': '\u2265',
'ge': '\u2265',
'geq': '\u2265',
'GreaterEqualLess': '\u22DB',
'gel': '\u22DB',
'gtreqless': '\u22DB',
'GreaterFullEqual': '\u2267',
'gE': '\u2267',
'geqq': '\u2267',
'GreaterGreater': '\u2AA2',
'GreaterLess': '\u2277',
'gl': '\u2277',
'gtrless': '\u2277',
'GreaterSlantEqual': '\u2A7E',
'geqslant': '\u2A7E',
'ges': '\u2A7E',
'GreaterTilde': '\u2273',
'gsim': '\u2273',
'gtrsim': '\u2273',
'Gscr': '\uD835\uDCA2',
'Gt': '\u226B',
'NestedGreaterGreater': '\u226B',
'gg': '\u226B',
'HARDcy': '\u042A',
'Hacek': '\u02C7',
'caron': '\u02C7',
'Hat': '\u005E',
'Hcirc': '\u0124',
'Hfr': '\u210C',
'Poincareplane': '\u210C',
'HilbertSpace': '\u210B',
'Hscr': '\u210B',
'hamilt': '\u210B',
'Hopf': '\u210D',
'quaternions': '\u210D',
'HorizontalLine': '\u2500',
'boxh': '\u2500',
'Hstrok': '\u0126',
'HumpEqual': '\u224F',
'bumpe': '\u224F',
'bumpeq': '\u224F',
'IEcy': '\u0415',
'IJlig': '\u0132',
'IOcy': '\u0401',
'Iacute': '\u00CD',
'Icirc': '\u00CE',
'Icy': '\u0418',
'Idot': '\u0130',
'Ifr': '\u2111',
'Im': '\u2111',
'image': '\u2111',
'imagpart': '\u2111',
'Igrave': '\u00CC',
'Imacr': '\u012A',
'ImaginaryI': '\u2148',
'ii': '\u2148',
'Int': '\u222C',
'Integral': '\u222B',
'int': '\u222B',
'Intersection': '\u22C2',
'bigcap': '\u22C2',
'xcap': '\u22C2',
'InvisibleComma': '\u2063',
'ic': '\u2063',
'InvisibleTimes': '\u2062',
'it': '\u2062',
'Iogon': '\u012E',
'Iopf': '\uD835\uDD40',
'Iota': '\u0399',
'Iscr': '\u2110',
'imagline': '\u2110',
'Itilde': '\u0128',
'Iukcy': '\u0406',
'Iuml': '\u00CF',
'Jcirc': '\u0134',
'Jcy': '\u0419',
'Jfr': '\uD835\uDD0D',
'Jopf': '\uD835\uDD41',
'Jscr': '\uD835\uDCA5',
'Jsercy': '\u0408',
'Jukcy': '\u0404',
'KHcy': '\u0425',
'KJcy': '\u040C',
'Kappa': '\u039A',
'Kcedil': '\u0136',
'Kcy': '\u041A',
'Kfr': '\uD835\uDD0E',
'Kopf': '\uD835\uDD42',
'Kscr': '\uD835\uDCA6',
'LJcy': '\u0409',
'LT': '\u003C',
'lt': '\u003C',
'Lacute': '\u0139',
'Lambda': '\u039B',
'Lang': '\u27EA',
'Laplacetrf': '\u2112',
'Lscr': '\u2112',
'lagran': '\u2112',
'Larr': '\u219E',
'twoheadleftarrow': '\u219E',
'Lcaron': '\u013D',
'Lcedil': '\u013B',
'Lcy': '\u041B',
'LeftAngleBracket': '\u27E8',
'lang': '\u27E8',
'langle': '\u27E8',
'LeftArrow': '\u2190',
'ShortLeftArrow': '\u2190',
'larr': '\u2190',
'leftarrow': '\u2190',
'slarr': '\u2190',
'LeftArrowBar': '\u21E4',
'larrb': '\u21E4',
'LeftArrowRightArrow': '\u21C6',
'leftrightarrows': '\u21C6',
'lrarr': '\u21C6',
'LeftCeiling': '\u2308',
'lceil': '\u2308',
'LeftDoubleBracket': '\u27E6',
'lobrk': '\u27E6',
'LeftDownTeeVector': '\u2961',
'LeftDownVector': '\u21C3',
'dharl': '\u21C3',
'downharpoonleft': '\u21C3',
'LeftDownVectorBar': '\u2959',
'LeftFloor': '\u230A',
'lfloor': '\u230A',
'LeftRightArrow': '\u2194',
'harr': '\u2194',
'leftrightarrow': '\u2194',
'LeftRightVector': '\u294E',
'LeftTee': '\u22A3',
'dashv': '\u22A3',
'LeftTeeArrow': '\u21A4',
'mapstoleft': '\u21A4',
'LeftTeeVector': '\u295A',
'LeftTriangle': '\u22B2',
'vartriangleleft': '\u22B2',
'vltri': '\u22B2',
'LeftTriangleBar': '\u29CF',
'LeftTriangleEqual': '\u22B4',
'ltrie': '\u22B4',
'trianglelefteq': '\u22B4',
'LeftUpDownVector': '\u2951',
'LeftUpTeeVector': '\u2960',
'LeftUpVector': '\u21BF',
'uharl': '\u21BF',
'upharpoonleft': '\u21BF',
'LeftUpVectorBar': '\u2958',
'LeftVector': '\u21BC',
'leftharpoonup': '\u21BC',
'lharu': '\u21BC',
'LeftVectorBar': '\u2952',
'LessEqualGreater': '\u22DA',
'leg': '\u22DA',
'lesseqgtr': '\u22DA',
'LessFullEqual': '\u2266',
'lE': '\u2266',
'leqq': '\u2266',
'LessGreater': '\u2276',
'lessgtr': '\u2276',
'lg': '\u2276',
'LessLess': '\u2AA1',
'LessSlantEqual': '\u2A7D',
'leqslant': '\u2A7D',
'les': '\u2A7D',
'LessTilde': '\u2272',
'lesssim': '\u2272',
'lsim': '\u2272',
'Lfr': '\uD835\uDD0F',
'Ll': '\u22D8',
'Lleftarrow': '\u21DA',
'lAarr': '\u21DA',
'Lmidot': '\u013F',
'LongLeftArrow': '\u27F5',
'longleftarrow': '\u27F5',
'xlarr': '\u27F5',
'LongLeftRightArrow': '\u27F7',
'longleftrightarrow': '\u27F7',
'xharr': '\u27F7',
'LongRightArrow': '\u27F6',
'longrightarrow': '\u27F6',
'xrarr': '\u27F6',
'Lopf': '\uD835\uDD43',
'LowerLeftArrow': '\u2199',
'swarr': '\u2199',
'swarrow': '\u2199',
'LowerRightArrow': '\u2198',
'searr': '\u2198',
'searrow': '\u2198',
'Lsh': '\u21B0',
'lsh': '\u21B0',
'Lstrok': '\u0141',
'Lt': '\u226A',
'NestedLessLess': '\u226A',
'll': '\u226A',
'Map': '\u2905',
'Mcy': '\u041C',
'MediumSpace': '\u205F',
'Mellintrf': '\u2133',
'Mscr': '\u2133',
'phmmat': '\u2133',
'Mfr': '\uD835\uDD10',
'MinusPlus': '\u2213',
'mnplus': '\u2213',
'mp': '\u2213',
'Mopf': '\uD835\uDD44',
'Mu': '\u039C',
'NJcy': '\u040A',
'Nacute': '\u0143',
'Ncaron': '\u0147',
'Ncedil': '\u0145',
'Ncy': '\u041D',
'NegativeMediumSpace': '\u200B',
'NegativeThickSpace': '\u200B',
'NegativeThinSpace': '\u200B',
'NegativeVeryThinSpace': '\u200B',
'ZeroWidthSpace': '\u200B',
'NewLine': '\u000A',
'Nfr': '\uD835\uDD11',
'NoBreak': '\u2060',
'NonBreakingSpace': '\u00A0',
'nbsp': '\u00A0',
'Nopf': '\u2115',
'naturals': '\u2115',
'Not': '\u2AEC',
'NotCongruent': '\u2262',
'nequiv': '\u2262',
'NotCupCap': '\u226D',
'NotDoubleVerticalBar': '\u2226',
'npar': '\u2226',
'nparallel': '\u2226',
'nshortparallel': '\u2226',
'nspar': '\u2226',
'NotElement': '\u2209',
'notin': '\u2209',
'notinva': '\u2209',
'NotEqual': '\u2260',
'ne': '\u2260',
'NotEqualTilde': '\u2242\u0338',
'nesim': '\u2242\u0338',
'NotExists': '\u2204',
'nexist': '\u2204',
'nexists': '\u2204',
'NotGreater': '\u226F',
'ngt': '\u226F',
'ngtr': '\u226F',
'NotGreaterEqual': '\u2271',
'nge': '\u2271',
'ngeq': '\u2271',
'NotGreaterFullEqual': '\u2267\u0338',
'ngE': '\u2267\u0338',
'ngeqq': '\u2267\u0338',
'NotGreaterGreater': '\u226B\u0338',
'nGtv': '\u226B\u0338',
'NotGreaterLess': '\u2279',
'ntgl': '\u2279',
'NotGreaterSlantEqual': '\u2A7E\u0338',
'ngeqslant': '\u2A7E\u0338',
'nges': '\u2A7E\u0338',
'NotGreaterTilde': '\u2275',
'ngsim': '\u2275',
'NotHumpDownHump': '\u224E\u0338',
'nbump': '\u224E\u0338',
'NotHumpEqual': '\u224F\u0338',
'nbumpe': '\u224F\u0338',
'NotLeftTriangle': '\u22EA',
'nltri': '\u22EA',
'ntriangleleft': '\u22EA',
'NotLeftTriangleBar': '\u29CF\u0338',
'NotLeftTriangleEqual': '\u22EC',
'nltrie': '\u22EC',
'ntrianglelefteq': '\u22EC',
'NotLess': '\u226E',
'nless': '\u226E',
'nlt': '\u226E',
'NotLessEqual': '\u2270',
'nle': '\u2270',
'nleq': '\u2270',
'NotLessGreater': '\u2278',
'ntlg': '\u2278',
'NotLessLess': '\u226A\u0338',
'nLtv': '\u226A\u0338',
'NotLessSlantEqual': '\u2A7D\u0338',
'nleqslant': '\u2A7D\u0338',
'nles': '\u2A7D\u0338',
'NotLessTilde': '\u2274',
'nlsim': '\u2274',
'NotNestedGreaterGreater': '\u2AA2\u0338',
'NotNestedLessLess': '\u2AA1\u0338',
'NotPrecedes': '\u2280',
'npr': '\u2280',
'nprec': '\u2280',
'NotPrecedesEqual': '\u2AAF\u0338',
'npre': '\u2AAF\u0338',
'npreceq': '\u2AAF\u0338',
'NotPrecedesSlantEqual': '\u22E0',
'nprcue': '\u22E0',
'NotReverseElement': '\u220C',
'notni': '\u220C',
'notniva': '\u220C',
'NotRightTriangle': '\u22EB',
'nrtri': '\u22EB',
'ntriangleright': '\u22EB',
'NotRightTriangleBar': '\u29D0\u0338',
'NotRightTriangleEqual': '\u22ED',
'nrtrie': '\u22ED',
'ntrianglerighteq': '\u22ED',
'NotSquareSubset': '\u228F\u0338',
'NotSquareSubsetEqual': '\u22E2',
'nsqsube': '\u22E2',
'NotSquareSuperset': '\u2290\u0338',
'NotSquareSupersetEqual': '\u22E3',
'nsqsupe': '\u22E3',
'NotSubset': '\u2282\u20D2',
'nsubset': '\u2282\u20D2',
'vnsub': '\u2282\u20D2',
'NotSubsetEqual': '\u2288',
'nsube': '\u2288',
'nsubseteq': '\u2288',
'NotSucceeds': '\u2281',
'nsc': '\u2281',
'nsucc': '\u2281',
'NotSucceedsEqual': '\u2AB0\u0338',
'nsce': '\u2AB0\u0338',
'nsucceq': '\u2AB0\u0338',
'NotSucceedsSlantEqual': '\u22E1',
'nsccue': '\u22E1',
'NotSucceedsTilde': '\u227F\u0338',
'NotSuperset': '\u2283\u20D2',
'nsupset': '\u2283\u20D2',
'vnsup': '\u2283\u20D2',
'NotSupersetEqual': '\u2289',
'nsupe': '\u2289',
'nsupseteq': '\u2289',
'NotTilde': '\u2241',
'nsim': '\u2241',
'NotTildeEqual': '\u2244',
'nsime': '\u2244',
'nsimeq': '\u2244',
'NotTildeFullEqual': '\u2247',
'ncong': '\u2247',
'NotTildeTilde': '\u2249',
'nap': '\u2249',
'napprox': '\u2249',
'NotVerticalBar': '\u2224',
'nmid': '\u2224',
'nshortmid': '\u2224',
'nsmid': '\u2224',
'Nscr': '\uD835\uDCA9',
'Ntilde': '\u00D1',
'Nu': '\u039D',
'OElig': '\u0152',
'Oacute': '\u00D3',
'Ocirc': '\u00D4',
'Ocy': '\u041E',
'Odblac': '\u0150',
'Ofr': '\uD835\uDD12',
'Ograve': '\u00D2',
'Omacr': '\u014C',
'Omega': '\u03A9',
'ohm': '\u03A9',
'Omicron': '\u039F',
'Oopf': '\uD835\uDD46',
'OpenCurlyDoubleQuote': '\u201C',
'ldquo': '\u201C',
'OpenCurlyQuote': '\u2018',
'lsquo': '\u2018',
'Or': '\u2A54',
'Oscr': '\uD835\uDCAA',
'Oslash': '\u00D8',
'Otilde': '\u00D5',
'Otimes': '\u2A37',
'Ouml': '\u00D6',
'OverBar': '\u203E',
'oline': '\u203E',
'OverBrace': '\u23DE',
'OverBracket': '\u23B4',
'tbrk': '\u23B4',
'OverParenthesis': '\u23DC',
'PartialD': '\u2202',
'part': '\u2202',
'Pcy': '\u041F',
'Pfr': '\uD835\uDD13',
'Phi': '\u03A6',
'Pi': '\u03A0',
'PlusMinus': '\u00B1',
'plusmn': '\u00B1',
'pm': '\u00B1',
'Popf': '\u2119',
'primes': '\u2119',
'Pr': '\u2ABB',
'Precedes': '\u227A',
'pr': '\u227A',
'prec': '\u227A',
'PrecedesEqual': '\u2AAF',
'pre': '\u2AAF',
'preceq': '\u2AAF',
'PrecedesSlantEqual': '\u227C',
'prcue': '\u227C',
'preccurlyeq': '\u227C',
'PrecedesTilde': '\u227E',
'precsim': '\u227E',
'prsim': '\u227E',
'Prime': '\u2033',
'Product': '\u220F',
'prod': '\u220F',
'Proportional': '\u221D',
'prop': '\u221D',
'propto': '\u221D',
'varpropto': '\u221D',
'vprop': '\u221D',
'Pscr': '\uD835\uDCAB',
'Psi': '\u03A8',
'QUOT': '\u0022',
'quot': '\u0022',
'Qfr': '\uD835\uDD14',
'Qopf': '\u211A',
'rationals': '\u211A',
'Qscr': '\uD835\uDCAC',
'RBarr': '\u2910',
'drbkarow': '\u2910',
'REG': '\u00AE',
'circledR': '\u00AE',
'reg': '\u00AE',
'Racute': '\u0154',
'Rang': '\u27EB',
'Rarr': '\u21A0',
'twoheadrightarrow': '\u21A0',
'Rarrtl': '\u2916',
'Rcaron': '\u0158',
'Rcedil': '\u0156',
'Rcy': '\u0420',
'Re': '\u211C',
'Rfr': '\u211C',
'real': '\u211C',
'realpart': '\u211C',
'ReverseElement': '\u220B',
'SuchThat': '\u220B',
'ni': '\u220B',
'niv': '\u220B',
'ReverseEquilibrium': '\u21CB',
'leftrightharpoons': '\u21CB',
'lrhar': '\u21CB',
'ReverseUpEquilibrium': '\u296F',
'duhar': '\u296F',
'Rho': '\u03A1',
'RightAngleBracket': '\u27E9',
'rang': '\u27E9',
'rangle': '\u27E9',
'RightArrow': '\u2192',
'ShortRightArrow': '\u2192',
'rarr': '\u2192',
'rightarrow': '\u2192',
'srarr': '\u2192',
'RightArrowBar': '\u21E5',
'rarrb': '\u21E5',
'RightArrowLeftArrow': '\u21C4',
'rightleftarrows': '\u21C4',
'rlarr': '\u21C4',
'RightCeiling': '\u2309',
'rceil': '\u2309',
'RightDoubleBracket': '\u27E7',
'robrk': '\u27E7',
'RightDownTeeVector': '\u295D',
'RightDownVector': '\u21C2',
'dharr': '\u21C2',
'downharpoonright': '\u21C2',
'RightDownVectorBar': '\u2955',
'RightFloor': '\u230B',
'rfloor': '\u230B',
'RightTee': '\u22A2',
'vdash': '\u22A2',
'RightTeeArrow': '\u21A6',
'map': '\u21A6',
'mapsto': '\u21A6',
'RightTeeVector': '\u295B',
'RightTriangle': '\u22B3',
'vartriangleright': '\u22B3',
'vrtri': '\u22B3',
'RightTriangleBar': '\u29D0',
'RightTriangleEqual': '\u22B5',
'rtrie': '\u22B5',
'trianglerighteq': '\u22B5',
'RightUpDownVector': '\u294F',
'RightUpTeeVector': '\u295C',
'RightUpVector': '\u21BE',
'uharr': '\u21BE',
'upharpoonright': '\u21BE',
'RightUpVectorBar': '\u2954',
'RightVector': '\u21C0',
'rharu': '\u21C0',
'rightharpoonup': '\u21C0',
'RightVectorBar': '\u2953',
'Ropf': '\u211D',
'reals': '\u211D',
'RoundImplies': '\u2970',
'Rrightarrow': '\u21DB',
'rAarr': '\u21DB',
'Rscr': '\u211B',
'realine': '\u211B',
'Rsh': '\u21B1',
'rsh': '\u21B1',
'RuleDelayed': '\u29F4',
'SHCHcy': '\u0429',
'SHcy': '\u0428',
'SOFTcy': '\u042C',
'Sacute': '\u015A',
'Sc': '\u2ABC',
'Scaron': '\u0160',
'Scedil': '\u015E',
'Scirc': '\u015C',
'Scy': '\u0421',
'Sfr': '\uD835\uDD16',
'ShortUpArrow': '\u2191',
'UpArrow': '\u2191',
'uarr': '\u2191',
'uparrow': '\u2191',
'Sigma': '\u03A3',
'SmallCircle': '\u2218',
'compfn': '\u2218',
'Sopf': '\uD835\uDD4A',
'Sqrt': '\u221A',
'radic': '\u221A',
'Square': '\u25A1',
'squ': '\u25A1',
'square': '\u25A1',
'SquareIntersection': '\u2293',
'sqcap': '\u2293',
'SquareSubset': '\u228F',
'sqsub': '\u228F',
'sqsubset': '\u228F',
'SquareSubsetEqual': '\u2291',
'sqsube': '\u2291',
'sqsubseteq': '\u2291',
'SquareSuperset': '\u2290',
'sqsup': '\u2290',
'sqsupset': '\u2290',
'SquareSupersetEqual': '\u2292',
'sqsupe': '\u2292',
'sqsupseteq': '\u2292',
'SquareUnion': '\u2294',
'sqcup': '\u2294',
'Sscr': '\uD835\uDCAE',
'Star': '\u22C6',
'sstarf': '\u22C6',
'Sub': '\u22D0',
'Subset': '\u22D0',
'SubsetEqual': '\u2286',
'sube': '\u2286',
'subseteq': '\u2286',
'Succeeds': '\u227B',
'sc': '\u227B',
'succ': '\u227B',
'SucceedsEqual': '\u2AB0',
'sce': '\u2AB0',
'succeq': '\u2AB0',
'SucceedsSlantEqual': '\u227D',
'sccue': '\u227D',
'succcurlyeq': '\u227D',
'SucceedsTilde': '\u227F',
'scsim': '\u227F',
'succsim': '\u227F',
'Sum': '\u2211',
'sum': '\u2211',
'Sup': '\u22D1',
'Supset': '\u22D1',
'Superset': '\u2283',
'sup': '\u2283',
'supset': '\u2283',
'SupersetEqual': '\u2287',
'supe': '\u2287',
'supseteq': '\u2287',
'THORN': '\u00DE',
'TRADE': '\u2122',
'trade': '\u2122',
'TSHcy': '\u040B',
'TScy': '\u0426',
'Tab': '\u0009',
'Tau': '\u03A4',
'Tcaron': '\u0164',
'Tcedil': '\u0162',
'Tcy': '\u0422',
'Tfr': '\uD835\uDD17',
'Therefore': '\u2234',
'there4': '\u2234',
'therefore': '\u2234',
'Theta': '\u0398',
'ThickSpace': '\u205F\u200A',
'ThinSpace': '\u2009',
'thinsp': '\u2009',
'Tilde': '\u223C',
'sim': '\u223C',
'thicksim': '\u223C',
'thksim': '\u223C',
'TildeEqual': '\u2243',
'sime': '\u2243',
'simeq': '\u2243',
'TildeFullEqual': '\u2245',
'cong': '\u2245',
'TildeTilde': '\u2248',
'ap': '\u2248',
'approx': '\u2248',
'asymp': '\u2248',
'thickapprox': '\u2248',
'thkap': '\u2248',
'Topf': '\uD835\uDD4B',
'TripleDot': '\u20DB',
'tdot': '\u20DB',
'Tscr': '\uD835\uDCAF',
'Tstrok': '\u0166',
'Uacute': '\u00DA',
'Uarr': '\u219F',
'Uarrocir': '\u2949',
'Ubrcy': '\u040E',
'Ubreve': '\u016C',
'Ucirc': '\u00DB',
'Ucy': '\u0423',
'Udblac': '\u0170',
'Ufr': '\uD835\uDD18',
'Ugrave': '\u00D9',
'Umacr': '\u016A',
'UnderBar': '\u005F',
'lowbar': '\u005F',
'UnderBrace': '\u23DF',
'UnderBracket': '\u23B5',
'bbrk': '\u23B5',
'UnderParenthesis': '\u23DD',
'Union': '\u22C3',
'bigcup': '\u22C3',
'xcup': '\u22C3',
'UnionPlus': '\u228E',
'uplus': '\u228E',
'Uogon': '\u0172',
'Uopf': '\uD835\uDD4C',
'UpArrowBar': '\u2912',
'UpArrowDownArrow': '\u21C5',
'udarr': '\u21C5',
'UpDownArrow': '\u2195',
'updownarrow': '\u2195',
'varr': '\u2195',
'UpEquilibrium': '\u296E',
'udhar': '\u296E',
'UpTee': '\u22A5',
'bot': '\u22A5',
'bottom': '\u22A5',
'perp': '\u22A5',
'UpTeeArrow': '\u21A5',
'mapstoup': '\u21A5',
'UpperLeftArrow': '\u2196',
'nwarr': '\u2196',
'nwarrow': '\u2196',
'UpperRightArrow': '\u2197',
'nearr': '\u2197',
'nearrow': '\u2197',
'Upsi': '\u03D2',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'Uring': '\u016E',
'Uscr': '\uD835\uDCB0',
'Utilde': '\u0168',
'Uuml': '\u00DC',
'VDash': '\u22AB',
'Vbar': '\u2AEB',
'Vcy': '\u0412',
'Vdash': '\u22A9',
'Vdashl': '\u2AE6',
'Vee': '\u22C1',
'bigvee': '\u22C1',
'xvee': '\u22C1',
'Verbar': '\u2016',
'Vert': '\u2016',
'VerticalBar': '\u2223',
'mid': '\u2223',
'shortmid': '\u2223',
'smid': '\u2223',
'VerticalLine': '\u007C',
'verbar': '\u007C',
'vert': '\u007C',
'VerticalSeparator': '\u2758',
'VerticalTilde': '\u2240',
'wr': '\u2240',
'wreath': '\u2240',
'VeryThinSpace': '\u200A',
'hairsp': '\u200A',
'Vfr': '\uD835\uDD19',
'Vopf': '\uD835\uDD4D',
'Vscr': '\uD835\uDCB1',
'Vvdash': '\u22AA',
'Wcirc': '\u0174',
'Wedge': '\u22C0',
'bigwedge': '\u22C0',
'xwedge': '\u22C0',
'Wfr': '\uD835\uDD1A',
'Wopf': '\uD835\uDD4E',
'Wscr': '\uD835\uDCB2',
'Xfr': '\uD835\uDD1B',
'Xi': '\u039E',
'Xopf': '\uD835\uDD4F',
'Xscr': '\uD835\uDCB3',
'YAcy': '\u042F',
'YIcy': '\u0407',
'YUcy': '\u042E',
'Yacute': '\u00DD',
'Ycirc': '\u0176',
'Ycy': '\u042B',
'Yfr': '\uD835\uDD1C',
'Yopf': '\uD835\uDD50',
'Yscr': '\uD835\uDCB4',
'Yuml': '\u0178',
'ZHcy': '\u0416',
'Zacute': '\u0179',
'Zcaron': '\u017D',
'Zcy': '\u0417',
'Zdot': '\u017B',
'Zeta': '\u0396',
'Zfr': '\u2128',
'zeetrf': '\u2128',
'Zopf': '\u2124',
'integers': '\u2124',
'Zscr': '\uD835\uDCB5',
'aacute': '\u00E1',
'abreve': '\u0103',
'ac': '\u223E',
'mstpos': '\u223E',
'acE': '\u223E\u0333',
'acd': '\u223F',
'acirc': '\u00E2',
'acy': '\u0430',
'aelig': '\u00E6',
'afr': '\uD835\uDD1E',
'agrave': '\u00E0',
'alefsym': '\u2135',
'aleph': '\u2135',
'alpha': '\u03B1',
'amacr': '\u0101',
'amalg': '\u2A3F',
'and': '\u2227',
'wedge': '\u2227',
'andand': '\u2A55',
'andd': '\u2A5C',
'andslope': '\u2A58',
'andv': '\u2A5A',
'ang': '\u2220',
'angle': '\u2220',
'ange': '\u29A4',
'angmsd': '\u2221',
'measuredangle': '\u2221',
'angmsdaa': '\u29A8',
'angmsdab': '\u29A9',
'angmsdac': '\u29AA',
'angmsdad': '\u29AB',
'angmsdae': '\u29AC',
'angmsdaf': '\u29AD',
'angmsdag': '\u29AE',
'angmsdah': '\u29AF',
'angrt': '\u221F',
'angrtvb': '\u22BE',
'angrtvbd': '\u299D',
'angsph': '\u2222',
'angzarr': '\u237C',
'aogon': '\u0105',
'aopf': '\uD835\uDD52',
'apE': '\u2A70',
'apacir': '\u2A6F',
'ape': '\u224A',
'approxeq': '\u224A',
'apid': '\u224B',
'apos': '\u0027',
'aring': '\u00E5',
'ascr': '\uD835\uDCB6',
'ast': '\u002A',
'midast': '\u002A',
'atilde': '\u00E3',
'auml': '\u00E4',
'awint': '\u2A11',
'bNot': '\u2AED',
'backcong': '\u224C',
'bcong': '\u224C',
'backepsilon': '\u03F6',
'bepsi': '\u03F6',
'backprime': '\u2035',
'bprime': '\u2035',
'backsim': '\u223D',
'bsim': '\u223D',
'backsimeq': '\u22CD',
'bsime': '\u22CD',
'barvee': '\u22BD',
'barwed': '\u2305',
'barwedge': '\u2305',
'bbrktbrk': '\u23B6',
'bcy': '\u0431',
'bdquo': '\u201E',
'ldquor': '\u201E',
'bemptyv': '\u29B0',
'beta': '\u03B2',
'beth': '\u2136',
'between': '\u226C',
'twixt': '\u226C',
'bfr': '\uD835\uDD1F',
'bigcirc': '\u25EF',
'xcirc': '\u25EF',
'bigodot': '\u2A00',
'xodot': '\u2A00',
'bigoplus': '\u2A01',
'xoplus': '\u2A01',
'bigotimes': '\u2A02',
'xotime': '\u2A02',
'bigsqcup': '\u2A06',
'xsqcup': '\u2A06',
'bigstar': '\u2605',
'starf': '\u2605',
'bigtriangledown': '\u25BD',
'xdtri': '\u25BD',
'bigtriangleup': '\u25B3',
'xutri': '\u25B3',
'biguplus': '\u2A04',
'xuplus': '\u2A04',
'bkarow': '\u290D',
'rbarr': '\u290D',
'blacklozenge': '\u29EB',
'lozf': '\u29EB',
'blacktriangle': '\u25B4',
'utrif': '\u25B4',
'blacktriangledown': '\u25BE',
'dtrif': '\u25BE',
'blacktriangleleft': '\u25C2',
'ltrif': '\u25C2',
'blacktriangleright': '\u25B8',
'rtrif': '\u25B8',
'blank': '\u2423',
'blk12': '\u2592',
'blk14': '\u2591',
'blk34': '\u2593',
'block': '\u2588',
'bne': '\u003D\u20E5',
'bnequiv': '\u2261\u20E5',
'bnot': '\u2310',
'bopf': '\uD835\uDD53',
'bowtie': '\u22C8',
'boxDL': '\u2557',
'boxDR': '\u2554',
'boxDl': '\u2556',
'boxDr': '\u2553',
'boxH': '\u2550',
'boxHD': '\u2566',
'boxHU': '\u2569',
'boxHd': '\u2564',
'boxHu': '\u2567',
'boxUL': '\u255D',
'boxUR': '\u255A',
'boxUl': '\u255C',
'boxUr': '\u2559',
'boxV': '\u2551',
'boxVH': '\u256C',
'boxVL': '\u2563',
'boxVR': '\u2560',
'boxVh': '\u256B',
'boxVl': '\u2562',
'boxVr': '\u255F',
'boxbox': '\u29C9',
'boxdL': '\u2555',
'boxdR': '\u2552',
'boxdl': '\u2510',
'boxdr': '\u250C',
'boxhD': '\u2565',
'boxhU': '\u2568',
'boxhd': '\u252C',
'boxhu': '\u2534',
'boxminus': '\u229F',
'minusb': '\u229F',
'boxplus': '\u229E',
'plusb': '\u229E',
'boxtimes': '\u22A0',
'timesb': '\u22A0',
'boxuL': '\u255B',
'boxuR': '\u2558',
'boxul': '\u2518',
'boxur': '\u2514',
'boxv': '\u2502',
'boxvH': '\u256A',
'boxvL': '\u2561',
'boxvR': '\u255E',
'boxvh': '\u253C',
'boxvl': '\u2524',
'boxvr': '\u251C',
'brvbar': '\u00A6',
'bscr': '\uD835\uDCB7',
'bsemi': '\u204F',
'bsol': '\u005C',
'bsolb': '\u29C5',
'bsolhsub': '\u27C8',
'bull': '\u2022',
'bullet': '\u2022',
'bumpE': '\u2AAE',
'cacute': '\u0107',
'cap': '\u2229',
'capand': '\u2A44',
'capbrcup': '\u2A49',
'capcap': '\u2A4B',
'capcup': '\u2A47',
'capdot': '\u2A40',
'caps': '\u2229\uFE00',
'caret': '\u2041',
'ccaps': '\u2A4D',
'ccaron': '\u010D',
'ccedil': '\u00E7',
'ccirc': '\u0109',
'ccups': '\u2A4C',
'ccupssm': '\u2A50',
'cdot': '\u010B',
'cemptyv': '\u29B2',
'cent': '\u00A2',
'cfr': '\uD835\uDD20',
'chcy': '\u0447',
'check': '\u2713',
'checkmark': '\u2713',
'chi': '\u03C7',
'cir': '\u25CB',
'cirE': '\u29C3',
'circ': '\u02C6',
'circeq': '\u2257',
'cire': '\u2257',
'circlearrowleft': '\u21BA',
'olarr': '\u21BA',
'circlearrowright': '\u21BB',
'orarr': '\u21BB',
'circledS': '\u24C8',
'oS': '\u24C8',
'circledast': '\u229B',
'oast': '\u229B',
'circledcirc': '\u229A',
'ocir': '\u229A',
'circleddash': '\u229D',
'odash': '\u229D',
'cirfnint': '\u2A10',
'cirmid': '\u2AEF',
'cirscir': '\u29C2',
'clubs': '\u2663',
'clubsuit': '\u2663',
'colon': '\u003A',
'comma': '\u002C',
'commat': '\u0040',
'comp': '\u2201',
'complement': '\u2201',
'congdot': '\u2A6D',
'copf': '\uD835\uDD54',
'copysr': '\u2117',
'crarr': '\u21B5',
'cross': '\u2717',
'cscr': '\uD835\uDCB8',
'csub': '\u2ACF',
'csube': '\u2AD1',
'csup': '\u2AD0',
'csupe': '\u2AD2',
'ctdot': '\u22EF',
'cudarrl': '\u2938',
'cudarrr': '\u2935',
'cuepr': '\u22DE',
'curlyeqprec': '\u22DE',
'cuesc': '\u22DF',
'curlyeqsucc': '\u22DF',
'cularr': '\u21B6',
'curvearrowleft': '\u21B6',
'cularrp': '\u293D',
'cup': '\u222A',
'cupbrcap': '\u2A48',
'cupcap': '\u2A46',
'cupcup': '\u2A4A',
'cupdot': '\u228D',
'cupor': '\u2A45',
'cups': '\u222A\uFE00',
'curarr': '\u21B7',
'curvearrowright': '\u21B7',
'curarrm': '\u293C',
'curlyvee': '\u22CE',
'cuvee': '\u22CE',
'curlywedge': '\u22CF',
'cuwed': '\u22CF',
'curren': '\u00A4',
'cwint': '\u2231',
'cylcty': '\u232D',
'dHar': '\u2965',
'dagger': '\u2020',
'daleth': '\u2138',
'dash': '\u2010',
'hyphen': '\u2010',
'dbkarow': '\u290F',
'rBarr': '\u290F',
'dcaron': '\u010F',
'dcy': '\u0434',
'ddarr': '\u21CA',
'downdownarrows': '\u21CA',
'ddotseq': '\u2A77',
'eDDot': '\u2A77',
'deg': '\u00B0',
'delta': '\u03B4',
'demptyv': '\u29B1',
'dfisht': '\u297F',
'dfr': '\uD835\uDD21',
'diamondsuit': '\u2666',
'diams': '\u2666',
'digamma': '\u03DD',
'gammad': '\u03DD',
'disin': '\u22F2',
'div': '\u00F7',
'divide': '\u00F7',
'divideontimes': '\u22C7',
'divonx': '\u22C7',
'djcy': '\u0452',
'dlcorn': '\u231E',
'llcorner': '\u231E',
'dlcrop': '\u230D',
'dollar': '\u0024',
'dopf': '\uD835\uDD55',
'doteqdot': '\u2251',
'eDot': '\u2251',
'dotminus': '\u2238',
'minusd': '\u2238',
'dotplus': '\u2214',
'plusdo': '\u2214',
'dotsquare': '\u22A1',
'sdotb': '\u22A1',
'drcorn': '\u231F',
'lrcorner': '\u231F',
'drcrop': '\u230C',
'dscr': '\uD835\uDCB9',
'dscy': '\u0455',
'dsol': '\u29F6',
'dstrok': '\u0111',
'dtdot': '\u22F1',
'dtri': '\u25BF',
'triangledown': '\u25BF',
'dwangle': '\u29A6',
'dzcy': '\u045F',
'dzigrarr': '\u27FF',
'eacute': '\u00E9',
'easter': '\u2A6E',
'ecaron': '\u011B',
'ecir': '\u2256',
'eqcirc': '\u2256',
'ecirc': '\u00EA',
'ecolon': '\u2255',
'eqcolon': '\u2255',
'ecy': '\u044D',
'edot': '\u0117',
'efDot': '\u2252',
'fallingdotseq': '\u2252',
'efr': '\uD835\uDD22',
'eg': '\u2A9A',
'egrave': '\u00E8',
'egs': '\u2A96',
'eqslantgtr': '\u2A96',
'egsdot': '\u2A98',
'el': '\u2A99',
'elinters': '\u23E7',
'ell': '\u2113',
'els': '\u2A95',
'eqslantless': '\u2A95',
'elsdot': '\u2A97',
'emacr': '\u0113',
'empty': '\u2205',
'emptyset': '\u2205',
'emptyv': '\u2205',
'varnothing': '\u2205',
'emsp13': '\u2004',
'emsp14': '\u2005',
'emsp': '\u2003',
'eng': '\u014B',
'ensp': '\u2002',
'eogon': '\u0119',
'eopf': '\uD835\uDD56',
'epar': '\u22D5',
'eparsl': '\u29E3',
'eplus': '\u2A71',
'epsi': '\u03B5',
'epsilon': '\u03B5',
'epsiv': '\u03F5',
'straightepsilon': '\u03F5',
'varepsilon': '\u03F5',
'equals': '\u003D',
'equest': '\u225F',
'questeq': '\u225F',
'equivDD': '\u2A78',
'eqvparsl': '\u29E5',
'erDot': '\u2253',
'risingdotseq': '\u2253',
'erarr': '\u2971',
'escr': '\u212F',
'eta': '\u03B7',
'eth': '\u00F0',
'euml': '\u00EB',
'euro': '\u20AC',
'excl': '\u0021',
'fcy': '\u0444',
'female': '\u2640',
'ffilig': '\uFB03',
'fflig': '\uFB00',
'ffllig': '\uFB04',
'ffr': '\uD835\uDD23',
'filig': '\uFB01',
'fjlig': '\u0066\u006A',
'flat': '\u266D',
'fllig': '\uFB02',
'fltns': '\u25B1',
'fnof': '\u0192',
'fopf': '\uD835\uDD57',
'fork': '\u22D4',
'pitchfork': '\u22D4',
'forkv': '\u2AD9',
'fpartint': '\u2A0D',
'frac12': '\u00BD',
'half': '\u00BD',
'frac13': '\u2153',
'frac14': '\u00BC',
'frac15': '\u2155',
'frac16': '\u2159',
'frac18': '\u215B',
'frac23': '\u2154',
'frac25': '\u2156',
'frac34': '\u00BE',
'frac35': '\u2157',
'frac38': '\u215C',
'frac45': '\u2158',
'frac56': '\u215A',
'frac58': '\u215D',
'frac78': '\u215E',
'frasl': '\u2044',
'frown': '\u2322',
'sfrown': '\u2322',
'fscr': '\uD835\uDCBB',
'gEl': '\u2A8C',
'gtreqqless': '\u2A8C',
'gacute': '\u01F5',
'gamma': '\u03B3',
'gap': '\u2A86',
'gtrapprox': '\u2A86',
'gbreve': '\u011F',
'gcirc': '\u011D',
'gcy': '\u0433',
'gdot': '\u0121',
'gescc': '\u2AA9',
'gesdot': '\u2A80',
'gesdoto': '\u2A82',
'gesdotol': '\u2A84',
'gesl': '\u22DB\uFE00',
'gesles': '\u2A94',
'gfr': '\uD835\uDD24',
'gimel': '\u2137',
'gjcy': '\u0453',
'glE': '\u2A92',
'gla': '\u2AA5',
'glj': '\u2AA4',
'gnE': '\u2269',
'gneqq': '\u2269',
'gnap': '\u2A8A',
'gnapprox': '\u2A8A',
'gne': '\u2A88',
'gneq': '\u2A88',
'gnsim': '\u22E7',
'gopf': '\uD835\uDD58',
'gscr': '\u210A',
'gsime': '\u2A8E',
'gsiml': '\u2A90',
'gtcc': '\u2AA7',
'gtcir': '\u2A7A',
'gtdot': '\u22D7',
'gtrdot': '\u22D7',
'gtlPar': '\u2995',
'gtquest': '\u2A7C',
'gtrarr': '\u2978',
'gvertneqq': '\u2269\uFE00',
'gvnE': '\u2269\uFE00',
'hardcy': '\u044A',
'harrcir': '\u2948',
'harrw': '\u21AD',
'leftrightsquigarrow': '\u21AD',
'hbar': '\u210F',
'hslash': '\u210F',
'planck': '\u210F',
'plankv': '\u210F',
'hcirc': '\u0125',
'hearts': '\u2665',
'heartsuit': '\u2665',
'hellip': '\u2026',
'mldr': '\u2026',
'hercon': '\u22B9',
'hfr': '\uD835\uDD25',
'hksearow': '\u2925',
'searhk': '\u2925',
'hkswarow': '\u2926',
'swarhk': '\u2926',
'hoarr': '\u21FF',
'homtht': '\u223B',
'hookleftarrow': '\u21A9',
'larrhk': '\u21A9',
'hookrightarrow': '\u21AA',
'rarrhk': '\u21AA',
'hopf': '\uD835\uDD59',
'horbar': '\u2015',
'hscr': '\uD835\uDCBD',
'hstrok': '\u0127',
'hybull': '\u2043',
'iacute': '\u00ED',
'icirc': '\u00EE',
'icy': '\u0438',
'iecy': '\u0435',
'iexcl': '\u00A1',
'ifr': '\uD835\uDD26',
'igrave': '\u00EC',
'iiiint': '\u2A0C',
'qint': '\u2A0C',
'iiint': '\u222D',
'tint': '\u222D',
'iinfin': '\u29DC',
'iiota': '\u2129',
'ijlig': '\u0133',
'imacr': '\u012B',
'imath': '\u0131',
'inodot': '\u0131',
'imof': '\u22B7',
'imped': '\u01B5',
'incare': '\u2105',
'infin': '\u221E',
'infintie': '\u29DD',
'intcal': '\u22BA',
'intercal': '\u22BA',
'intlarhk': '\u2A17',
'intprod': '\u2A3C',
'iprod': '\u2A3C',
'iocy': '\u0451',
'iogon': '\u012F',
'iopf': '\uD835\uDD5A',
'iota': '\u03B9',
'iquest': '\u00BF',
'iscr': '\uD835\uDCBE',
'isinE': '\u22F9',
'isindot': '\u22F5',
'isins': '\u22F4',
'isinsv': '\u22F3',
'itilde': '\u0129',
'iukcy': '\u0456',
'iuml': '\u00EF',
'jcirc': '\u0135',
'jcy': '\u0439',
'jfr': '\uD835\uDD27',
'jmath': '\u0237',
'jopf': '\uD835\uDD5B',
'jscr': '\uD835\uDCBF',
'jsercy': '\u0458',
'jukcy': '\u0454',
'kappa': '\u03BA',
'kappav': '\u03F0',
'varkappa': '\u03F0',
'kcedil': '\u0137',
'kcy': '\u043A',
'kfr': '\uD835\uDD28',
'kgreen': '\u0138',
'khcy': '\u0445',
'kjcy': '\u045C',
'kopf': '\uD835\uDD5C',
'kscr': '\uD835\uDCC0',
'lAtail': '\u291B',
'lBarr': '\u290E',
'lEg': '\u2A8B',
'lesseqqgtr': '\u2A8B',
'lHar': '\u2962',
'lacute': '\u013A',
'laemptyv': '\u29B4',
'lambda': '\u03BB',
'langd': '\u2991',
'lap': '\u2A85',
'lessapprox': '\u2A85',
'laquo': '\u00AB',
'larrbfs': '\u291F',
'larrfs': '\u291D',
'larrlp': '\u21AB',
'looparrowleft': '\u21AB',
'larrpl': '\u2939',
'larrsim': '\u2973',
'larrtl': '\u21A2',
'leftarrowtail': '\u21A2',
'lat': '\u2AAB',
'latail': '\u2919',
'late': '\u2AAD',
'lates': '\u2AAD\uFE00',
'lbarr': '\u290C',
'lbbrk': '\u2772',
'lbrace': '\u007B',
'lcub': '\u007B',
'lbrack': '\u005B',
'lsqb': '\u005B',
'lbrke': '\u298B',
'lbrksld': '\u298F',
'lbrkslu': '\u298D',
'lcaron': '\u013E',
'lcedil': '\u013C',
'lcy': '\u043B',
'ldca': '\u2936',
'ldrdhar': '\u2967',
'ldrushar': '\u294B',
'ldsh': '\u21B2',
'le': '\u2264',
'leq': '\u2264',
'leftleftarrows': '\u21C7',
'llarr': '\u21C7',
'leftthreetimes': '\u22CB',
'lthree': '\u22CB',
'lescc': '\u2AA8',
'lesdot': '\u2A7F',
'lesdoto': '\u2A81',
'lesdotor': '\u2A83',
'lesg': '\u22DA\uFE00',
'lesges': '\u2A93',
'lessdot': '\u22D6',
'ltdot': '\u22D6',
'lfisht': '\u297C',
'lfr': '\uD835\uDD29',
'lgE': '\u2A91',
'lharul': '\u296A',
'lhblk': '\u2584',
'ljcy': '\u0459',
'llhard': '\u296B',
'lltri': '\u25FA',
'lmidot': '\u0140',
'lmoust': '\u23B0',
'lmoustache': '\u23B0',
'lnE': '\u2268',
'lneqq': '\u2268',
'lnap': '\u2A89',
'lnapprox': '\u2A89',
'lne': '\u2A87',
'lneq': '\u2A87',
'lnsim': '\u22E6',
'loang': '\u27EC',
'loarr': '\u21FD',
'longmapsto': '\u27FC',
'xmap': '\u27FC',
'looparrowright': '\u21AC',
'rarrlp': '\u21AC',
'lopar': '\u2985',
'lopf': '\uD835\uDD5D',
'loplus': '\u2A2D',
'lotimes': '\u2A34',
'lowast': '\u2217',
'loz': '\u25CA',
'lozenge': '\u25CA',
'lpar': '\u0028',
'lparlt': '\u2993',
'lrhard': '\u296D',
'lrm': '\u200E',
'lrtri': '\u22BF',
'lsaquo': '\u2039',
'lscr': '\uD835\uDCC1',
'lsime': '\u2A8D',
'lsimg': '\u2A8F',
'lsquor': '\u201A',
'sbquo': '\u201A',
'lstrok': '\u0142',
'ltcc': '\u2AA6',
'ltcir': '\u2A79',
'ltimes': '\u22C9',
'ltlarr': '\u2976',
'ltquest': '\u2A7B',
'ltrPar': '\u2996',
'ltri': '\u25C3',
'triangleleft': '\u25C3',
'lurdshar': '\u294A',
'luruhar': '\u2966',
'lvertneqq': '\u2268\uFE00',
'lvnE': '\u2268\uFE00',
'mDDot': '\u223A',
'macr': '\u00AF',
'strns': '\u00AF',
'male': '\u2642',
'malt': '\u2720',
'maltese': '\u2720',
'marker': '\u25AE',
'mcomma': '\u2A29',
'mcy': '\u043C',
'mdash': '\u2014',
'mfr': '\uD835\uDD2A',
'mho': '\u2127',
'micro': '\u00B5',
'midcir': '\u2AF0',
'minus': '\u2212',
'minusdu': '\u2A2A',
'mlcp': '\u2ADB',
'models': '\u22A7',
'mopf': '\uD835\uDD5E',
'mscr': '\uD835\uDCC2',
'mu': '\u03BC',
'multimap': '\u22B8',
'mumap': '\u22B8',
'nGg': '\u22D9\u0338',
'nGt': '\u226B\u20D2',
'nLeftarrow': '\u21CD',
'nlArr': '\u21CD',
'nLeftrightarrow': '\u21CE',
'nhArr': '\u21CE',
'nLl': '\u22D8\u0338',
'nLt': '\u226A\u20D2',
'nRightarrow': '\u21CF',
'nrArr': '\u21CF',
'nVDash': '\u22AF',
'nVdash': '\u22AE',
'nacute': '\u0144',
'nang': '\u2220\u20D2',
'napE': '\u2A70\u0338',
'napid': '\u224B\u0338',
'napos': '\u0149',
'natur': '\u266E',
'natural': '\u266E',
'ncap': '\u2A43',
'ncaron': '\u0148',
'ncedil': '\u0146',
'ncongdot': '\u2A6D\u0338',
'ncup': '\u2A42',
'ncy': '\u043D',
'ndash': '\u2013',
'neArr': '\u21D7',
'nearhk': '\u2924',
'nedot': '\u2250\u0338',
'nesear': '\u2928',
'toea': '\u2928',
'nfr': '\uD835\uDD2B',
'nharr': '\u21AE',
'nleftrightarrow': '\u21AE',
'nhpar': '\u2AF2',
'nis': '\u22FC',
'nisd': '\u22FA',
'njcy': '\u045A',
'nlE': '\u2266\u0338',
'nleqq': '\u2266\u0338',
'nlarr': '\u219A',
'nleftarrow': '\u219A',
'nldr': '\u2025',
'nopf': '\uD835\uDD5F',
'not': '\u00AC',
'notinE': '\u22F9\u0338',
'notindot': '\u22F5\u0338',
'notinvb': '\u22F7',
'notinvc': '\u22F6',
'notnivb': '\u22FE',
'notnivc': '\u22FD',
'nparsl': '\u2AFD\u20E5',
'npart': '\u2202\u0338',
'npolint': '\u2A14',
'nrarr': '\u219B',
'nrightarrow': '\u219B',
'nrarrc': '\u2933\u0338',
'nrarrw': '\u219D\u0338',
'nscr': '\uD835\uDCC3',
'nsub': '\u2284',
'nsubE': '\u2AC5\u0338',
'nsubseteqq': '\u2AC5\u0338',
'nsup': '\u2285',
'nsupE': '\u2AC6\u0338',
'nsupseteqq': '\u2AC6\u0338',
'ntilde': '\u00F1',
'nu': '\u03BD',
'num': '\u0023',
'numero': '\u2116',
'numsp': '\u2007',
'nvDash': '\u22AD',
'nvHarr': '\u2904',
'nvap': '\u224D\u20D2',
'nvdash': '\u22AC',
'nvge': '\u2265\u20D2',
'nvgt': '\u003E\u20D2',
'nvinfin': '\u29DE',
'nvlArr': '\u2902',
'nvle': '\u2264\u20D2',
'nvlt': '\u003C\u20D2',
'nvltrie': '\u22B4\u20D2',
'nvrArr': '\u2903',
'nvrtrie': '\u22B5\u20D2',
'nvsim': '\u223C\u20D2',
'nwArr': '\u21D6',
'nwarhk': '\u2923',
'nwnear': '\u2927',
'oacute': '\u00F3',
'ocirc': '\u00F4',
'ocy': '\u043E',
'odblac': '\u0151',
'odiv': '\u2A38',
'odsold': '\u29BC',
'oelig': '\u0153',
'ofcir': '\u29BF',
'ofr': '\uD835\uDD2C',
'ogon': '\u02DB',
'ograve': '\u00F2',
'ogt': '\u29C1',
'ohbar': '\u29B5',
'olcir': '\u29BE',
'olcross': '\u29BB',
'olt': '\u29C0',
'omacr': '\u014D',
'omega': '\u03C9',
'omicron': '\u03BF',
'omid': '\u29B6',
'oopf': '\uD835\uDD60',
'opar': '\u29B7',
'operp': '\u29B9',
'or': '\u2228',
'vee': '\u2228',
'ord': '\u2A5D',
'order': '\u2134',
'orderof': '\u2134',
'oscr': '\u2134',
'ordf': '\u00AA',
'ordm': '\u00BA',
'origof': '\u22B6',
'oror': '\u2A56',
'orslope': '\u2A57',
'orv': '\u2A5B',
'oslash': '\u00F8',
'osol': '\u2298',
'otilde': '\u00F5',
'otimesas': '\u2A36',
'ouml': '\u00F6',
'ovbar': '\u233D',
'para': '\u00B6',
'parsim': '\u2AF3',
'parsl': '\u2AFD',
'pcy': '\u043F',
'percnt': '\u0025',
'period': '\u002E',
'permil': '\u2030',
'pertenk': '\u2031',
'pfr': '\uD835\uDD2D',
'phi': '\u03C6',
'phiv': '\u03D5',
'straightphi': '\u03D5',
'varphi': '\u03D5',
'phone': '\u260E',
'pi': '\u03C0',
'piv': '\u03D6',
'varpi': '\u03D6',
'planckh': '\u210E',
'plus': '\u002B',
'plusacir': '\u2A23',
'pluscir': '\u2A22',
'plusdu': '\u2A25',
'pluse': '\u2A72',
'plussim': '\u2A26',
'plustwo': '\u2A27',
'pointint': '\u2A15',
'popf': '\uD835\uDD61',
'pound': '\u00A3',
'prE': '\u2AB3',
'prap': '\u2AB7',
'precapprox': '\u2AB7',
'precnapprox': '\u2AB9',
'prnap': '\u2AB9',
'precneqq': '\u2AB5',
'prnE': '\u2AB5',
'precnsim': '\u22E8',
'prnsim': '\u22E8',
'prime': '\u2032',
'profalar': '\u232E',
'profline': '\u2312',
'profsurf': '\u2313',
'prurel': '\u22B0',
'pscr': '\uD835\uDCC5',
'psi': '\u03C8',
'puncsp': '\u2008',
'qfr': '\uD835\uDD2E',
'qopf': '\uD835\uDD62',
'qprime': '\u2057',
'qscr': '\uD835\uDCC6',
'quatint': '\u2A16',
'quest': '\u003F',
'rAtail': '\u291C',
'rHar': '\u2964',
'race': '\u223D\u0331',
'racute': '\u0155',
'raemptyv': '\u29B3',
'rangd': '\u2992',
'range': '\u29A5',
'raquo': '\u00BB',
'rarrap': '\u2975',
'rarrbfs': '\u2920',
'rarrc': '\u2933',
'rarrfs': '\u291E',
'rarrpl': '\u2945',
'rarrsim': '\u2974',
'rarrtl': '\u21A3',
'rightarrowtail': '\u21A3',
'rarrw': '\u219D',
'rightsquigarrow': '\u219D',
'ratail': '\u291A',
'ratio': '\u2236',
'rbbrk': '\u2773',
'rbrace': '\u007D',
'rcub': '\u007D',
'rbrack': '\u005D',
'rsqb': '\u005D',
'rbrke': '\u298C',
'rbrksld': '\u298E',
'rbrkslu': '\u2990',
'rcaron': '\u0159',
'rcedil': '\u0157',
'rcy': '\u0440',
'rdca': '\u2937',
'rdldhar': '\u2969',
'rdsh': '\u21B3',
'rect': '\u25AD',
'rfisht': '\u297D',
'rfr': '\uD835\uDD2F',
'rharul': '\u296C',
'rho': '\u03C1',
'rhov': '\u03F1',
'varrho': '\u03F1',
'rightrightarrows': '\u21C9',
'rrarr': '\u21C9',
'rightthreetimes': '\u22CC',
'rthree': '\u22CC',
'ring': '\u02DA',
'rlm': '\u200F',
'rmoust': '\u23B1',
'rmoustache': '\u23B1',
'rnmid': '\u2AEE',
'roang': '\u27ED',
'roarr': '\u21FE',
'ropar': '\u2986',
'ropf': '\uD835\uDD63',
'roplus': '\u2A2E',
'rotimes': '\u2A35',
'rpar': '\u0029',
'rpargt': '\u2994',
'rppolint': '\u2A12',
'rsaquo': '\u203A',
'rscr': '\uD835\uDCC7',
'rtimes': '\u22CA',
'rtri': '\u25B9',
'triangleright': '\u25B9',
'rtriltri': '\u29CE',
'ruluhar': '\u2968',
'rx': '\u211E',
'sacute': '\u015B',
'scE': '\u2AB4',
'scap': '\u2AB8',
'succapprox': '\u2AB8',
'scaron': '\u0161',
'scedil': '\u015F',
'scirc': '\u015D',
'scnE': '\u2AB6',
'succneqq': '\u2AB6',
'scnap': '\u2ABA',
'succnapprox': '\u2ABA',
'scnsim': '\u22E9',
'succnsim': '\u22E9',
'scpolint': '\u2A13',
'scy': '\u0441',
'sdot': '\u22C5',
'sdote': '\u2A66',
'seArr': '\u21D8',
'sect': '\u00A7',
'semi': '\u003B',
'seswar': '\u2929',
'tosa': '\u2929',
'sext': '\u2736',
'sfr': '\uD835\uDD30',
'sharp': '\u266F',
'shchcy': '\u0449',
'shcy': '\u0448',
'shy': '\u00AD',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sigmav': '\u03C2',
'varsigma': '\u03C2',
'simdot': '\u2A6A',
'simg': '\u2A9E',
'simgE': '\u2AA0',
'siml': '\u2A9D',
'simlE': '\u2A9F',
'simne': '\u2246',
'simplus': '\u2A24',
'simrarr': '\u2972',
'smashp': '\u2A33',
'smeparsl': '\u29E4',
'smile': '\u2323',
'ssmile': '\u2323',
'smt': '\u2AAA',
'smte': '\u2AAC',
'smtes': '\u2AAC\uFE00',
'softcy': '\u044C',
'sol': '\u002F',
'solb': '\u29C4',
'solbar': '\u233F',
'sopf': '\uD835\uDD64',
'spades': '\u2660',
'spadesuit': '\u2660',
'sqcaps': '\u2293\uFE00',
'sqcups': '\u2294\uFE00',
'sscr': '\uD835\uDCC8',
'star': '\u2606',
'sub': '\u2282',
'subset': '\u2282',
'subE': '\u2AC5',
'subseteqq': '\u2AC5',
'subdot': '\u2ABD',
'subedot': '\u2AC3',
'submult': '\u2AC1',
'subnE': '\u2ACB',
'subsetneqq': '\u2ACB',
'subne': '\u228A',
'subsetneq': '\u228A',
'subplus': '\u2ABF',
'subrarr': '\u2979',
'subsim': '\u2AC7',
'subsub': '\u2AD5',
'subsup': '\u2AD3',
'sung': '\u266A',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supE': '\u2AC6',
'supseteqq': '\u2AC6',
'supdot': '\u2ABE',
'supdsub': '\u2AD8',
'supedot': '\u2AC4',
'suphsol': '\u27C9',
'suphsub': '\u2AD7',
'suplarr': '\u297B',
'supmult': '\u2AC2',
'supnE': '\u2ACC',
'supsetneqq': '\u2ACC',
'supne': '\u228B',
'supsetneq': '\u228B',
'supplus': '\u2AC0',
'supsim': '\u2AC8',
'supsub': '\u2AD4',
'supsup': '\u2AD6',
'swArr': '\u21D9',
'swnwar': '\u292A',
'szlig': '\u00DF',
'target': '\u2316',
'tau': '\u03C4',
'tcaron': '\u0165',
'tcedil': '\u0163',
'tcy': '\u0442',
'telrec': '\u2315',
'tfr': '\uD835\uDD31',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thetav': '\u03D1',
'vartheta': '\u03D1',
'thorn': '\u00FE',
'times': '\u00D7',
'timesbar': '\u2A31',
'timesd': '\u2A30',
'topbot': '\u2336',
'topcir': '\u2AF1',
'topf': '\uD835\uDD65',
'topfork': '\u2ADA',
'tprime': '\u2034',
'triangle': '\u25B5',
'utri': '\u25B5',
'triangleq': '\u225C',
'trie': '\u225C',
'tridot': '\u25EC',
'triminus': '\u2A3A',
'triplus': '\u2A39',
'trisb': '\u29CD',
'tritime': '\u2A3B',
'trpezium': '\u23E2',
'tscr': '\uD835\uDCC9',
'tscy': '\u0446',
'tshcy': '\u045B',
'tstrok': '\u0167',
'uHar': '\u2963',
'uacute': '\u00FA',
'ubrcy': '\u045E',
'ubreve': '\u016D',
'ucirc': '\u00FB',
'ucy': '\u0443',
'udblac': '\u0171',
'ufisht': '\u297E',
'ufr': '\uD835\uDD32',
'ugrave': '\u00F9',
'uhblk': '\u2580',
'ulcorn': '\u231C',
'ulcorner': '\u231C',
'ulcrop': '\u230F',
'ultri': '\u25F8',
'umacr': '\u016B',
'uogon': '\u0173',
'uopf': '\uD835\uDD66',
'upsi': '\u03C5',
'upsilon': '\u03C5',
'upuparrows': '\u21C8',
'uuarr': '\u21C8',
'urcorn': '\u231D',
'urcorner': '\u231D',
'urcrop': '\u230E',
'uring': '\u016F',
'urtri': '\u25F9',
'uscr': '\uD835\uDCCA',
'utdot': '\u22F0',
'utilde': '\u0169',
'uuml': '\u00FC',
'uwangle': '\u29A7',
'vBar': '\u2AE8',
'vBarv': '\u2AE9',
'vangrt': '\u299C',
'varsubsetneq': '\u228A\uFE00',
'vsubne': '\u228A\uFE00',
'varsubsetneqq': '\u2ACB\uFE00',
'vsubnE': '\u2ACB\uFE00',
'varsupsetneq': '\u228B\uFE00',
'vsupne': '\u228B\uFE00',
'varsupsetneqq': '\u2ACC\uFE00',
'vsupnE': '\u2ACC\uFE00',
'vcy': '\u0432',
'veebar': '\u22BB',
'veeeq': '\u225A',
'vellip': '\u22EE',
'vfr': '\uD835\uDD33',
'vopf': '\uD835\uDD67',
'vscr': '\uD835\uDCCB',
'vzigzag': '\u299A',
'wcirc': '\u0175',
'wedbar': '\u2A5F',
'wedgeq': '\u2259',
'weierp': '\u2118',
'wp': '\u2118',
'wfr': '\uD835\uDD34',
'wopf': '\uD835\uDD68',
'wscr': '\uD835\uDCCC',
'xfr': '\uD835\uDD35',
'xi': '\u03BE',
'xnis': '\u22FB',
'xopf': '\uD835\uDD69',
'xscr': '\uD835\uDCCD',
'yacute': '\u00FD',
'yacy': '\u044F',
'ycirc': '\u0177',
'ycy': '\u044B',
'yen': '\u00A5',
'yfr': '\uD835\uDD36',
'yicy': '\u0457',
'yopf': '\uD835\uDD6A',
'yscr': '\uD835\uDCCE',
'yucy': '\u044E',
'yuml': '\u00FF',
'zacute': '\u017A',
'zcaron': '\u017E',
'zcy': '\u0437',
'zdot': '\u017C',
'zeta': '\u03B6',
'zfr': '\uD835\uDD37',
'zhcy': '\u0436',
'zigrarr': '\u21DD',
'zopf': '\uD835\uDD6B',
'zscr': '\uD835\uDCCF',
'zwj': '\u200D',
'zwnj': '\u200C'
};
// The &ngsp; pseudo-entity is denoting a space.
// 0xE500 is a PUA (Private Use Areas) unicode character
// This is inspired by the Angular Dart implementation.
const NGSP_UNICODE = '\uE500';
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE;
class TokenError extends ParseError {
constructor(errorMsg, tokenType, span) {
super(span, errorMsg);
this.tokenType = tokenType;
}
}
class TokenizeResult {
constructor(tokens, errors, nonNormalizedIcuExpressions) {
this.tokens = tokens;
this.errors = errors;
this.nonNormalizedIcuExpressions = nonNormalizedIcuExpressions;
}
}
function tokenize(source, url, getTagDefinition, options = {}) {
const tokenizer = new _Tokenizer(new ParseSourceFile(source, url), getTagDefinition, options);
tokenizer.tokenize();
return new TokenizeResult(mergeTextTokens(tokenizer.tokens), tokenizer.errors, tokenizer.nonNormalizedIcuExpressions);
}
const _CR_OR_CRLF_REGEXP = /\r\n?/g;
function _unexpectedCharacterErrorMsg(charCode) {
const char = charCode === $EOF ? 'EOF' : String.fromCharCode(charCode);
return `Unexpected character "${char}"`;
}
function _unknownEntityErrorMsg(entitySrc) {
return `Unknown entity "${entitySrc}" - use the ";" or ";" syntax`;
}
function _unparsableEntityErrorMsg(type, entityStr) {
return `Unable to parse entity "${entityStr}" - ${type} character reference entities must end with ";"`;
}
var CharacterReferenceType;
(function (CharacterReferenceType) {
CharacterReferenceType["HEX"] = "hexadecimal";
CharacterReferenceType["DEC"] = "decimal";
})(CharacterReferenceType || (CharacterReferenceType = {}));
class _ControlFlowError {
constructor(error) {
this.error = error;
}
}
// See https://www.w3.org/TR/html51/syntax.html#writing-html-documents
class _Tokenizer {
/**
* @param _file The html source file being tokenized.
* @param _getTagDefinition A function that will retrieve a tag definition for a given tag name.
* @param options Configuration of the tokenization.
*/
constructor(_file, _getTagDefinition, options) {
this._getTagDefinition = _getTagDefinition;
this._currentTokenStart = null;
this._currentTokenType = null;
this._expansionCaseStack = [];
this._inInterpolation = false;
this.tokens = [];
this.errors = [];
this.nonNormalizedIcuExpressions = [];
this._tokenizeIcu = options.tokenizeExpansionForms || false;
this._interpolationConfig = options.interpolationConfig || DEFAULT_INTERPOLATION_CONFIG;
this._leadingTriviaCodePoints =
options.leadingTriviaChars && options.leadingTriviaChars.map(c => c.codePointAt(0) || 0);
const range = options.range || { endPos: _file.content.length, startPos: 0, startLine: 0, startCol: 0 };
this._cursor = options.escapedString ? new EscapedCharacterCursor(_file, range) :
new PlainCharacterCursor(_file, range);
this._preserveLineEndings = options.preserveLineEndings || false;
this._i18nNormalizeLineEndingsInICUs = options.i18nNormalizeLineEndingsInICUs || false;
this._tokenizeBlocks = options.tokenizeBlocks ?? true;
try {
this._cursor.init();
}
catch (e) {
this.handleError(e);
}
}
_processCarriageReturns(content) {
if (this._preserveLineEndings) {
return content;
}
// https://www.w3.org/TR/html51/syntax.html#preprocessing-the-input-stream
// In order to keep the original position in the source, we can not
// pre-process it.
// Instead CRs are processed right before instantiating the tokens.
return content.replace(_CR_OR_CRLF_REGEXP, '\n');
}
tokenize() {
while (this._cursor.peek() !== $EOF) {
const start = this._cursor.clone();
try {
if (this._attemptCharCode($LT)) {
if (this._attemptCharCode($BANG)) {
if (this._attemptCharCode($LBRACKET)) {
this._consumeCdata(start);
}
else if (this._attemptCharCode($MINUS)) {
this._consumeComment(start);
}
else {
this._consumeDocType(start);
}
}
else if (this._attemptCharCode($SLASH)) {
this._consumeTagClose(start);
}
else {
this._consumeTagOpen(start);
}
}
else if (this._tokenizeBlocks && this._attemptCharCode($AT)) {
this._consumeBlockStart(start);
}
else if (this._tokenizeBlocks && !this._inInterpolation && !this._isInExpansionCase() &&
!this._isInExpansionForm() && this._attemptCharCode($RBRACE)) {
this._consumeBlockEnd(start);
}
else if (!(this._tokenizeIcu && this._tokenizeExpansionForm())) {
// In (possibly interpolated) text the end of the text is given by `isTextEnd()`, while
// the premature end of an interpolation is given by the start of a new HTML element.
this._consumeWithInterpolation(5 /* TokenType.TEXT */, 8 /* TokenType.INTERPOLATION */, () => this._isTextEnd(), () => this._isTagStart());
}
}
catch (e) {
this.handleError(e);
}
}
this._beginToken(29 /* TokenType.EOF */);
this._endToken([]);
}
_getBlockName() {
// This allows us to capture up something like `@else if`, but not `@ if`.
let spacesInNameAllowed = false;
const nameCursor = this._cursor.clone();
this._attemptCharCodeUntilFn(code => {
if (isWhitespace(code)) {
return !spacesInNameAllowed;
}
if (isBlockNameChar(code)) {
spacesInNameAllowed = true;
return false;
}
return true;
});
return this._cursor.getChars(nameCursor).trim();
}
_consumeBlockStart(start) {
this._beginToken(24 /* TokenType.BLOCK_OPEN_START */, start);
const startToken = this._endToken([this._getBlockName()]);
if (this._cursor.peek() === $LPAREN) {
// Advance past the opening paren.
this._cursor.advance();
// Capture the parameters.
this._consumeBlockParameters();
// Allow spaces before the closing paren.
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode($RPAREN)) {
// Allow spaces after the paren.
this._attemptCharCodeUntilFn(isNotWhitespace);
}
else {
startToken.type = 28 /* TokenType.INCOMPLETE_BLOCK_OPEN */;
return;
}
}
if (this._attemptCharCode($LBRACE)) {
this._beginToken(25 /* TokenType.BLOCK_OPEN_END */);
this._endToken([]);
}
else {
startToken.type = 28 /* TokenType.INCOMPLETE_BLOCK_OPEN */;
}
}
_consumeBlockEnd(start) {
this._beginToken(26 /* TokenType.BLOCK_CLOSE */, start);
this._endToken([]);
}
_consumeBlockParameters() {
// Trim the whitespace until the first parameter.
this._attemptCharCodeUntilFn(isBlockParameterChar);
while (this._cursor.peek() !== $RPAREN && this._cursor.peek() !== $EOF) {
this._beginToken(27 /* TokenType.BLOCK_PARAMETER */);
const start = this._cursor.clone();
let inQuote = null;
let openParens = 0;
// Consume the parameter until the next semicolon or brace.
// Note that we skip over semicolons/braces inside of strings.
while ((this._cursor.peek() !== $SEMICOLON && this._cursor.peek() !== $EOF) ||
inQuote !== null) {
const char = this._cursor.peek();
// Skip to the next character if it was escaped.
if (char === $BACKSLASH) {
this._cursor.advance();
}
else if (char === inQuote) {
inQuote = null;
}
else if (inQuote === null && isQuote(char)) {
inQuote = char;
}
else if (char === $LPAREN && inQuote === null) {
openParens++;
}
else if (char === $RPAREN && inQuote === null) {
if (openParens === 0) {
break;
}
else if (openParens > 0) {
openParens--;
}
}
this._cursor.advance();
}
this._endToken([this._cursor.getChars(start)]);
// Skip to the next parameter.
this._attemptCharCodeUntilFn(isBlockParameterChar);
}
}
/**
* @returns whether an ICU token has been created
* @internal
*/
_tokenizeExpansionForm() {
if (this.isExpansionFormStart()) {
this._consumeExpansionFormStart();
return true;
}
if (isExpansionCaseStart(this._cursor.peek()) && this._isInExpansionForm()) {
this._consumeExpansionCaseStart();
return true;
}
if (this._cursor.peek() === $RBRACE) {
if (this._isInExpansionCase()) {
this._consumeExpansionCaseEnd();
return true;
}
if (this._isInExpansionForm()) {
this._consumeExpansionFormEnd();
return true;
}
}
return false;
}
_beginToken(type, start = this._cursor.clone()) {
this._currentTokenStart = start;
this._currentTokenType = type;
}
_endToken(parts, end) {
if (this._currentTokenStart === null) {
throw new TokenError('Programming error - attempted to end a token when there was no start to the token', this._currentTokenType, this._cursor.getSpan(end));
}
if (this._currentTokenType === null) {
throw new TokenError('Programming error - attempted to end a token which has no token type', null, this._cursor.getSpan(this._currentTokenStart));
}
const token = {
type: this._currentTokenType,
parts,
sourceSpan: (end ?? this._cursor).getSpan(this._currentTokenStart, this._leadingTriviaCodePoints),
};
this.tokens.push(token);
this._currentTokenStart = null;
this._currentTokenType = null;
return token;
}
_createError(msg, span) {
if (this._isInExpansionForm()) {
msg += ` (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`;
}
const error = new TokenError(msg, this._currentTokenType, span);
this._currentTokenStart = null;
this._currentTokenType = null;
return new _ControlFlowError(error);
}
handleError(e) {
if (e instanceof CursorError) {
e = this._createError(e.msg, this._cursor.getSpan(e.cursor));
}
if (e instanceof _ControlFlowError) {
this.errors.push(e.error);
}
else {
throw e;
}
}
_attemptCharCode(charCode) {
if (this._cursor.peek() === charCode) {
this._cursor.advance();
return true;
}
return false;
}
_attemptCharCodeCaseInsensitive(charCode) {
if (compareCharCodeCaseInsensitive(this._cursor.peek(), charCode)) {
this._cursor.advance();
return true;
}
return false;
}
_requireCharCode(charCode) {
const location = this._cursor.clone();
if (!this._attemptCharCode(charCode)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(location));
}
}
_attemptStr(chars) {
const len = chars.length;
if (this._cursor.charsLeft() < len) {
return false;
}
const initialPosition = this._cursor.clone();
for (let i = 0; i < len; i++) {
if (!this._attemptCharCode(chars.charCodeAt(i))) {
// If attempting to parse the string fails, we want to reset the parser
// to where it was before the attempt
this._cursor = initialPosition;
return false;
}
}
return true;
}
_attemptStrCaseInsensitive(chars) {
for (let i = 0; i < chars.length; i++) {
if (!this._attemptCharCodeCaseInsensitive(chars.charCodeAt(i))) {
return false;
}
}
return true;
}
_requireStr(chars) {
const location = this._cursor.clone();
if (!this._attemptStr(chars)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(location));
}
}
_attemptCharCodeUntilFn(predicate) {
while (!predicate(this._cursor.peek())) {
this._cursor.advance();
}
}
_requireCharCodeUntilFn(predicate, len) {
const start = this._cursor.clone();
this._attemptCharCodeUntilFn(predicate);
if (this._cursor.diff(start) < len) {
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start));
}
}
_attemptUntilChar(char) {
while (this._cursor.peek() !== char) {
this._cursor.advance();
}
}
_readChar() {
// Don't rely upon reading directly from `_input` as the actual char value
// may have been generated from an escape sequence.
const char = String.fromCodePoint(this._cursor.peek());
this._cursor.advance();
return char;
}
_consumeEntity(textTokenType) {
this._beginToken(9 /* TokenType.ENCODED_ENTITY */);
const start = this._cursor.clone();
this._cursor.advance();
if (this._attemptCharCode($HASH)) {
const isHex = this._attemptCharCode($x) || this._attemptCharCode($X);
const codeStart = this._cursor.clone();
this._attemptCharCodeUntilFn(isDigitEntityEnd);
if (this._cursor.peek() != $SEMICOLON) {
// Advance cursor to include the peeked character in the string provided to the error
// message.
this._cursor.advance();
const entityType = isHex ? CharacterReferenceType.HEX : CharacterReferenceType.DEC;
throw this._createError(_unparsableEntityErrorMsg(entityType, this._cursor.getChars(start)), this._cursor.getSpan());
}
const strNum = this._cursor.getChars(codeStart);
this._cursor.advance();
try {
const charCode = parseInt(strNum, isHex ? 16 : 10);
this._endToken([String.fromCharCode(charCode), this._cursor.getChars(start)]);
}
catch {
throw this._createError(_unknownEntityErrorMsg(this._cursor.getChars(start)), this._cursor.getSpan());
}
}
else {
const nameStart = this._cursor.clone();
this._attemptCharCodeUntilFn(isNamedEntityEnd);
if (this._cursor.peek() != $SEMICOLON) {
// No semicolon was found so abort the encoded entity token that was in progress, and treat
// this as a text token
this._beginToken(textTokenType, start);
this._cursor = nameStart;
this._endToken(['&']);
}
else {
const name = this._cursor.getChars(nameStart);
this._cursor.advance();
const char = NAMED_ENTITIES[name];
if (!char) {
throw this._createError(_unknownEntityErrorMsg(name), this._cursor.getSpan(start));
}
this._endToken([char, `&${name};`]);
}
}
}
_consumeRawText(consumeEntities, endMarkerPredicate) {
this._beginToken(consumeEntities ? 6 /* TokenType.ESCAPABLE_RAW_TEXT */ : 7 /* TokenType.RAW_TEXT */);
const parts = [];
while (true) {
const tagCloseStart = this._cursor.clone();
const foundEndMarker = endMarkerPredicate();
this._cursor = tagCloseStart;
if (foundEndMarker) {
break;
}
if (consumeEntities && this._cursor.peek() === $AMPERSAND) {
this._endToken([this._processCarriageReturns(parts.join(''))]);
parts.length = 0;
this._consumeEntity(6 /* TokenType.ESCAPABLE_RAW_TEXT */);
this._beginToken(6 /* TokenType.ESCAPABLE_RAW_TEXT */);
}
else {
parts.push(this._readChar());
}
}
this._endToken([this._processCarriageReturns(parts.join(''))]);
}
_consumeComment(start) {
this._beginToken(10 /* TokenType.COMMENT_START */, start);
this._requireCharCode($MINUS);
this._endToken([]);
this._consumeRawText(false, () => this._attemptStr('-->'));
this._beginToken(11 /* TokenType.COMMENT_END */);
this._requireStr('-->');
this._endToken([]);
}
_consumeCdata(start) {
this._beginToken(12 /* TokenType.CDATA_START */, start);
this._requireStr('CDATA[');
this._endToken([]);
this._consumeRawText(false, () => this._attemptStr(']]>'));
this._beginToken(13 /* TokenType.CDATA_END */);
this._requireStr(']]>');
this._endToken([]);
}
_consumeDocType(start) {
this._beginToken(18 /* TokenType.DOC_TYPE */, start);
const contentStart = this._cursor.clone();
this._attemptUntilChar($GT);
const content = this._cursor.getChars(contentStart);
this._cursor.advance();
this._endToken([content]);
}
_consumePrefixAndName() {
const nameOrPrefixStart = this._cursor.clone();
let prefix = '';
while (this._cursor.peek() !== $COLON && !isPrefixEnd(this._cursor.peek())) {
this._cursor.advance();
}
let nameStart;
if (this._cursor.peek() === $COLON) {
prefix = this._cursor.getChars(nameOrPrefixStart);
this._cursor.advance();
nameStart = this._cursor.clone();
}
else {
nameStart = nameOrPrefixStart;
}
this._requireCharCodeUntilFn(isNameEnd, prefix === '' ? 0 : 1);
const name = this._cursor.getChars(nameStart);
return [prefix, name];
}
_consumeTagOpen(start) {
let tagName;
let prefix;
let openTagToken;
try {
if (!isAsciiLetter(this._cursor.peek())) {
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start));
}
openTagToken = this._consumeTagOpenStart(start);
prefix = openTagToken.parts[0];
tagName = openTagToken.parts[1];
this._attemptCharCodeUntilFn(isNotWhitespace);
while (this._cursor.peek() !== $SLASH && this._cursor.peek() !== $GT &&
this._cursor.peek() !== $LT && this._cursor.peek() !== $EOF) {
this._consumeAttributeName();
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode($EQ)) {
this._attemptCharCodeUntilFn(isNotWhitespace);
this._consumeAttributeValue();
}
this._attemptCharCodeUntilFn(isNotWhitespace);
}
this._consumeTagOpenEnd();
}
catch (e) {
if (e instanceof _ControlFlowError) {
if (openTagToken) {
// We errored before we could close the opening tag, so it is incomplete.
openTagToken.type = 4 /* TokenType.INCOMPLETE_TAG_OPEN */;
}
else {
// When the start tag is invalid, assume we want a "<" as text.
// Back to back text tokens are merged at the end.
this._beginToken(5 /* TokenType.TEXT */, start);
this._endToken(['<']);
}
return;
}
throw e;
}
const contentTokenType = this._getTagDefinition(tagName).getContentType(prefix);
if (contentTokenType === TagContentType.RAW_TEXT) {
this._consumeRawTextWithTagClose(prefix, tagName, false);
}
else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) {
this._consumeRawTextWithTagClose(prefix, tagName, true);
}
}
_consumeRawTextWithTagClose(prefix, tagName, consumeEntities) {
this._consumeRawText(consumeEntities, () => {
if (!this._attemptCharCode($LT))
return false;
if (!this._attemptCharCode($SLASH))
return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
if (!this._attemptStrCaseInsensitive(tagName))
return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
return this._attemptCharCode($GT);
});
this._beginToken(3 /* TokenType.TAG_CLOSE */);
this._requireCharCodeUntilFn(code => code === $GT, 3);
this._cursor.advance(); // Consume the `>`
this._endToken([prefix, tagName]);
}
_consumeTagOpenStart(start) {
this._beginToken(0 /* TokenType.TAG_OPEN_START */, start);
const parts = this._consumePrefixAndName();
return this._endToken(parts);
}
_consumeAttributeName() {
const attrNameStart = this._cursor.peek();
if (attrNameStart === $SQ || attrNameStart === $DQ) {
throw this._createError(_unexpectedCharacterErrorMsg(attrNameStart), this._cursor.getSpan());
}
this._beginToken(14 /* TokenType.ATTR_NAME */);
const prefixAndName = this._consumePrefixAndName();
this._endToken(prefixAndName);
}
_consumeAttributeValue() {
if (this._cursor.peek() === $SQ || this._cursor.peek() === $DQ) {
const quoteChar = this._cursor.peek();
this._consumeQuote(quoteChar);
// In an attribute then end of the attribute value and the premature end to an interpolation
// are both triggered by the `quoteChar`.
const endPredicate = () => this._cursor.peek() === quoteChar;
this._consumeWithInterpolation(16 /* TokenType.ATTR_VALUE_TEXT */, 17 /* TokenType.ATTR_VALUE_INTERPOLATION */, endPredicate, endPredicate);
this._consumeQuote(quoteChar);
}
else {
const endPredicate = () => isNameEnd(this._cursor.peek());
this._consumeWithInterpolation(16 /* TokenType.ATTR_VALUE_TEXT */, 17 /* TokenType.ATTR_VALUE_INTERPOLATION */, endPredicate, endPredicate);
}
}
_consumeQuote(quoteChar) {
this._beginToken(15 /* TokenType.ATTR_QUOTE */);
this._requireCharCode(quoteChar);
this._endToken([String.fromCodePoint(quoteChar)]);
}
_consumeTagOpenEnd() {
const tokenType = this._attemptCharCode($SLASH) ? 2 /* TokenType.TAG_OPEN_END_VOID */ : 1 /* TokenType.TAG_OPEN_END */;
this._beginToken(tokenType);
this._requireCharCode($GT);
this._endToken([]);
}
_consumeTagClose(start) {
this._beginToken(3 /* TokenType.TAG_CLOSE */, start);
this._attemptCharCodeUntilFn(isNotWhitespace);
const prefixAndName = this._consumePrefixAndName();
this._attemptCharCodeUntilFn(isNotWhitespace);
this._requireCharCode($GT);
this._endToken(prefixAndName);
}
_consumeExpansionFormStart() {
this._beginToken(19 /* TokenType.EXPANSION_FORM_START */);
this._requireCharCode($LBRACE);
this._endToken([]);
this._expansionCaseStack.push(19 /* TokenType.EXPANSION_FORM_START */);
this._beginToken(7 /* TokenType.RAW_TEXT */);
const condition = this._readUntil($COMMA);
const normalizedCondition = this._processCarriageReturns(condition);
if (this._i18nNormalizeLineEndingsInICUs) {
// We explicitly want to normalize line endings for this text.
this._endToken([normalizedCondition]);
}
else {
// We are not normalizing line endings.
const conditionToken = this._endToken([condition]);
if (normalizedCondition !== condition) {
this.nonNormalizedIcuExpressions.push(conditionToken);
}
}
this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(7 /* TokenType.RAW_TEXT */);
const type = this._readUntil($COMMA);
this._endToken([type]);
this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
}
_consumeExpansionCaseStart() {
this._beginToken(20 /* TokenType.EXPANSION_CASE_VALUE */);
const value = this._readUntil($LBRACE).trim();
this._endToken([value]);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(21 /* TokenType.EXPANSION_CASE_EXP_START */);
this._requireCharCode($LBRACE);
this._endToken([]);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.push(21 /* TokenType.EXPANSION_CASE_EXP_START */);
}
_consumeExpansionCaseEnd() {
this._beginToken(22 /* TokenType.EXPANSION_CASE_EXP_END */);
this._requireCharCode($RBRACE);
this._endToken([]);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.pop();
}
_consumeExpansionFormEnd() {
this._beginToken(23 /* TokenType.EXPANSION_FORM_END */);
this._requireCharCode($RBRACE);
this._endToken([]);
this._expansionCaseStack.pop();
}
/**
* Consume a string that may contain interpolation expressions.
*
* The first token consumed will be of `tokenType` and then there will be alternating
* `interpolationTokenType` and `tokenType` tokens until the `endPredicate()` returns true.
*
* If an interpolation token ends prematurely it will have no end marker in its `parts` array.
*
* @param textTokenType the kind of tokens to interleave around interpolation tokens.
* @param interpolationTokenType the kind of tokens that contain interpolation.
* @param endPredicate a function that should return true when we should stop consuming.
* @param endInterpolation a function that should return true if there is a premature end to an
* interpolation expression - i.e. before we get to the normal interpolation closing marker.
*/
_consumeWithInterpolation(textTokenType, interpolationTokenType, endPredicate, endInterpolation) {
this._beginToken(textTokenType);
const parts = [];
while (!endPredicate()) {
const current = this._cursor.clone();
if (this._interpolationConfig && this._attemptStr(this._interpolationConfig.start)) {
this._endToken([this._processCarriageReturns(parts.join(''))], current);
parts.length = 0;
this._consumeInterpolation(interpolationTokenType, current, endInterpolation);
this._beginToken(textTokenType);
}
else if (this._cursor.peek() === $AMPERSAND) {
this._endToken([this._processCarriageReturns(parts.join(''))]);
parts.length = 0;
this._consumeEntity(textTokenType);
this._beginToken(textTokenType);
}
else {
parts.push(this._readChar());
}
}
// It is possible that an interpolation was started but not ended inside this text token.
// Make sure that we reset the state of the lexer correctly.
this._inInterpolation = false;
this._endToken([this._processCarriageReturns(parts.join(''))]);
}
/**
* Consume a block of text that has been interpreted as an Angular interpolation.
*
* @param interpolationTokenType the type of the interpolation token to generate.
* @param interpolationStart a cursor that points to the start of this interpolation.
* @param prematureEndPredicate a function that should return true if the next characters indicate
* an end to the interpolation before its normal closing marker.
*/
_consumeInterpolation(interpolationTokenType, interpolationStart, prematureEndPredicate) {
const parts = [];
this._beginToken(interpolationTokenType, interpolationStart);
parts.push(this._interpolationConfig.start);
// Find the end of the interpolation, ignoring content inside quotes.
const expressionStart = this._cursor.clone();
let inQuote = null;
let inComment = false;
while (this._cursor.peek() !== $EOF &&
(prematureEndPredicate === null || !prematureEndPredicate())) {
const current = this._cursor.clone();
if (this._isTagStart()) {
// We are starting what looks like an HTML element in the middle of this interpolation.
// Reset the cursor to before the `<` character and end the interpolation token.
// (This is actually wrong but here for backward compatibility).
this._cursor = current;
parts.push(this._getProcessedChars(expressionStart, current));
this._endToken(parts);
return;
}
if (inQuote === null) {
if (this._attemptStr(this._interpolationConfig.end)) {
// We are not in a string, and we hit the end interpolation marker
parts.push(this._getProcessedChars(expressionStart, current));
parts.push(this._interpolationConfig.end);
this._endToken(parts);
return;
}
else if (this._attemptStr('//')) {
// Once we are in a comment we ignore any quotes
inComment = true;
}
}
const char = this._cursor.peek();
this._cursor.advance();
if (char === $BACKSLASH) {
// Skip the next character because it was escaped.
this._cursor.advance();
}
else if (char === inQuote) {
// Exiting the current quoted string
inQuote = null;
}
else if (!inComment && inQuote === null && isQuote(char)) {
// Entering a new quoted string
inQuote = char;
}
}
// We hit EOF without finding a closing interpolation marker
parts.push(this._getProcessedChars(expressionStart, this._cursor));
this._endToken(parts);
}
_getProcessedChars(start, end) {
return this._processCarriageReturns(end.getChars(start));
}
_isTextEnd() {
if (this._isTagStart() || this._cursor.peek() === $EOF) {
return true;
}
if (this._tokenizeIcu && !this._inInterpolation) {
if (this.isExpansionFormStart()) {
// start of an expansion form
return true;
}
if (this._cursor.peek() === $RBRACE && this._isInExpansionCase()) {
// end of and expansion case
return true;
}
}
if (this._tokenizeBlocks && !this._inInterpolation && !this._isInExpansion() &&
(this._cursor.peek() === $AT || this._cursor.peek() === $RBRACE)) {
return true;
}
return false;
}
/**
* Returns true if the current cursor is pointing to the start of a tag
* (opening/closing/comments/cdata/etc).
*/
_isTagStart() {
if (this._cursor.peek() === $LT) {
// We assume that `<` followed by whitespace is not the start of an HTML element.
const tmp = this._cursor.clone();
tmp.advance();
// If the next character is alphabetic, ! nor / then it is a tag start
const code = tmp.peek();
if (($a <= code && code <= $z) || ($A <= code && code <= $Z) ||
code === $SLASH || code === $BANG) {
return true;
}
}
return false;
}
_readUntil(char) {
const start = this._cursor.clone();
this._attemptUntilChar(char);
return this._cursor.getChars(start);
}
_isInExpansion() {
return this._isInExpansionCase() || this._isInExpansionForm();
}
_isInExpansionCase() {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
21 /* TokenType.EXPANSION_CASE_EXP_START */;
}
_isInExpansionForm() {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
19 /* TokenType.EXPANSION_FORM_START */;
}
isExpansionFormStart() {
if (this._cursor.peek() !== $LBRACE) {
return false;
}
if (this._interpolationConfig) {
const start = this._cursor.clone();
const isInterpolation = this._attemptStr(this._interpolationConfig.start);
this._cursor = start;
return !isInterpolation;
}
return true;
}
}
function isNotWhitespace(code) {
return !isWhitespace(code) || code === $EOF;
}
function isNameEnd(code) {
return isWhitespace(code) || code === $GT || code === $LT ||
code === $SLASH || code === $SQ || code === $DQ || code === $EQ ||
code === $EOF;
}
function isPrefixEnd(code) {
return (code < $a || $z < code) && (code < $A || $Z < code) &&
(code < $0 || code > $9);
}
function isDigitEntityEnd(code) {
return code === $SEMICOLON || code === $EOF || !isAsciiHexDigit(code);
}
function isNamedEntityEnd(code) {
return code === $SEMICOLON || code === $EOF || !isAsciiLetter(code);
}
function isExpansionCaseStart(peek) {
return peek !== $RBRACE;
}
function compareCharCodeCaseInsensitive(code1, code2) {
return toUpperCaseCharCode(code1) === toUpperCaseCharCode(code2);
}
function toUpperCaseCharCode(code) {
return code >= $a && code <= $z ? code - $a + $A : code;
}
function isBlockNameChar(code) {
return isAsciiLetter(code) || isDigit(code) || code === $_;
}
function isBlockParameterChar(code) {
return code !== $SEMICOLON && isNotWhitespace(code);
}
function mergeTextTokens(srcTokens) {
const dstTokens = [];
let lastDstToken = undefined;
for (let i = 0; i < srcTokens.length; i++) {
const token = srcTokens[i];
if ((lastDstToken && lastDstToken.type === 5 /* TokenType.TEXT */ && token.type === 5 /* TokenType.TEXT */) ||
(lastDstToken && lastDstToken.type === 16 /* TokenType.ATTR_VALUE_TEXT */ &&
token.type === 16 /* TokenType.ATTR_VALUE_TEXT */)) {
lastDstToken.parts[0] += token.parts[0];
lastDstToken.sourceSpan.end = token.sourceSpan.end;
}
else {
lastDstToken = token;
dstTokens.push(lastDstToken);
}
}
return dstTokens;
}
class PlainCharacterCursor {
constructor(fileOrCursor, range) {
if (fileOrCursor instanceof PlainCharacterCursor) {
this.file = fileOrCursor.file;
this.input = fileOrCursor.input;
this.end = fileOrCursor.end;
const state = fileOrCursor.state;
// Note: avoid using `{...fileOrCursor.state}` here as that has a severe performance penalty.
// In ES5 bundles the object spread operator is translated into the `__assign` helper, which
// is not optimized by VMs as efficiently as a raw object literal. Since this constructor is
// called in tight loops, this difference matters.
this.state = {
peek: state.peek,
offset: state.offset,
line: state.line,
column: state.column,
};
}
else {
if (!range) {
throw new Error('Programming error: the range argument must be provided with a file argument.');
}
this.file = fileOrCursor;
this.input = fileOrCursor.content;
this.end = range.endPos;
this.state = {
peek: -1,
offset: range.startPos,
line: range.startLine,
column: range.startCol,
};
}
}
clone() {
return new PlainCharacterCursor(this);
}
peek() {
return this.state.peek;
}
charsLeft() {
return this.end - this.state.offset;
}
diff(other) {
return this.state.offset - other.state.offset;
}
advance() {
this.advanceState(this.state);
}
init() {
this.updatePeek(this.state);
}
getSpan(start, leadingTriviaCodePoints) {
start = start || this;
let fullStart = start;
if (leadingTriviaCodePoints) {
while (this.diff(start) > 0 && leadingTriviaCodePoints.indexOf(start.peek()) !== -1) {
if (fullStart === start) {
start = start.clone();
}
start.advance();
}
}
const startLocation = this.locationFromCursor(start);
const endLocation = this.locationFromCursor(this);
const fullStartLocation = fullStart !== start ? this.locationFromCursor(fullStart) : startLocation;
return new ParseSourceSpan(startLocation, endLocation, fullStartLocation);
}
getChars(start) {
return this.input.substring(start.state.offset, this.state.offset);
}
charAt(pos) {
return this.input.charCodeAt(pos);
}
advanceState(state) {
if (state.offset >= this.end) {
this.state = state;
throw new CursorError('Unexpected character "EOF"', this);
}
const currentChar = this.charAt(state.offset);
if (currentChar === $LF) {
state.line++;
state.column = 0;
}
else if (!isNewLine(currentChar)) {
state.column++;
}
state.offset++;
this.updatePeek(state);
}
updatePeek(state) {
state.peek = state.offset >= this.end ? $EOF : this.charAt(state.offset);
}
locationFromCursor(cursor) {
return new ParseLocation(cursor.file, cursor.state.offset, cursor.state.line, cursor.state.column);
}
}
class EscapedCharacterCursor extends PlainCharacterCursor {
constructor(fileOrCursor, range) {
if (fileOrCursor instanceof EscapedCharacterCursor) {
super(fileOrCursor);
this.internalState = { ...fileOrCursor.internalState };
}
else {
super(fileOrCursor, range);
this.internalState = this.state;
}
}
advance() {
this.state = this.internalState;
super.advance();
this.processEscapeSequence();
}
init() {
super.init();
this.processEscapeSequence();
}
clone() {
return new EscapedCharacterCursor(this);
}
getChars(start) {
const cursor = start.clone();
let chars = '';
while (cursor.internalState.offset < this.internalState.offset) {
chars += String.fromCodePoint(cursor.peek());
cursor.advance();
}
return chars;
}
/**
* Process the escape sequence that starts at the current position in the text.
*
* This method is called to ensure that `peek` has the unescaped value of escape sequences.
*/
processEscapeSequence() {
const peek = () => this.internalState.peek;
if (peek() === $BACKSLASH) {
// We have hit an escape sequence so we need the internal state to become independent
// of the external state.
this.internalState = { ...this.state };
// Move past the backslash
this.advanceState(this.internalState);
// First check for standard control char sequences
if (peek() === $n) {
this.state.peek = $LF;
}
else if (peek() === $r) {
this.state.peek = $CR;
}
else if (peek() === $v) {
this.state.peek = $VTAB;
}
else if (peek() === $t) {
this.state.peek = $TAB;
}
else if (peek() === $b) {
this.state.peek = $BSPACE;
}
else if (peek() === $f) {
this.state.peek = $FF;
}
// Now consider more complex sequences
else if (peek() === $u) {
// Unicode code-point sequence
this.advanceState(this.internalState); // advance past the `u` char
if (peek() === $LBRACE) {
// Variable length Unicode, e.g. `\x{123}`
this.advanceState(this.internalState); // advance past the `{` char
// Advance past the variable number of hex digits until we hit a `}` char
const digitStart = this.clone();
let length = 0;
while (peek() !== $RBRACE) {
this.advanceState(this.internalState);
length++;
}
this.state.peek = this.decodeHexDigits(digitStart, length);
}
else {
// Fixed length Unicode, e.g. `\u1234`
const digitStart = this.clone();
this.advanceState(this.internalState);
this.advanceState(this.internalState);
this.advanceState(this.internalState);
this.state.peek = this.decodeHexDigits(digitStart, 4);
}
}
else if (peek() === $x) {
// Hex char code, e.g. `\x2F`
this.advanceState(this.internalState); // advance past the `x` char
const digitStart = this.clone();
this.advanceState(this.internalState);
this.state.peek = this.decodeHexDigits(digitStart, 2);
}
else if (isOctalDigit(peek())) {
// Octal char code, e.g. `\012`,
let octal = '';
let length = 0;
let previous = this.clone();
while (isOctalDigit(peek()) && length < 3) {
previous = this.clone();
octal += String.fromCodePoint(peek());
this.advanceState(this.internalState);
length++;
}
this.state.peek = parseInt(octal, 8);
// Backup one char
this.internalState = previous.internalState;
}
else if (isNewLine(this.internalState.peek)) {
// Line continuation `\` followed by a new line
this.advanceState(this.internalState); // advance over the newline
this.state = this.internalState;
}
else {
// If none of the `if` blocks were executed then we just have an escaped normal character.
// In that case we just, effectively, skip the backslash from the character.
this.state.peek = this.internalState.peek;
}
}
}
decodeHexDigits(start, length) {
const hex = this.input.slice(start.internalState.offset, start.internalState.offset + length);
const charCode = parseInt(hex, 16);
if (!isNaN(charCode)) {
return charCode;
}
else {
start.state = start.internalState;
throw new CursorError('Invalid hexadecimal escape sequence', start);
}
}
}
class CursorError {
constructor(msg, cursor) {
this.msg = msg;
this.cursor = cursor;
}
}
class TreeError extends ParseError {
static create(elementName, span, msg) {
return new TreeError(elementName, span, msg);
}
constructor(elementName, span, msg) {
super(span, msg);
this.elementName = elementName;
}
}
class ParseTreeResult {
constructor(rootNodes, errors) {
this.rootNodes = rootNodes;
this.errors = errors;
}
}
class Parser {
constructor(getTagDefinition) {
this.getTagDefinition = getTagDefinition;
}
parse(source, url, options) {
const tokenizeResult = tokenize(source, url, this.getTagDefinition, options);
const parser = new _TreeBuilder(tokenizeResult.tokens, this.getTagDefinition);
parser.build();
return new ParseTreeResult(parser.rootNodes, tokenizeResult.errors.concat(parser.errors));
}
}
class _TreeBuilder {
constructor(tokens, getTagDefinition) {
this.tokens = tokens;
this.getTagDefinition = getTagDefinition;
this._index = -1;
this._containerStack = [];
this.rootNodes = [];
this.errors = [];
this._advance();
}
build() {
while (this._peek.type !== 29 /* TokenType.EOF */) {
if (this._peek.type === 0 /* TokenType.TAG_OPEN_START */ ||
this._peek.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) {
this._consumeStartTag(this._advance());
}
else if (this._peek.type === 3 /* TokenType.TAG_CLOSE */) {
this._consumeEndTag(this._advance());
}
else if (this._peek.type === 12 /* TokenType.CDATA_START */) {
this._closeVoidElement();
this._consumeCdata(this._advance());
}
else if (this._peek.type === 10 /* TokenType.COMMENT_START */) {
this._closeVoidElement();
this._consumeComment(this._advance());
}
else if (this._peek.type === 5 /* TokenType.TEXT */ || this._peek.type === 7 /* TokenType.RAW_TEXT */ ||
this._peek.type === 6 /* TokenType.ESCAPABLE_RAW_TEXT */) {
this._closeVoidElement();
this._consumeText(this._advance());
}
else if (this._peek.type === 19 /* TokenType.EXPANSION_FORM_START */) {
this._consumeExpansion(this._advance());
}
else if (this._peek.type === 24 /* TokenType.BLOCK_OPEN_START */) {
this._closeVoidElement();
this._consumeBlockOpen(this._advance());
}
else if (this._peek.type === 26 /* TokenType.BLOCK_CLOSE */) {
this._closeVoidElement();
this._consumeBlockClose(this._advance());
}
else if (this._peek.type === 28 /* TokenType.INCOMPLETE_BLOCK_OPEN */) {
this._closeVoidElement();
this._consumeIncompleteBlock(this._advance());
}
else {
// Skip all other tokens...
this._advance();
}
}
for (const leftoverContainer of this._containerStack) {
// Unlike HTML elements, blocks aren't closed implicitly by the end of the file.
if (leftoverContainer instanceof Block) {
this.errors.push(TreeError.create(leftoverContainer.name, leftoverContainer.sourceSpan, `Unclosed block "${leftoverContainer.name}"`));
}
}
}
_advance() {
const prev = this._peek;
if (this._index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this._index++;
}
this._peek = this.tokens[this._index];
return prev;
}
_advanceIf(type) {
if (this._peek.type === type) {
return this._advance();
}
return null;
}
_consumeCdata(_startToken) {
this._consumeText(this._advance());
this._advanceIf(13 /* TokenType.CDATA_END */);
}
_consumeComment(token) {
const text = this._advanceIf(7 /* TokenType.RAW_TEXT */);
const endToken = this._advanceIf(11 /* TokenType.COMMENT_END */);
const value = text != null ? text.parts[0].trim() : null;
const sourceSpan = endToken == null ?
token.sourceSpan :
new ParseSourceSpan(token.sourceSpan.start, endToken.sourceSpan.end, token.sourceSpan.fullStart);
this._addToParent(new Comment(value, sourceSpan));
}
_consumeExpansion(token) {
const switchValue = this._advance();
const type = this._advance();
const cases = [];
// read =
while (this._peek.type === 20 /* TokenType.EXPANSION_CASE_VALUE */) {
const expCase = this._parseExpansionCase();
if (!expCase)
return; // error
cases.push(expCase);
}
// read the final }
if (this._peek.type !== 23 /* TokenType.EXPANSION_FORM_END */) {
this.errors.push(TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
return;
}
const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end, token.sourceSpan.fullStart);
this._addToParent(new Expansion(switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan));
this._advance();
}
_parseExpansionCase() {
const value = this._advance();
// read {
if (this._peek.type !== 21 /* TokenType.EXPANSION_CASE_EXP_START */) {
this.errors.push(TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
return null;
}
// read until }
const start = this._advance();
const exp = this._collectExpansionExpTokens(start);
if (!exp)
return null;
const end = this._advance();
exp.push({ type: 29 /* TokenType.EOF */, parts: [], sourceSpan: end.sourceSpan });
// parse everything in between { and }
const expansionCaseParser = new _TreeBuilder(exp, this.getTagDefinition);
expansionCaseParser.build();
if (expansionCaseParser.errors.length > 0) {
this.errors = this.errors.concat(expansionCaseParser.errors);
return null;
}
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end, value.sourceSpan.fullStart);
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end, start.sourceSpan.fullStart);
return new ExpansionCase(value.parts[0], expansionCaseParser.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
}
_collectExpansionExpTokens(start) {
const exp = [];
const expansionFormStack = [21 /* TokenType.EXPANSION_CASE_EXP_START */];
while (true) {
if (this._peek.type === 19 /* TokenType.EXPANSION_FORM_START */ ||
this._peek.type === 21 /* TokenType.EXPANSION_CASE_EXP_START */) {
expansionFormStack.push(this._peek.type);
}
if (this._peek.type === 22 /* TokenType.EXPANSION_CASE_EXP_END */) {
if (lastOnStack(expansionFormStack, 21 /* TokenType.EXPANSION_CASE_EXP_START */)) {
expansionFormStack.pop();
if (expansionFormStack.length === 0)
return exp;
}
else {
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === 23 /* TokenType.EXPANSION_FORM_END */) {
if (lastOnStack(expansionFormStack, 19 /* TokenType.EXPANSION_FORM_START */)) {
expansionFormStack.pop();
}
else {
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === 29 /* TokenType.EOF */) {
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
_consumeText(token) {
const tokens = [token];
const startSpan = token.sourceSpan;
let text = token.parts[0];
if (text.length > 0 && text[0] === '\n') {
const parent = this._getContainer();
if (parent != null && parent.children.length === 0 &&
this.getTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
tokens[0] = { type: token.type, sourceSpan: token.sourceSpan, parts: [text] };
}
}
while (this._peek.type === 8 /* TokenType.INTERPOLATION */ || this._peek.type === 5 /* TokenType.TEXT */ ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */) {
token = this._advance();
tokens.push(token);
if (token.type === 8 /* TokenType.INTERPOLATION */) {
// For backward compatibility we decode HTML entities that appear in interpolation
// expressions. This is arguably a bug, but it could be a considerable breaking change to
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer
// chain after View Engine has been removed.
text += token.parts.join('').replace(/&([^;]+);/g, decodeEntity);
}
else if (token.type === 9 /* TokenType.ENCODED_ENTITY */) {
text += token.parts[0];
}
else {
text += token.parts.join('');
}
}
if (text.length > 0) {
const endSpan = token.sourceSpan;
this._addToParent(new Text(text, new ParseSourceSpan(startSpan.start, endSpan.end, startSpan.fullStart, startSpan.details), tokens));
}
}
_closeVoidElement() {
const el = this._getContainer();
if (el instanceof Element && this.getTagDefinition(el.name).isVoid) {
this._containerStack.pop();
}
}
_consumeStartTag(startTagToken) {
const [prefix, name] = startTagToken.parts;
const attrs = [];
while (this._peek.type === 14 /* TokenType.ATTR_NAME */) {
attrs.push(this._consumeAttr(this._advance()));
}
const fullName = this._getElementFullName(prefix, name, this._getClosestParentElement());
let selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this._peek.type === 2 /* TokenType.TAG_OPEN_END_VOID */) {
this._advance();
selfClosing = true;
const tagDef = this.getTagDefinition(fullName);
if (!(tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) {
this.errors.push(TreeError.create(fullName, startTagToken.sourceSpan, `Only void, custom and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
}
else if (this._peek.type === 1 /* TokenType.TAG_OPEN_END */) {
this._advance();
selfClosing = false;
}
const end = this._peek.sourceSpan.fullStart;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end, startTagToken.sourceSpan.fullStart);
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
const startSpan = new ParseSourceSpan(startTagToken.sourceSpan.start, end, startTagToken.sourceSpan.fullStart);
const el = new Element(fullName, attrs, [], span, startSpan, undefined);
const parentEl = this._getContainer();
this._pushContainer(el, parentEl instanceof Element &&
this.getTagDefinition(parentEl.name).isClosedByChild(el.name));
if (selfClosing) {
// Elements that are self-closed have their `endSourceSpan` set to the full span, as the
// element start tag also represents the end tag.
this._popContainer(fullName, Element, span);
}
else if (startTagToken.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) {
// We already know the opening tag is not complete, so it is unlikely it has a corresponding
// close tag. Let's optimistically parse it as a full element and emit an error.
this._popContainer(fullName, Element, null);
this.errors.push(TreeError.create(fullName, span, `Opening tag "${fullName}" not terminated.`));
}
}
_pushContainer(node, isClosedByChild) {
if (isClosedByChild) {
this._containerStack.pop();
}
this._addToParent(node);
this._containerStack.push(node);
}
_consumeEndTag(endTagToken) {
const fullName = this._getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getClosestParentElement());
if (this.getTagDefinition(fullName).isVoid) {
this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, `Void elements do not have end tags "${endTagToken.parts[1]}"`));
}
else if (!this._popContainer(fullName, Element, endTagToken.sourceSpan)) {
const errMsg = `Unexpected closing tag "${fullName}". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags`;
this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, errMsg));
}
}
/**
* Closes the nearest element with the tag name `fullName` in the parse tree.
* `endSourceSpan` is the span of the closing tag, or null if the element does
* not have a closing tag (for example, this happens when an incomplete
* opening tag is recovered).
*/
_popContainer(expectedName, expectedType, endSourceSpan) {
let unexpectedCloseTagDetected = false;
for (let stackIndex = this._containerStack.length - 1; stackIndex >= 0; stackIndex--) {
const node = this._containerStack[stackIndex];
if ((node.name === expectedName || expectedName === null) && node instanceof expectedType) {
// Record the parse span with the element that is being closed. Any elements that are
// removed from the element stack at this point are closed implicitly, so they won't get
// an end source span (as there is no explicit closing element).
node.endSourceSpan = endSourceSpan;
node.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : node.sourceSpan.end;
this._containerStack.splice(stackIndex, this._containerStack.length - stackIndex);
return !unexpectedCloseTagDetected;
}
// Blocks and most elements are not self closing.
if (node instanceof Block ||
node instanceof Element && !this.getTagDefinition(node.name).closedByParent) {
// Note that we encountered an unexpected close tag but continue processing the element
// stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this
// end tag in the stack.
unexpectedCloseTagDetected = true;
}
}
return false;
}
_consumeAttr(attrName) {
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
let attrEnd = attrName.sourceSpan.end;
// Consume any quote
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) {
this._advance();
}
// Consume the attribute value
let value = '';
const valueTokens = [];
let valueStartSpan = undefined;
let valueEnd = undefined;
// NOTE: We need to use a new variable `nextTokenType` here to hide the actual type of
// `_peek.type` from TS. Otherwise TS will narrow the type of `_peek.type` preventing it from
// being able to consider `ATTR_VALUE_INTERPOLATION` as an option. This is because TS is not
// able to see that `_advance()` will actually mutate `_peek`.
const nextTokenType = this._peek.type;
if (nextTokenType === 16 /* TokenType.ATTR_VALUE_TEXT */) {
valueStartSpan = this._peek.sourceSpan;
valueEnd = this._peek.sourceSpan.end;
while (this._peek.type === 16 /* TokenType.ATTR_VALUE_TEXT */ ||
this._peek.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */ ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */) {
const valueToken = this._advance();
valueTokens.push(valueToken);
if (valueToken.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */) {
// For backward compatibility we decode HTML entities that appear in interpolation
// expressions. This is arguably a bug, but it could be a considerable breaking change to
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer
// chain after View Engine has been removed.
value += valueToken.parts.join('').replace(/&([^;]+);/g, decodeEntity);
}
else if (valueToken.type === 9 /* TokenType.ENCODED_ENTITY */) {
value += valueToken.parts[0];
}
else {
value += valueToken.parts.join('');
}
valueEnd = attrEnd = valueToken.sourceSpan.end;
}
}
// Consume any quote
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) {
const quoteToken = this._advance();
attrEnd = quoteToken.sourceSpan.end;
}
const valueSpan = valueStartSpan && valueEnd &&
new ParseSourceSpan(valueStartSpan.start, valueEnd, valueStartSpan.fullStart);
return new Attribute(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, attrEnd, attrName.sourceSpan.fullStart), attrName.sourceSpan, valueSpan, valueTokens.length > 0 ? valueTokens : undefined, undefined);
}
_consumeBlockOpen(token) {
const parameters = [];
while (this._peek.type === 27 /* TokenType.BLOCK_PARAMETER */) {
const paramToken = this._advance();
parameters.push(new BlockParameter(paramToken.parts[0], paramToken.sourceSpan));
}
if (this._peek.type === 25 /* TokenType.BLOCK_OPEN_END */) {
this._advance();
}
const end = this._peek.sourceSpan.fullStart;
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
const block = new Block(token.parts[0], parameters, [], span, token.sourceSpan, startSpan);
this._pushContainer(block, false);
}
_consumeBlockClose(token) {
if (!this._popContainer(null, Block, token.sourceSpan)) {
this.errors.push(TreeError.create(null, token.sourceSpan, `Unexpected closing block. The block may have been closed earlier. ` +
`If you meant to write the } character, you should use the "}" ` +
`HTML entity instead.`));
}
}
_consumeIncompleteBlock(token) {
const parameters = [];
while (this._peek.type === 27 /* TokenType.BLOCK_PARAMETER */) {
const paramToken = this._advance();
parameters.push(new BlockParameter(paramToken.parts[0], paramToken.sourceSpan));
}
const end = this._peek.sourceSpan.fullStart;
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
const block = new Block(token.parts[0], parameters, [], span, token.sourceSpan, startSpan);
this._pushContainer(block, false);
// Incomplete blocks don't have children so we close them immediately and report an error.
this._popContainer(null, Block, null);
this.errors.push(TreeError.create(token.parts[0], span, `Incomplete block "${token.parts[0]}". If you meant to write the @ character, ` +
`you should use the "@" HTML entity instead.`));
}
_getContainer() {
return this._containerStack.length > 0 ? this._containerStack[this._containerStack.length - 1] :
null;
}
_getClosestParentElement() {
for (let i = this._containerStack.length - 1; i > -1; i--) {
if (this._containerStack[i] instanceof Element) {
return this._containerStack[i];
}
}
return null;
}
_addToParent(node) {
const parent = this._getContainer();
if (parent === null) {
this.rootNodes.push(node);
}
else {
parent.children.push(node);
}
}
_getElementFullName(prefix, localName, parentElement) {
if (prefix === '') {
prefix = this.getTagDefinition(localName).implicitNamespacePrefix || '';
if (prefix === '' && parentElement != null) {
const parentTagName = splitNsName(parentElement.name)[1];
const parentTagDefinition = this.getTagDefinition(parentTagName);
if (!parentTagDefinition.preventNamespaceInheritance) {
prefix = getNsPrefix(parentElement.name);
}
}
}
return mergeNsAndName(prefix, localName);
}
}
function lastOnStack(stack, element) {
return stack.length > 0 && stack[stack.length - 1] === element;
}
/**
* Decode the `entity` string, which we believe is the contents of an HTML entity.
*
* If the string is not actually a valid/known entity then just return the original `match` string.
*/
function decodeEntity(match, entity) {
if (NAMED_ENTITIES[entity] !== undefined) {
return NAMED_ENTITIES[entity] || match;
}
if (/^#x[a-f0-9]+$/i.test(entity)) {
return String.fromCodePoint(parseInt(entity.slice(2), 16));
}
if (/^#\d+$/.test(entity)) {
return String.fromCodePoint(parseInt(entity.slice(1), 10));
}
return match;
}
/**
* Set of tagName|propertyName corresponding to Trusted Types sinks. Properties applying to all
* tags use '*'.
*
* Extracted from, and should be kept in sync with
* https://w3c.github.io/webappsec-trusted-types/dist/spec/#integrations
*/
const TRUSTED_TYPES_SINKS = new Set([
// NOTE: All strings in this set *must* be lowercase!
// TrustedHTML
'iframe|srcdoc',
'*|innerhtml',
'*|outerhtml',
// NB: no TrustedScript here, as the corresponding tags are stripped by the compiler.
// TrustedScriptURL
'embed|src',
'object|codebase',
'object|data',
]);
/**
* isTrustedTypesSink returns true if the given property on the given DOM tag is a Trusted Types
* sink. In that case, use `ElementSchemaRegistry.securityContext` to determine which particular
* Trusted Type is required for values passed to the sink:
* - SecurityContext.HTML corresponds to TrustedHTML
* - SecurityContext.RESOURCE_URL corresponds to TrustedScriptURL
*/
function isTrustedTypesSink(tagName, propName) {
// Make sure comparisons are case insensitive, so that case differences between attribute and
// property names do not have a security impact.
tagName = tagName.toLowerCase();
propName = propName.toLowerCase();
return TRUSTED_TYPES_SINKS.has(tagName + '|' + propName) ||
TRUSTED_TYPES_SINKS.has('*|' + propName);
}
const setI18nRefs = (htmlNode, i18nNode) => {
if (htmlNode instanceof NodeWithI18n) {
if (i18nNode instanceof IcuPlaceholder && htmlNode.i18n instanceof Message) {
// This html node represents an ICU but this is a second processing pass, and the legacy id
// was computed in the previous pass and stored in the `i18n` property as a message.
// We are about to wipe out that property so capture the previous message to be reused when
// generating the message for this ICU later. See `_generateI18nMessage()`.
i18nNode.previousMessage = htmlNode.i18n;
}
htmlNode.i18n = i18nNode;
}
return i18nNode;
};
/**
* This visitor walks over HTML parse tree and converts information stored in
* i18n-related attributes ("i18n" and "i18n-*") into i18n meta object that is
* stored with other element's and attribute's information.
*/
class I18nMetaVisitor {
constructor(interpolationConfig = DEFAULT_INTERPOLATION_CONFIG, keepI18nAttrs = false, enableI18nLegacyMessageIdFormat = false, containerBlocks = DEFAULT_CONTAINER_BLOCKS) {
this.interpolationConfig = interpolationConfig;
this.keepI18nAttrs = keepI18nAttrs;
this.enableI18nLegacyMessageIdFormat = enableI18nLegacyMessageIdFormat;
this.containerBlocks = containerBlocks;
// whether visited nodes contain i18n information
this.hasI18nMeta = false;
this._errors = [];
}
_generateI18nMessage(nodes, meta = '', visitNodeFn) {
const { meaning, description, customId } = this._parseMetadata(meta);
const createI18nMessage = createI18nMessageFactory(this.interpolationConfig, this.containerBlocks);
const message = createI18nMessage(nodes, meaning, description, customId, visitNodeFn);
this._setMessageId(message, meta);
this._setLegacyIds(message, meta);
return message;
}
visitAllWithErrors(nodes) {
const result = nodes.map(node => node.visit(this, null));
return new ParseTreeResult(result, this._errors);
}
visitElement(element) {
let message = undefined;
if (hasI18nAttrs(element)) {
this.hasI18nMeta = true;
const attrs = [];
const attrsMeta = {};
for (const attr of element.attrs) {
if (attr.name === I18N_ATTR) {
// root 'i18n' node attribute
const i18n = element.i18n || attr.value;
message = this._generateI18nMessage(element.children, i18n, setI18nRefs);
if (message.nodes.length === 0) {
// Ignore the message if it is empty.
message = undefined;
}
// Store the message on the element
element.i18n = message;
}
else if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
// 'i18n-*' attributes
const name = attr.name.slice(I18N_ATTR_PREFIX.length);
if (isTrustedTypesSink(element.name, name)) {
this._reportError(attr, `Translating attribute '${name}' is disallowed for security reasons.`);
}
else {
attrsMeta[name] = attr.value;
}
}
else {
// non-i18n attributes
attrs.push(attr);
}
}
// set i18n meta for attributes
if (Object.keys(attrsMeta).length) {
for (const attr of attrs) {
const meta = attrsMeta[attr.name];
// do not create translation for empty attributes
if (meta !== undefined && attr.value) {
attr.i18n = this._generateI18nMessage([attr], attr.i18n || meta);
}
}
}
if (!this.keepI18nAttrs) {
// update element's attributes,
// keeping only non-i18n related ones
element.attrs = attrs;
}
}
visitAll(this, element.children, message);
return element;
}
visitExpansion(expansion, currentMessage) {
let message;
const meta = expansion.i18n;
this.hasI18nMeta = true;
if (meta instanceof IcuPlaceholder) {
// set ICU placeholder name (e.g. "ICU_1"),
// generated while processing root element contents,
// so we can reference it when we output translation
const name = meta.name;
message = this._generateI18nMessage([expansion], meta);
const icu = icuFromI18nMessage(message);
icu.name = name;
if (currentMessage !== null) {
// Also update the placeholderToMessage map with this new message
currentMessage.placeholderToMessage[name] = message;
}
}
else {
// ICU is a top level message, try to use metadata from container element if provided via
// `context` argument. Note: context may not be available for standalone ICUs (without
// wrapping element), so fallback to ICU metadata in this case.
message = this._generateI18nMessage([expansion], currentMessage || meta);
}
expansion.i18n = message;
return expansion;
}
visitText(text) {
return text;
}
visitAttribute(attribute) {
return attribute;
}
visitComment(comment) {
return comment;
}
visitExpansionCase(expansionCase) {
return expansionCase;
}
visitBlock(block, context) {
visitAll(this, block.children, context);
return block;
}
visitBlockParameter(parameter, context) {
return parameter;
}
/**
* Parse the general form `meta` passed into extract the explicit metadata needed to create a
* `Message`.
*
* There are three possibilities for the `meta` variable
* 1) a string from an `i18n` template attribute: parse it to extract the metadata values.
* 2) a `Message` from a previous processing pass: reuse the metadata values in the message.
* 4) other: ignore this and just process the message metadata as normal
*
* @param meta the bucket that holds information about the message
* @returns the parsed metadata.
*/
_parseMetadata(meta) {
return typeof meta === 'string' ? parseI18nMeta(meta) :
meta instanceof Message ? meta :
{};
}
/**
* Generate (or restore) message id if not specified already.
*/
_setMessageId(message, meta) {
if (!message.id) {
message.id = meta instanceof Message && meta.id || decimalDigest(message);
}
}
/**
* Update the `message` with a `legacyId` if necessary.
*
* @param message the message whose legacy id should be set
* @param meta information about the message being processed
*/
_setLegacyIds(message, meta) {
if (this.enableI18nLegacyMessageIdFormat) {
message.legacyIds = [computeDigest(message), computeDecimalDigest(message)];
}
else if (typeof meta !== 'string') {
// This occurs if we are doing the 2nd pass after whitespace removal (see `parseTemplate()` in
// `packages/compiler/src/render3/view/template.ts`).
// In that case we want to reuse the legacy message generated in the 1st pass (see
// `setI18nRefs()`).
const previousMessage = meta instanceof Message ? meta :
meta instanceof IcuPlaceholder ? meta.previousMessage :
undefined;
message.legacyIds = previousMessage ? previousMessage.legacyIds : [];
}
}
_reportError(node, msg) {
this._errors.push(new I18nError(node.sourceSpan, msg));
}
}
/** I18n separators for metadata **/
const I18N_MEANING_SEPARATOR = '|';
const I18N_ID_SEPARATOR = '@@';
/**
* Parses i18n metas like:
* - "@@id",
* - "description[@@id]",
* - "meaning|description[@@id]"
* and returns an object with parsed output.
*
* @param meta String that represents i18n meta
* @returns Object with id, meaning and description fields
*/
function parseI18nMeta(meta = '') {
let customId;
let meaning;
let description;
meta = meta.trim();
if (meta) {
const idIndex = meta.indexOf(I18N_ID_SEPARATOR);
const descIndex = meta.indexOf(I18N_MEANING_SEPARATOR);
let meaningAndDesc;
[meaningAndDesc, customId] =
(idIndex > -1) ? [meta.slice(0, idIndex), meta.slice(idIndex + 2)] : [meta, ''];
[meaning, description] = (descIndex > -1) ?
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
['', meaningAndDesc];
}
return { customId, meaning, description };
}
// Converts i18n meta information for a message (id, description, meaning)
// to a JsDoc statement formatted as expected by the Closure compiler.
function i18nMetaToJSDoc(meta) {
const tags = [];
if (meta.description) {
tags.push({ tagName: "desc" /* o.JSDocTagName.Desc */, text: meta.description });
}
else {
// Suppress the JSCompiler warning that a `@desc` was not given for this message.
tags.push({ tagName: "suppress" /* o.JSDocTagName.Suppress */, text: '{msgDescriptions}' });
}
if (meta.meaning) {
tags.push({ tagName: "meaning" /* o.JSDocTagName.Meaning */, text: meta.meaning });
}
return jsDocComment(tags);
}
/** Closure uses `goog.getMsg(message)` to lookup translations */
const GOOG_GET_MSG = 'goog.getMsg';
/**
* Generates a `goog.getMsg()` statement and reassignment. The template:
*
* ```html
* Sent from {{ sender }} to {{ receiver }}
* ```
*
* Generates:
*
* ```typescript
* const MSG_FOO = goog.getMsg(
* // Message template.
* 'Sent from {$interpolation} to {$startTagSpan}{$interpolation_1}{$closeTagSpan}.',
* // Placeholder values, set to magic strings which get replaced by the Angular runtime.
* {
* 'interpolation': '\uFFFD0\uFFFD',
* 'startTagSpan': '\uFFFD1\uFFFD',
* 'interpolation_1': '\uFFFD2\uFFFD',
* 'closeTagSpan': '\uFFFD3\uFFFD',
* },
* // Options bag.
* {
* // Maps each placeholder to the original Angular source code which generates it's value.
* original_code: {
* 'interpolation': '{{ sender }}',
* 'startTagSpan': '',
* 'interpolation_1': '{{ receiver }}',
* 'closeTagSpan': '',
* },
* },
* );
* const I18N_0 = MSG_FOO;
* ```
*/
function createGoogleGetMsgStatements(variable$1, message, closureVar, placeholderValues) {
const messageString = serializeI18nMessageForGetMsg(message);
const args = [literal(messageString)];
if (Object.keys(placeholderValues).length) {
// Message template parameters containing the magic strings replaced by the Angular runtime with
// real data, e.g. `{'interpolation': '\uFFFD0\uFFFD'}`.
args.push(mapLiteral(formatI18nPlaceholderNamesInMap(placeholderValues, true /* useCamelCase */), true /* quoted */));
// Message options object, which contains original source code for placeholders (as they are
// present in a template, e.g.
// `{original_code: {'interpolation': '{{ name }}', 'startTagSpan': ''}}`.
args.push(mapLiteral({
original_code: literalMap(Object.keys(placeholderValues)
.map((param) => ({
key: formatI18nPlaceholderName(param),
quoted: true,
value: message.placeholders[param] ?
// Get source span for typical placeholder if it exists.
literal(message.placeholders[param].sourceSpan.toString()) :
// Otherwise must be an ICU expression, get it's source span.
literal(message.placeholderToMessage[param]
.nodes.map((node) => node.sourceSpan.toString())
.join('')),
}))),
}));
}
// /**
// * @desc description of message
// * @meaning meaning of message
// */
// const MSG_... = goog.getMsg(..);
// I18N_X = MSG_...;
const googGetMsgStmt = closureVar.set(variable(GOOG_GET_MSG).callFn(args)).toConstDecl();
googGetMsgStmt.addLeadingComment(i18nMetaToJSDoc(message));
const i18nAssignmentStmt = new ExpressionStatement(variable$1.set(closureVar));
return [googGetMsgStmt, i18nAssignmentStmt];
}
/**
* This visitor walks over i18n tree and generates its string representation, including ICUs and
* placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format.
*/
class GetMsgSerializerVisitor {
formatPh(value) {
return `{$${formatI18nPlaceholderName(value)}}`;
}
visitText(text) {
return text.value;
}
visitContainer(container) {
return container.children.map(child => child.visit(this)).join('');
}
visitIcu(icu) {
return serializeIcuNode(icu);
}
visitTagPlaceholder(ph) {
return ph.isVoid ?
this.formatPh(ph.startName) :
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
}
visitPlaceholder(ph) {
return this.formatPh(ph.name);
}
visitBlockPlaceholder(ph) {
return `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
}
visitIcuPlaceholder(ph, context) {
return this.formatPh(ph.name);
}
}
const serializerVisitor = new GetMsgSerializerVisitor();
function serializeI18nMessageForGetMsg(message) {
return message.nodes.map(node => node.visit(serializerVisitor, null)).join('');
}
function createLocalizeStatements(variable, message, params) {
const { messageParts, placeHolders } = serializeI18nMessageForLocalize(message);
const sourceSpan = getSourceSpan(message);
const expressions = placeHolders.map(ph => params[ph.text]);
const localizedString$1 = localizedString(message, messageParts, placeHolders, expressions, sourceSpan);
const variableInitialization = variable.set(localizedString$1);
return [new ExpressionStatement(variableInitialization)];
}
/**
* This visitor walks over an i18n tree, capturing literal strings and placeholders.
*
* The result can be used for generating the `$localize` tagged template literals.
*/
class LocalizeSerializerVisitor {
constructor(placeholderToMessage, pieces) {
this.placeholderToMessage = placeholderToMessage;
this.pieces = pieces;
}
visitText(text) {
if (this.pieces[this.pieces.length - 1] instanceof LiteralPiece) {
// Two literal pieces in a row means that there was some comment node in-between.
this.pieces[this.pieces.length - 1].text += text.value;
}
else {
const sourceSpan = new ParseSourceSpan(text.sourceSpan.fullStart, text.sourceSpan.end, text.sourceSpan.fullStart, text.sourceSpan.details);
this.pieces.push(new LiteralPiece(text.value, sourceSpan));
}
}
visitContainer(container) {
container.children.forEach(child => child.visit(this));
}
visitIcu(icu) {
this.pieces.push(new LiteralPiece(serializeIcuNode(icu), icu.sourceSpan));
}
visitTagPlaceholder(ph) {
this.pieces.push(this.createPlaceholderPiece(ph.startName, ph.startSourceSpan ?? ph.sourceSpan));
if (!ph.isVoid) {
ph.children.forEach(child => child.visit(this));
this.pieces.push(this.createPlaceholderPiece(ph.closeName, ph.endSourceSpan ?? ph.sourceSpan));
}
}
visitPlaceholder(ph) {
this.pieces.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
visitBlockPlaceholder(ph) {
this.pieces.push(this.createPlaceholderPiece(ph.startName, ph.startSourceSpan ?? ph.sourceSpan));
ph.children.forEach(child => child.visit(this));
this.pieces.push(this.createPlaceholderPiece(ph.closeName, ph.endSourceSpan ?? ph.sourceSpan));
}
visitIcuPlaceholder(ph) {
this.pieces.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan, this.placeholderToMessage[ph.name]));
}
createPlaceholderPiece(name, sourceSpan, associatedMessage) {
return new PlaceholderPiece(formatI18nPlaceholderName(name, /* useCamelCase */ false), sourceSpan, associatedMessage);
}
}
/**
* Serialize an i18n message into two arrays: messageParts and placeholders.
*
* These arrays will be used to generate `$localize` tagged template literals.
*
* @param message The message to be serialized.
* @returns an object containing the messageParts and placeholders.
*/
function serializeI18nMessageForLocalize(message) {
const pieces = [];
const serializerVisitor = new LocalizeSerializerVisitor(message.placeholderToMessage, pieces);
message.nodes.forEach(node => node.visit(serializerVisitor));
return processMessagePieces(pieces);
}
function getSourceSpan(message) {
const startNode = message.nodes[0];
const endNode = message.nodes[message.nodes.length - 1];
return new ParseSourceSpan(startNode.sourceSpan.fullStart, endNode.sourceSpan.end, startNode.sourceSpan.fullStart, startNode.sourceSpan.details);
}
/**
* Convert the list of serialized MessagePieces into two arrays.
*
* One contains the literal string pieces and the other the placeholders that will be replaced by
* expressions when rendering `$localize` tagged template literals.
*
* @param pieces The pieces to process.
* @returns an object containing the messageParts and placeholders.
*/
function processMessagePieces(pieces) {
const messageParts = [];
const placeHolders = [];
if (pieces[0] instanceof PlaceholderPiece) {
// The first piece was a placeholder so we need to add an initial empty message part.
messageParts.push(createEmptyMessagePart(pieces[0].sourceSpan.start));
}
for (let i = 0; i < pieces.length; i++) {
const part = pieces[i];
if (part instanceof LiteralPiece) {
messageParts.push(part);
}
else {
placeHolders.push(part);
if (pieces[i - 1] instanceof PlaceholderPiece) {
// There were two placeholders in a row, so we need to add an empty message part.
messageParts.push(createEmptyMessagePart(pieces[i - 1].sourceSpan.end));
}
}
}
if (pieces[pieces.length - 1] instanceof PlaceholderPiece) {
// The last piece was a placeholder so we need to add a final empty message part.
messageParts.push(createEmptyMessagePart(pieces[pieces.length - 1].sourceSpan.end));
}
return { messageParts, placeHolders };
}
function createEmptyMessagePart(location) {
return new LiteralPiece('', new ParseSourceSpan(location, location));
}
/** Name of the global variable that is used to determine if we use Closure translations or not */
const NG_I18N_CLOSURE_MODE$1 = 'ngI18nClosureMode';
/**
* Prefix for non-`goog.getMsg` i18n-related vars.
* Note: the prefix uses lowercase characters intentionally due to a Closure behavior that
* considers variables like `I18N_0` as constants and throws an error when their value changes.
*/
const TRANSLATION_VAR_PREFIX = 'i18n_';
/** Prefix of ICU expressions for post processing */
const I18N_ICU_MAPPING_PREFIX = 'I18N_EXP_';
/**
* The escape sequence used for message param values.
*/
const ESCAPE = '\uFFFD';
/**
* Lifts i18n properties into the consts array.
* TODO: Can we use `ConstCollectedExpr`?
* TODO: The way the various attributes are linked together is very complex. Perhaps we could
* simplify the process, maybe by combining the context and message ops?
*/
function collectI18nConsts(job) {
const fileBasedI18nSuffix = job.relativeContextFilePath.replace(/[^A-Za-z0-9]/g, '_').toUpperCase() + '_';
// Step One: Build up various lookup maps we need to collect all the consts.
// Context Xref -> Extracted Attribute Ops
const extractedAttributesByI18nContext = new Map();
// Element/ElementStart Xref -> I18n Attributes config op
const i18nAttributesByElement = new Map();
// Element/ElementStart Xref -> All I18n Expression ops for attrs on that target
const i18nExpressionsByElement = new Map();
// I18n Message Xref -> I18n Message Op (TODO: use a central op map)
const messages = new Map();
for (const unit of job.units) {
for (const op of unit.ops()) {
if (op.kind === OpKind.ExtractedAttribute && op.i18nContext !== null) {
const attributes = extractedAttributesByI18nContext.get(op.i18nContext) ?? [];
attributes.push(op);
extractedAttributesByI18nContext.set(op.i18nContext, attributes);
}
else if (op.kind === OpKind.I18nAttributes) {
i18nAttributesByElement.set(op.target, op);
}
else if (op.kind === OpKind.I18nExpression && op.usage === I18nExpressionFor.I18nAttribute) {
const expressions = i18nExpressionsByElement.get(op.target) ?? [];
expressions.push(op);
i18nExpressionsByElement.set(op.target, expressions);
}
else if (op.kind === OpKind.I18nMessage) {
messages.set(op.xref, op);
}
}
}
// Step Two: Serialize the extracted i18n messages for root i18n blocks and i18n attributes into
// the const array.
//
// Also, each i18n message will have a variable expression that can refer to its
// value. Store these expressions in the appropriate place:
// 1. For normal i18n content, it also goes in the const array. We save the const index to use
// later.
// 2. For extracted attributes, it becomes the value of the extracted attribute instruction.
// 3. For i18n bindings, it will go in a separate const array instruction below; for now, we just
// save it.
const i18nValuesByContext = new Map();
const messageConstIndices = new Map();
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind === OpKind.I18nMessage) {
if (op.messagePlaceholder === null) {
const { mainVar, statements } = collectMessage(job, fileBasedI18nSuffix, messages, op);
if (op.i18nBlock !== null) {
// This is a regular i18n message with a corresponding i18n block. Collect it into the
// const array.
const i18nConst = job.addConst(mainVar, statements);
messageConstIndices.set(op.i18nBlock, i18nConst);
}
else {
// This is an i18n attribute. Extract the initializers into the const pool.
job.constsInitializers.push(...statements);
// Save the i18n variable value for later.
i18nValuesByContext.set(op.i18nContext, mainVar);
// This i18n message may correspond to an individual extracted attribute. If so, The
// value of that attribute is updated to read the extracted i18n variable.
const attributesForMessage = extractedAttributesByI18nContext.get(op.i18nContext);
if (attributesForMessage !== undefined) {
for (const attr of attributesForMessage) {
attr.expression = mainVar.clone();
}
}
}
}
OpList.remove(op);
}
}
}
// Step Three: Serialize I18nAttributes configurations into the const array. Each I18nAttributes
// instruction has a config array, which contains k-v pairs describing each binding name, and the
// i18n variable that provides the value.
for (const unit of job.units) {
for (const elem of unit.create) {
if (isElementOrContainerOp(elem)) {
const i18nAttributes = i18nAttributesByElement.get(elem.xref);
if (i18nAttributes === undefined) {
// This element is not associated with an i18n attributes configuration instruction.
continue;
}
let i18nExpressions = i18nExpressionsByElement.get(elem.xref);
if (i18nExpressions === undefined) {
// Unused i18nAttributes should have already been removed.
// TODO: Should the removal of those dead instructions be merged with this phase?
throw new Error('AssertionError: Could not find any i18n expressions associated with an I18nAttributes instruction');
}
// Find expressions for all the unique property names, removing duplicates.
const seenPropertyNames = new Set();
i18nExpressions = i18nExpressions.filter(i18nExpr => {
const seen = (seenPropertyNames.has(i18nExpr.name));
seenPropertyNames.add(i18nExpr.name);
return !seen;
});
const i18nAttributeConfig = i18nExpressions.flatMap(i18nExpr => {
const i18nExprValue = i18nValuesByContext.get(i18nExpr.context);
if (i18nExprValue === undefined) {
throw new Error('AssertionError: Could not find i18n expression\'s value');
}
return [literal(i18nExpr.name), i18nExprValue];
});
i18nAttributes.i18nAttributesConfig =
job.addConst(new LiteralArrayExpr(i18nAttributeConfig));
}
}
}
// Step Four: Propagate the extracted const index into i18n ops that messages were extracted from.
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind === OpKind.I18nStart) {
const msgIndex = messageConstIndices.get(op.root);
if (msgIndex === undefined) {
throw new Error('AssertionError: Could not find corresponding i18n block index for an i18n message op; was an i18n message incorrectly assumed to correspond to an attribute?');
}
op.messageIndex = msgIndex;
}
}
}
}
/**
* Collects the given message into a set of statements that can be added to the const array.
* This will recursively collect any sub-messages referenced from the parent message as well.
*/
function collectMessage(job, fileBasedI18nSuffix, messages, messageOp) {
// Recursively collect any sub-messages, record each sub-message's main variable under its
// placeholder so that we can add them to the params for the parent message. It is possible
// that multiple sub-messages will share the same placeholder, so we need to track an array of
// variables for each placeholder.
const statements = [];
const subMessagePlaceholders = new Map();
for (const subMessageId of messageOp.subMessages) {
const subMessage = messages.get(subMessageId);
const { mainVar: subMessageVar, statements: subMessageStatements } = collectMessage(job, fileBasedI18nSuffix, messages, subMessage);
statements.push(...subMessageStatements);
const subMessages = subMessagePlaceholders.get(subMessage.messagePlaceholder) ?? [];
subMessages.push(subMessageVar);
subMessagePlaceholders.set(subMessage.messagePlaceholder, subMessages);
}
addSubMessageParams(messageOp, subMessagePlaceholders);
// Sort the params for consistency with TemaplateDefinitionBuilder output.
messageOp.params = new Map([...messageOp.params.entries()].sort());
const mainVar = variable(job.pool.uniqueName(TRANSLATION_VAR_PREFIX));
// Closure Compiler requires const names to start with `MSG_` but disallows any other
// const to start with `MSG_`. We define a variable starting with `MSG_` just for the
// `goog.getMsg` call
const closureVar = i18nGenerateClosureVar(job.pool, messageOp.message.id, fileBasedI18nSuffix, job.i18nUseExternalIds);
let transformFn = undefined;
// If nescessary, add a post-processing step and resolve any placeholder params that are
// set in post-processing.
if (messageOp.needsPostprocessing || messageOp.postprocessingParams.size > 0) {
// Sort the post-processing params for consistency with TemaplateDefinitionBuilder output.
const postprocessingParams = Object.fromEntries([...messageOp.postprocessingParams.entries()].sort());
const formattedPostprocessingParams = formatI18nPlaceholderNamesInMap(postprocessingParams, /* useCamelCase */ false);
const extraTransformFnParams = [];
if (messageOp.postprocessingParams.size > 0) {
extraTransformFnParams.push(mapLiteral(formattedPostprocessingParams, /* quoted */ true));
}
transformFn = (expr) => importExpr(Identifiers.i18nPostprocess).callFn([expr, ...extraTransformFnParams]);
}
// Add the message's statements
statements.push(...getTranslationDeclStmts$1(messageOp.message, mainVar, closureVar, messageOp.params, transformFn));
return { mainVar, statements };
}
/**
* Adds the given subMessage placeholders to the given message op.
*
* If a placeholder only corresponds to a single sub-message variable, we just set that variable
* as the param value. However, if the placeholder corresponds to multiple sub-message
* variables, we need to add a special placeholder value that is handled by the post-processing
* step. We then add the array of variables as a post-processing param.
*/
function addSubMessageParams(messageOp, subMessagePlaceholders) {
for (const [placeholder, subMessages] of subMessagePlaceholders) {
if (subMessages.length === 1) {
messageOp.params.set(placeholder, subMessages[0]);
}
else {
messageOp.params.set(placeholder, literal(`${ESCAPE}${I18N_ICU_MAPPING_PREFIX}${placeholder}${ESCAPE}`));
messageOp.postprocessingParams.set(placeholder, literalArr(subMessages));
}
}
}
/**
* Generate statements that define a given translation message.
*
* ```
* var I18N_1;
* if (typeof ngI18nClosureMode !== undefined && ngI18nClosureMode) {
* var MSG_EXTERNAL_XXX = goog.getMsg(
* "Some message with {$interpolation}!",
* { "interpolation": "\uFFFD0\uFFFD" }
* );
* I18N_1 = MSG_EXTERNAL_XXX;
* }
* else {
* I18N_1 = $localize`Some message with ${'\uFFFD0\uFFFD'}!`;
* }
* ```
*
* @param message The original i18n AST message node
* @param variable The variable that will be assigned the translation, e.g. `I18N_1`.
* @param closureVar The variable for Closure `goog.getMsg` calls, e.g. `MSG_EXTERNAL_XXX`.
* @param params Object mapping placeholder names to their values (e.g.
* `{ "interpolation": "\uFFFD0\uFFFD" }`).
* @param transformFn Optional transformation function that will be applied to the translation
* (e.g.
* post-processing).
* @returns An array of statements that defined a given translation.
*/
function getTranslationDeclStmts$1(message, variable, closureVar, params, transformFn) {
const paramsObject = Object.fromEntries(params);
const statements = [
declareI18nVariable(variable),
ifStmt(createClosureModeGuard$1(), createGoogleGetMsgStatements(variable, message, closureVar, paramsObject), createLocalizeStatements(variable, message, formatI18nPlaceholderNamesInMap(paramsObject, /* useCamelCase */ false))),
];
if (transformFn) {
statements.push(new ExpressionStatement(variable.set(transformFn(variable))));
}
return statements;
}
/**
* Create the expression that will be used to guard the closure mode block
* It is equivalent to:
*
* ```
* typeof ngI18nClosureMode !== undefined && ngI18nClosureMode
* ```
*/
function createClosureModeGuard$1() {
return typeofExpr(variable(NG_I18N_CLOSURE_MODE$1))
.notIdentical(literal('undefined', STRING_TYPE))
.and(variable(NG_I18N_CLOSURE_MODE$1));
}
/**
* Generates vars with Closure-specific names for i18n blocks (i.e. `MSG_XXX`).
*/
function i18nGenerateClosureVar(pool, messageId, fileBasedI18nSuffix, useExternalIds) {
let name;
const suffix = fileBasedI18nSuffix;
if (useExternalIds) {
const prefix = getTranslationConstPrefix(`EXTERNAL_`);
const uniqueSuffix = pool.uniqueName(suffix);
name = `${prefix}${sanitizeIdentifier(messageId)}$$${uniqueSuffix}`;
}
else {
const prefix = getTranslationConstPrefix(suffix);
name = pool.uniqueName(prefix);
}
return variable(name);
}
/**
* Removes text nodes within i18n blocks since they are already hardcoded into the i18n message.
* Also, replaces interpolations on these text nodes with i18n expressions of the non-text portions,
* which will be applied later.
*/
function convertI18nText(job) {
for (const unit of job.units) {
// Remove all text nodes within i18n blocks, their content is already captured in the i18n
// message.
let currentI18n = null;
let currentIcu = null;
const textNodeI18nBlocks = new Map();
const textNodeIcus = new Map();
const icuPlaceholderByText = new Map();
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nStart:
if (op.context === null) {
throw Error('I18n op should have its context set.');
}
currentI18n = op;
break;
case OpKind.I18nEnd:
currentI18n = null;
break;
case OpKind.IcuStart:
if (op.context === null) {
throw Error('Icu op should have its context set.');
}
currentIcu = op;
break;
case OpKind.IcuEnd:
currentIcu = null;
break;
case OpKind.Text:
if (currentI18n !== null) {
textNodeI18nBlocks.set(op.xref, currentI18n);
textNodeIcus.set(op.xref, currentIcu);
if (op.icuPlaceholder !== null) {
// Create an op to represent the ICU placeholder. Initially set its static text to the
// value of the text op, though this may be overwritten later if this text op is a
// placeholder for an interpolation.
const icuPlaceholderOp = createIcuPlaceholderOp(job.allocateXrefId(), op.icuPlaceholder, [op.initialValue]);
OpList.replace(op, icuPlaceholderOp);
icuPlaceholderByText.set(op.xref, icuPlaceholderOp);
}
else {
// Otherwise just remove the text op, since its value is already accounted for in the
// translated message.
OpList.remove(op);
}
}
break;
}
}
// Update any interpolations to the removed text, and instead represent them as a series of i18n
// expressions that we then apply.
for (const op of unit.update) {
switch (op.kind) {
case OpKind.InterpolateText:
if (!textNodeI18nBlocks.has(op.target)) {
continue;
}
const i18nOp = textNodeI18nBlocks.get(op.target);
const icuOp = textNodeIcus.get(op.target);
const icuPlaceholder = icuPlaceholderByText.get(op.target);
const contextId = icuOp ? icuOp.context : i18nOp.context;
const resolutionTime = icuOp ? I18nParamResolutionTime.Postproccessing :
I18nParamResolutionTime.Creation;
const ops = [];
for (let i = 0; i < op.interpolation.expressions.length; i++) {
const expr = op.interpolation.expressions[i];
// For now, this i18nExpression depends on the slot context of the enclosing i18n block.
// Later, we will modify this, and advance to a different point.
ops.push(createI18nExpressionOp(contextId, i18nOp.xref, i18nOp.xref, i18nOp.handle, expr, icuPlaceholder?.xref ?? null, op.interpolation.i18nPlaceholders[i] ?? null, resolutionTime, I18nExpressionFor.I18nText, '', expr.sourceSpan ?? op.sourceSpan));
}
OpList.replaceWithMany(op, ops);
// If this interpolation is part of an ICU placeholder, add the strings and expressions to
// the placeholder.
if (icuPlaceholder !== undefined) {
icuPlaceholder.strings = op.interpolation.strings;
}
break;
}
}
}
}
/**
* Lifts local reference declarations on element-like structures within each view into an entry in
* the `consts` array for the whole component.
*/
function liftLocalRefs(job) {
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.ElementStart:
case OpKind.Template:
if (!Array.isArray(op.localRefs)) {
throw new Error(`AssertionError: expected localRefs to be an array still`);
}
op.numSlotsUsed += op.localRefs.length;
if (op.localRefs.length > 0) {
const localRefs = serializeLocalRefs(op.localRefs);
op.localRefs = job.addConst(localRefs);
}
else {
op.localRefs = null;
}
break;
}
}
}
}
function serializeLocalRefs(refs) {
const constRefs = [];
for (const ref of refs) {
constRefs.push(literal(ref.name), literal(ref.target));
}
return literalArr(constRefs);
}
/**
* Change namespaces between HTML, SVG and MathML, depending on the next element.
*/
function emitNamespaceChanges(job) {
for (const unit of job.units) {
let activeNamespace = Namespace.HTML;
for (const op of unit.create) {
if (op.kind !== OpKind.ElementStart) {
continue;
}
if (op.namespace !== activeNamespace) {
OpList.insertBefore(createNamespaceOp(op.namespace), op);
activeNamespace = op.namespace;
}
}
}
}
/**
* Parses string representation of a style and converts it into object literal.
*
* @param value string representation of style as used in the `style` attribute in HTML.
* Example: `color: red; height: auto`.
* @returns An array of style property name and value pairs, e.g. `['color', 'red', 'height',
* 'auto']`
*/
function parse(value) {
// we use a string array here instead of a string map
// because a string-map is not guaranteed to retain the
// order of the entries whereas a string array can be
// constructed in a [key, value, key, value] format.
const styles = [];
let i = 0;
let parenDepth = 0;
let quote = 0 /* Char.QuoteNone */;
let valueStart = 0;
let propStart = 0;
let currentProp = null;
while (i < value.length) {
const token = value.charCodeAt(i++);
switch (token) {
case 40 /* Char.OpenParen */:
parenDepth++;
break;
case 41 /* Char.CloseParen */:
parenDepth--;
break;
case 39 /* Char.QuoteSingle */:
// valueStart needs to be there since prop values don't
// have quotes in CSS
if (quote === 0 /* Char.QuoteNone */) {
quote = 39 /* Char.QuoteSingle */;
}
else if (quote === 39 /* Char.QuoteSingle */ && value.charCodeAt(i - 1) !== 92 /* Char.BackSlash */) {
quote = 0 /* Char.QuoteNone */;
}
break;
case 34 /* Char.QuoteDouble */:
// same logic as above
if (quote === 0 /* Char.QuoteNone */) {
quote = 34 /* Char.QuoteDouble */;
}
else if (quote === 34 /* Char.QuoteDouble */ && value.charCodeAt(i - 1) !== 92 /* Char.BackSlash */) {
quote = 0 /* Char.QuoteNone */;
}
break;
case 58 /* Char.Colon */:
if (!currentProp && parenDepth === 0 && quote === 0 /* Char.QuoteNone */) {
// TODO: Do not hyphenate CSS custom property names like: `--intentionallyCamelCase`
currentProp = hyphenate(value.substring(propStart, i - 1).trim());
valueStart = i;
}
break;
case 59 /* Char.Semicolon */:
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === 0 /* Char.QuoteNone */) {
const styleVal = value.substring(valueStart, i - 1).trim();
styles.push(currentProp, styleVal);
propStart = i;
valueStart = 0;
currentProp = null;
}
break;
}
}
if (currentProp && valueStart) {
const styleVal = value.slice(valueStart).trim();
styles.push(currentProp, styleVal);
}
return styles;
}
function hyphenate(value) {
return value
.replace(/[a-z][A-Z]/g, v => {
return v.charAt(0) + '-' + v.charAt(1);
})
.toLowerCase();
}
/**
* Generate names for functions and variables across all views.
*
* This includes propagating those names into any `ir.ReadVariableExpr`s of those variables, so that
* the reads can be emitted correctly.
*/
function nameFunctionsAndVariables(job) {
addNamesToView(job.root, job.componentName, { index: 0 }, job.compatibility === CompatibilityMode.TemplateDefinitionBuilder);
}
function addNamesToView(unit, baseName, state, compatibility) {
if (unit.fnName === null) {
// Ensure unique names for view units. This is necessary because there might be multiple
// components with same names in the context of the same pool. Only add the suffix
// if really needed.
unit.fnName = unit.job.pool.uniqueName(sanitizeIdentifier(`${baseName}_${unit.job.fnSuffix}`), /* alwaysIncludeSuffix */ false);
}
// Keep track of the names we assign to variables in the view. We'll need to propagate these
// into reads of those variables afterwards.
const varNames = new Map();
for (const op of unit.ops()) {
switch (op.kind) {
case OpKind.Property:
case OpKind.HostProperty:
if (op.isAnimationTrigger) {
op.name = '@' + op.name;
}
break;
case OpKind.Listener:
if (op.handlerFnName !== null) {
break;
}
if (!op.hostListener && op.targetSlot.slot === null) {
throw new Error(`Expected a slot to be assigned`);
}
let animation = '';
if (op.isAnimationListener) {
op.name = `@${op.name}.${op.animationPhase}`;
animation = 'animation';
}
if (op.hostListener) {
op.handlerFnName = `${baseName}_${animation}${op.name}_HostBindingHandler`;
}
else {
op.handlerFnName = `${unit.fnName}_${op.tag.replace('-', '_')}_${animation}${op.name}_${op.targetSlot.slot}_listener`;
}
op.handlerFnName = sanitizeIdentifier(op.handlerFnName);
break;
case OpKind.Variable:
varNames.set(op.xref, getVariableName(unit, op.variable, state));
break;
case OpKind.RepeaterCreate:
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
if (op.handle.slot === null) {
throw new Error(`Expected slot to be assigned`);
}
if (op.emptyView !== null) {
const emptyView = unit.job.views.get(op.emptyView);
// Repeater empty view function is at slot +2 (metadata is in the first slot).
addNamesToView(emptyView, `${baseName}_${`${op.functionNameSuffix}Empty`}_${op.handle.slot + 2}`, state, compatibility);
}
// Repeater primary view function is at slot +1 (metadata is in the first slot).
addNamesToView(unit.job.views.get(op.xref), `${baseName}_${op.functionNameSuffix}_${op.handle.slot + 1}`, state, compatibility);
break;
case OpKind.Template:
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
const childView = unit.job.views.get(op.xref);
if (op.handle.slot === null) {
throw new Error(`Expected slot to be assigned`);
}
const suffix = op.functionNameSuffix.length === 0 ? '' : `_${op.functionNameSuffix}`;
addNamesToView(childView, `${baseName}${suffix}_${op.handle.slot}`, state, compatibility);
break;
case OpKind.StyleProp:
op.name = normalizeStylePropName(op.name);
if (compatibility) {
op.name = stripImportant(op.name);
}
break;
case OpKind.ClassProp:
if (compatibility) {
op.name = stripImportant(op.name);
}
break;
}
}
// Having named all variables declared in the view, now we can push those names into the
// `ir.ReadVariableExpr` expressions which represent reads of those variables.
for (const op of unit.ops()) {
visitExpressionsInOp(op, expr => {
if (!(expr instanceof ReadVariableExpr) || expr.name !== null) {
return;
}
if (!varNames.has(expr.xref)) {
throw new Error(`Variable ${expr.xref} not yet named`);
}
expr.name = varNames.get(expr.xref);
});
}
}
function getVariableName(unit, variable, state) {
if (variable.name === null) {
switch (variable.kind) {
case SemanticVariableKind.Context:
variable.name = `ctx_r${state.index++}`;
break;
case SemanticVariableKind.Identifier:
if (unit.job.compatibility === CompatibilityMode.TemplateDefinitionBuilder) {
// TODO: Prefix increment and `_r` are for compatiblity with the old naming scheme.
// This has the potential to cause collisions when `ctx` is the identifier, so we need a
// special check for that as well.
const compatPrefix = variable.identifier === 'ctx' ? 'i' : '';
variable.name = `${variable.identifier}_${compatPrefix}r${++state.index}`;
}
else {
variable.name = `${variable.identifier}_i${state.index++}`;
}
break;
default:
// TODO: Prefix increment for compatibility only.
variable.name = `_r${++state.index}`;
break;
}
}
return variable.name;
}
/**
* Normalizes a style prop name by hyphenating it (unless its a CSS variable).
*/
function normalizeStylePropName(name) {
return name.startsWith('--') ? name : hyphenate(name);
}
/**
* Strips `!important` out of the given style or class name.
*/
function stripImportant(name) {
const importantIndex = name.indexOf('!important');
if (importantIndex > -1) {
return name.substring(0, importantIndex);
}
return name;
}
/**
* Merges logically sequential `NextContextExpr` operations.
*
* `NextContextExpr` can be referenced repeatedly, "popping" the runtime's context stack each time.
* When two such expressions appear back-to-back, it's possible to merge them together into a single
* `NextContextExpr` that steps multiple contexts. This merging is possible if all conditions are
* met:
*
* * The result of the `NextContextExpr` that's folded into the subsequent one is not stored (that
* is, the call is purely side-effectful).
* * No operations in between them uses the implicit context.
*/
function mergeNextContextExpressions(job) {
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind === OpKind.Listener) {
mergeNextContextsInOps(op.handlerOps);
}
}
mergeNextContextsInOps(unit.update);
}
}
function mergeNextContextsInOps(ops) {
for (const op of ops) {
// Look for a candidate operation to maybe merge.
if (op.kind !== OpKind.Statement || !(op.statement instanceof ExpressionStatement) ||
!(op.statement.expr instanceof NextContextExpr)) {
continue;
}
const mergeSteps = op.statement.expr.steps;
// Try to merge this `ir.NextContextExpr`.
let tryToMerge = true;
for (let candidate = op.next; candidate.kind !== OpKind.ListEnd && tryToMerge; candidate = candidate.next) {
visitExpressionsInOp(candidate, (expr, flags) => {
if (!isIrExpression(expr)) {
return expr;
}
if (!tryToMerge) {
// Either we've already merged, or failed to merge.
return;
}
if (flags & VisitorContextFlag.InChildOperation) {
// We cannot merge into child operations.
return;
}
switch (expr.kind) {
case ExpressionKind.NextContext:
// Merge the previous `ir.NextContextExpr` into this one.
expr.steps += mergeSteps;
OpList.remove(op);
tryToMerge = false;
break;
case ExpressionKind.GetCurrentView:
case ExpressionKind.Reference:
// Can't merge past a dependency on the context.
tryToMerge = false;
break;
}
return;
});
}
}
}
const CONTAINER_TAG = 'ng-container';
/**
* Replace an `Element` or `ElementStart` whose tag is `ng-container` with a specific op.
*/
function generateNgContainerOps(job) {
for (const unit of job.units) {
const updatedElementXrefs = new Set();
for (const op of unit.create) {
if (op.kind === OpKind.ElementStart && op.tag === CONTAINER_TAG) {
// Transmute the `ElementStart` instruction to `ContainerStart`.
op.kind = OpKind.ContainerStart;
updatedElementXrefs.add(op.xref);
}
if (op.kind === OpKind.ElementEnd && updatedElementXrefs.has(op.xref)) {
// This `ElementEnd` is associated with an `ElementStart` we already transmuted.
op.kind = OpKind.ContainerEnd;
}
}
}
}
/**
* Looks up an element in the given map by xref ID.
*/
function lookupElement(elements, xref) {
const el = elements.get(xref);
if (el === undefined) {
throw new Error('All attributes should have an element-like target.');
}
return el;
}
/**
* When a container is marked with `ngNonBindable`, the non-bindable characteristic also applies to
* all descendants of that container. Therefore, we must emit `disableBindings` and `enableBindings`
* instructions for every such container.
*/
function disableBindings$1(job) {
const elements = new Map();
for (const view of job.units) {
for (const op of view.create) {
if (!isElementOrContainerOp(op)) {
continue;
}
elements.set(op.xref, op);
}
}
for (const unit of job.units) {
for (const op of unit.create) {
if ((op.kind === OpKind.ElementStart || op.kind === OpKind.ContainerStart) &&
op.nonBindable) {
OpList.insertAfter(createDisableBindingsOp(op.xref), op);
}
if ((op.kind === OpKind.ElementEnd || op.kind === OpKind.ContainerEnd) &&
lookupElement(elements, op.xref).nonBindable) {
OpList.insertBefore(createEnableBindingsOp(op.xref), op);
}
}
}
}
/**
* Nullish coalescing expressions such as `a ?? b` have different semantics in Angular templates as
* compared to JavaScript. In particular, they default to `null` instead of `undefined`. Therefore,
* we replace them with ternary expressions, assigning temporaries as needed to avoid re-evaluating
* the same sub-expression multiple times.
*/
function generateNullishCoalesceExpressions(job) {
for (const unit of job.units) {
for (const op of unit.ops()) {
transformExpressionsInOp(op, expr => {
if (!(expr instanceof BinaryOperatorExpr) ||
expr.operator !== BinaryOperator.NullishCoalesce) {
return expr;
}
const assignment = new AssignTemporaryExpr(expr.lhs.clone(), job.allocateXrefId());
const read = new ReadTemporaryExpr(assignment.xref);
// TODO: When not in compatibility mode for TemplateDefinitionBuilder, we can just emit
// `t != null` instead of including an undefined check as well.
return new ConditionalExpr(new BinaryOperatorExpr(BinaryOperator.And, new BinaryOperatorExpr(BinaryOperator.NotIdentical, assignment, NULL_EXPR), new BinaryOperatorExpr(BinaryOperator.NotIdentical, read, new LiteralExpr(undefined))), read.clone(), expr.rhs);
}, VisitorContextFlag.None);
}
}
}
function kindTest(kind) {
return (op) => op.kind === kind;
}
function kindWithInterpolationTest(kind, interpolation) {
return (op) => {
return op.kind === kind && interpolation === op.expression instanceof Interpolation;
};
}
/**
* Defines the groups based on `OpKind` that ops will be divided into, for the various create
* op kinds. Ops will be collected into groups, then optionally transformed, before recombining
* the groups in the order defined here.
*/
const CREATE_ORDERING = [
{ test: op => op.kind === OpKind.Listener && op.hostListener && op.isAnimationListener },
{ test: op => op.kind === OpKind.Listener && !(op.hostListener && op.isAnimationListener) },
];
/**
* Defines the groups based on `OpKind` that ops will be divided into, for the various update
* op kinds.
*/
const UPDATE_ORDERING = [
{ test: kindTest(OpKind.StyleMap), transform: keepLast },
{ test: kindTest(OpKind.ClassMap), transform: keepLast },
{ test: kindTest(OpKind.StyleProp) },
{ test: kindTest(OpKind.ClassProp) },
{ test: kindWithInterpolationTest(OpKind.Attribute, true) },
{ test: kindWithInterpolationTest(OpKind.Property, true) },
{ test: kindWithInterpolationTest(OpKind.Property, false) },
{ test: kindWithInterpolationTest(OpKind.Attribute, false) },
];
/**
* Host bindings have their own update ordering.
*/
const UPDATE_HOST_ORDERING = [
{ test: kindWithInterpolationTest(OpKind.HostProperty, true) },
{ test: kindWithInterpolationTest(OpKind.HostProperty, false) },
{ test: kindTest(OpKind.Attribute) },
{ test: kindTest(OpKind.StyleMap), transform: keepLast },
{ test: kindTest(OpKind.ClassMap), transform: keepLast },
{ test: kindTest(OpKind.StyleProp) },
{ test: kindTest(OpKind.ClassProp) },
];
/**
* The set of all op kinds we handle in the reordering phase.
*/
const handledOpKinds = new Set([
OpKind.Listener, OpKind.StyleMap, OpKind.ClassMap, OpKind.StyleProp,
OpKind.ClassProp, OpKind.Property, OpKind.HostProperty, OpKind.Attribute
]);
/**
* Many type of operations have ordering constraints that must be respected. For example, a
* `ClassMap` instruction must be ordered after a `StyleMap` instruction, in order to have
* predictable semantics that match TemplateDefinitionBuilder and don't break applications.
*/
function orderOps(job) {
for (const unit of job.units) {
// First, we pull out ops that need to be ordered. Then, when we encounter an op that shouldn't
// be reordered, put the ones we've pulled so far back in the correct order. Finally, if we
// still have ops pulled at the end, put them back in the correct order.
// Create mode:
orderWithin(unit.create, CREATE_ORDERING);
// Update mode:
const ordering = unit.job.kind === CompilationJobKind.Host ? UPDATE_HOST_ORDERING : UPDATE_ORDERING;
orderWithin(unit.update, ordering);
}
}
/**
* Order all the ops within the specified group.
*/
function orderWithin(opList, ordering) {
let opsToOrder = [];
// Only reorder ops that target the same xref; do not mix ops that target different xrefs.
let firstTargetInGroup = null;
for (const op of opList) {
const currentTarget = hasDependsOnSlotContextTrait(op) ? op.target : null;
if (!handledOpKinds.has(op.kind) ||
(currentTarget !== firstTargetInGroup &&
(firstTargetInGroup !== null && currentTarget !== null))) {
OpList.insertBefore(reorder(opsToOrder, ordering), op);
opsToOrder = [];
firstTargetInGroup = null;
}
if (handledOpKinds.has(op.kind)) {
opsToOrder.push(op);
OpList.remove(op);
firstTargetInGroup = currentTarget ?? firstTargetInGroup;
}
}
opList.push(reorder(opsToOrder, ordering));
}
/**
* Reorders the given list of ops according to the ordering defined by `ORDERING`.
*/
function reorder(ops, ordering) {
// Break the ops list into groups based on OpKind.
const groups = Array.from(ordering, () => new Array());
for (const op of ops) {
const groupIndex = ordering.findIndex(o => o.test(op));
groups[groupIndex].push(op);
}
// Reassemble the groups into a single list, in the correct order.
return groups.flatMap((group, i) => {
const transform = ordering[i].transform;
return transform ? transform(group) : group;
});
}
/**
* Keeps only the last op in a list of ops.
*/
function keepLast(ops) {
return ops.slice(ops.length - 1);
}
/**
* Parses extracted style and class attributes into separate ExtractedAttributeOps per style or
* class property.
*/
function parseExtractedStyles(job) {
const elements = new Map();
for (const unit of job.units) {
for (const op of unit.create) {
if (isElementOrContainerOp(op)) {
elements.set(op.xref, op);
}
}
}
for (const unit of job.units) {
for (const op of unit.create) {
if (op.kind === OpKind.ExtractedAttribute && op.bindingKind === BindingKind.Attribute &&
isStringLiteral(op.expression)) {
const target = elements.get(op.target);
if (target !== undefined && target.kind === OpKind.Template &&
target.templateKind === TemplateKind.Structural) {
// TemplateDefinitionBuilder will not apply class and style bindings to structural
// directives; instead, it will leave them as attributes.
// (It's not clear what that would mean, anyway -- classes and styles on a structural
// element should probably be a parse error.)
// TODO: We may be able to remove this once Template Pipeline is the default.
continue;
}
if (op.name === 'style') {
const parsedStyles = parse(op.expression.value);
for (let i = 0; i < parsedStyles.length - 1; i += 2) {
OpList.insertBefore(createExtractedAttributeOp(op.target, BindingKind.StyleProperty, null, parsedStyles[i], literal(parsedStyles[i + 1]), null, null, SecurityContext.STYLE), op);
}
OpList.remove(op);
}
else if (op.name === 'class') {
const parsedClasses = op.expression.value.trim().split(/\s+/g);
for (const parsedClass of parsedClasses) {
OpList.insertBefore(createExtractedAttributeOp(op.target, BindingKind.ClassName, null, parsedClass, null, null, null, SecurityContext.NONE), op);
}
OpList.remove(op);
}
}
}
}
}
/**
* Attributes of `ng-content` named 'select' are specifically removed, because they control which
* content matches as a property of the `projection`, and are not a plain attribute.
*/
function removeContentSelectors(job) {
for (const unit of job.units) {
const elements = createOpXrefMap(unit);
for (const op of unit.ops()) {
switch (op.kind) {
case OpKind.Binding:
const target = lookupInXrefMap(elements, op.target);
if (isSelectAttribute(op.name) && target.kind === OpKind.Projection) {
OpList.remove(op);
}
break;
}
}
}
}
function isSelectAttribute(name) {
return name.toLowerCase() === 'select';
}
/**
* Looks up an element in the given map by xref ID.
*/
function lookupInXrefMap(map, xref) {
const el = map.get(xref);
if (el === undefined) {
throw new Error('All attributes should have an slottable target.');
}
return el;
}
/**
* This phase generates pipe creation instructions. We do this based on the pipe bindings found in
* the update block, in the order we see them.
*
* When not in compatibility mode, we can simply group all these creation instructions together, to
* maximize chaining opportunities.
*/
function createPipes(job) {
for (const unit of job.units) {
processPipeBindingsInView(unit);
}
}
function processPipeBindingsInView(unit) {
for (const updateOp of unit.update) {
visitExpressionsInOp(updateOp, (expr, flags) => {
if (!isIrExpression(expr)) {
return;
}
if (expr.kind !== ExpressionKind.PipeBinding) {
return;
}
if (flags & VisitorContextFlag.InChildOperation) {
throw new Error(`AssertionError: pipe bindings should not appear in child expressions`);
}
if (unit.job.compatibility) {
// TODO: We can delete this cast and check once compatibility mode is removed.
const slotHandle = updateOp.target;
if (slotHandle == undefined) {
throw new Error(`AssertionError: expected slot handle to be assigned for pipe creation`);
}
addPipeToCreationBlock(unit, updateOp.target, expr);
}
else {
// When not in compatibility mode, we just add the pipe to the end of the create block. This
// is not only simpler and faster, but allows more chaining opportunities for other
// instructions.
unit.create.push(createPipeOp(expr.target, expr.targetSlot, expr.name));
}
});
}
}
function addPipeToCreationBlock(unit, afterTargetXref, binding) {
// Find the appropriate point to insert the Pipe creation operation.
// We're looking for `afterTargetXref` (and also want to insert after any other pipe operations
// which might be beyond it).
for (let op = unit.create.head.next; op.kind !== OpKind.ListEnd; op = op.next) {
if (!hasConsumesSlotTrait(op)) {
continue;
}
if (op.xref !== afterTargetXref) {
continue;
}
// We've found a tentative insertion point; however, we also want to skip past any _other_ pipe
// operations present.
while (op.next.kind === OpKind.Pipe) {
op = op.next;
}
const pipe = createPipeOp(binding.target, binding.targetSlot, binding.name);
OpList.insertBefore(pipe, op.next);
// This completes adding the pipe to the creation block.
return;
}
// At this point, we've failed to add the pipe to the creation block.
throw new Error(`AssertionError: unable to find insertion point for pipe ${binding.name}`);
}
/**
* Pipes that accept more than 4 arguments are variadic, and are handled with a different runtime
* instruction.
*/
function createVariadicPipes(job) {
for (const unit of job.units) {
for (const op of unit.update) {
transformExpressionsInOp(op, expr => {
if (!(expr instanceof PipeBindingExpr)) {
return expr;
}
// Pipes are variadic if they have more than 4 arguments.
if (expr.args.length <= 4) {
return expr;
}
return new PipeBindingVariadicExpr(expr.target, expr.targetSlot, expr.name, literalArr(expr.args), expr.args.length);
}, VisitorContextFlag.None);
}
}
}
/**
* Propagate i18n blocks down through child templates that act as placeholders in the root i18n
* message. Specifically, perform an in-order traversal of all the views, and add i18nStart/i18nEnd
* op pairs into descending views. Also, assign an increasing sub-template index to each
* descending view.
*/
function propagateI18nBlocks(job) {
propagateI18nBlocksToTemplates(job.root, 0);
}
/**
* Propagates i18n ops in the given view through to any child views recursively.
*/
function propagateI18nBlocksToTemplates(unit, subTemplateIndex) {
let i18nBlock = null;
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nStart:
op.subTemplateIndex = subTemplateIndex === 0 ? null : subTemplateIndex;
i18nBlock = op;
break;
case OpKind.I18nEnd:
// When we exit a root-level i18n block, reset the sub-template index counter.
if (i18nBlock.subTemplateIndex === null) {
subTemplateIndex = 0;
}
i18nBlock = null;
break;
case OpKind.Template:
subTemplateIndex = propagateI18nBlocksForView(unit.job.views.get(op.xref), i18nBlock, op.i18nPlaceholder, subTemplateIndex);
break;
case OpKind.RepeaterCreate:
// Propagate i18n blocks to the @for template.
const forView = unit.job.views.get(op.xref);
subTemplateIndex = propagateI18nBlocksForView(unit.job.views.get(op.xref), i18nBlock, op.i18nPlaceholder, subTemplateIndex);
// Then if there's an @empty template, propagate the i18n blocks for it as well.
if (op.emptyView !== null) {
subTemplateIndex = propagateI18nBlocksForView(unit.job.views.get(op.emptyView), i18nBlock, op.emptyI18nPlaceholder, subTemplateIndex);
}
break;
}
}
return subTemplateIndex;
}
/**
* Propagate i18n blocks for a view.
*/
function propagateI18nBlocksForView(view, i18nBlock, i18nPlaceholder, subTemplateIndex) {
// We found an inside an i18n block; increment the sub-template counter and
// wrap the template's view in a child i18n block.
if (i18nPlaceholder !== undefined) {
if (i18nBlock === null) {
throw Error('Expected template with i18n placeholder to be in an i18n block.');
}
subTemplateIndex++;
wrapTemplateWithI18n(view, i18nBlock);
}
// Continue traversing inside the template's view.
return propagateI18nBlocksToTemplates(view, subTemplateIndex);
}
/**
* Wraps a template view with i18n start and end ops.
*/
function wrapTemplateWithI18n(unit, parentI18n) {
// Only add i18n ops if they have not already been propagated to this template.
if (unit.create.head.next?.kind !== OpKind.I18nStart) {
const id = unit.job.allocateXrefId();
OpList.insertAfter(
// Nested ng-template i18n start/end ops should not recieve source spans.
createI18nStartOp(id, parentI18n.message, parentI18n.root, null), unit.create.head);
OpList.insertBefore(createI18nEndOp(id, null), unit.create.tail);
}
}
function extractPureFunctions(job) {
for (const view of job.units) {
for (const op of view.ops()) {
visitExpressionsInOp(op, expr => {
if (!(expr instanceof PureFunctionExpr) || expr.body === null) {
return;
}
const constantDef = new PureFunctionConstant(expr.args.length);
expr.fn = job.pool.getSharedConstant(constantDef, expr.body);
expr.body = null;
});
}
}
}
class PureFunctionConstant extends GenericKeyFn {
constructor(numArgs) {
super();
this.numArgs = numArgs;
}
keyOf(expr) {
if (expr instanceof PureFunctionParameterExpr) {
return `param(${expr.index})`;
}
else {
return super.keyOf(expr);
}
}
// TODO: Use the new pool method `getSharedFunctionReference`
toSharedConstantDeclaration(declName, keyExpr) {
const fnParams = [];
for (let idx = 0; idx < this.numArgs; idx++) {
fnParams.push(new FnParam('a' + idx));
}
// We will never visit `ir.PureFunctionParameterExpr`s that don't belong to us, because this
// transform runs inside another visitor which will visit nested pure functions before this one.
const returnExpr = transformExpressionsInExpression(keyExpr, expr => {
if (!(expr instanceof PureFunctionParameterExpr)) {
return expr;
}
return variable('a' + expr.index);
}, VisitorContextFlag.None);
return new DeclareVarStmt(declName, new ArrowFunctionExpr(fnParams, returnExpr), undefined, StmtModifier.Final);
}
}
function generatePureLiteralStructures(job) {
for (const unit of job.units) {
for (const op of unit.update) {
transformExpressionsInOp(op, (expr, flags) => {
if (flags & VisitorContextFlag.InChildOperation) {
return expr;
}
if (expr instanceof LiteralArrayExpr) {
return transformLiteralArray(expr);
}
else if (expr instanceof LiteralMapExpr) {
return transformLiteralMap(expr);
}
return expr;
}, VisitorContextFlag.None);
}
}
}
function transformLiteralArray(expr) {
const derivedEntries = [];
const nonConstantArgs = [];
for (const entry of expr.entries) {
if (entry.isConstant()) {
derivedEntries.push(entry);
}
else {
const idx = nonConstantArgs.length;
nonConstantArgs.push(entry);
derivedEntries.push(new PureFunctionParameterExpr(idx));
}
}
return new PureFunctionExpr(literalArr(derivedEntries), nonConstantArgs);
}
function transformLiteralMap(expr) {
let derivedEntries = [];
const nonConstantArgs = [];
for (const entry of expr.entries) {
if (entry.value.isConstant()) {
derivedEntries.push(entry);
}
else {
const idx = nonConstantArgs.length;
nonConstantArgs.push(entry.value);
derivedEntries.push(new LiteralMapEntry(entry.key, new PureFunctionParameterExpr(idx), entry.quoted));
}
}
return new PureFunctionExpr(literalMap(derivedEntries), nonConstantArgs);
}
// This file contains helpers for generating calls to Ivy instructions. In particular, each
// instruction type is represented as a function, which may select a specific instruction variant
// depending on the exact arguments.
function element(slot, tag, constIndex, localRefIndex, sourceSpan) {
return elementOrContainerBase(Identifiers.element, slot, tag, constIndex, localRefIndex, sourceSpan);
}
function elementStart(slot, tag, constIndex, localRefIndex, sourceSpan) {
return elementOrContainerBase(Identifiers.elementStart, slot, tag, constIndex, localRefIndex, sourceSpan);
}
function elementOrContainerBase(instruction, slot, tag, constIndex, localRefIndex, sourceSpan) {
const args = [literal(slot)];
if (tag !== null) {
args.push(literal(tag));
}
if (localRefIndex !== null) {
args.push(literal(constIndex), // might be null, but that's okay.
literal(localRefIndex));
}
else if (constIndex !== null) {
args.push(literal(constIndex));
}
return call(instruction, args, sourceSpan);
}
function elementEnd(sourceSpan) {
return call(Identifiers.elementEnd, [], sourceSpan);
}
function elementContainerStart(slot, constIndex, localRefIndex, sourceSpan) {
return elementOrContainerBase(Identifiers.elementContainerStart, slot, /* tag */ null, constIndex, localRefIndex, sourceSpan);
}
function elementContainer(slot, constIndex, localRefIndex, sourceSpan) {
return elementOrContainerBase(Identifiers.elementContainer, slot, /* tag */ null, constIndex, localRefIndex, sourceSpan);
}
function elementContainerEnd() {
return call(Identifiers.elementContainerEnd, [], null);
}
function template(slot, templateFnRef, decls, vars, tag, constIndex, localRefs, sourceSpan) {
const args = [
literal(slot),
templateFnRef,
literal(decls),
literal(vars),
literal(tag),
literal(constIndex),
];
if (localRefs !== null) {
args.push(literal(localRefs));
args.push(importExpr(Identifiers.templateRefExtractor));
}
while (args[args.length - 1].isEquivalent(NULL_EXPR)) {
args.pop();
}
return call(Identifiers.templateCreate, args, sourceSpan);
}
function disableBindings() {
return call(Identifiers.disableBindings, [], null);
}
function enableBindings() {
return call(Identifiers.enableBindings, [], null);
}
function listener(name, handlerFn, eventTargetResolver, syntheticHost, sourceSpan) {
const args = [literal(name), handlerFn];
if (eventTargetResolver !== null) {
args.push(literal(false)); // `useCapture` flag, defaults to `false`
args.push(importExpr(eventTargetResolver));
}
return call(syntheticHost ? Identifiers.syntheticHostListener : Identifiers.listener, args, sourceSpan);
}
function pipe(slot, name) {
return call(Identifiers.pipe, [
literal(slot),
literal(name),
], null);
}
function namespaceHTML() {
return call(Identifiers.namespaceHTML, [], null);
}
function namespaceSVG() {
return call(Identifiers.namespaceSVG, [], null);
}
function namespaceMath() {
return call(Identifiers.namespaceMathML, [], null);
}
function advance(delta, sourceSpan) {
return call(Identifiers.advance, delta > 1 ? [literal(delta)] : [], sourceSpan);
}
function reference(slot) {
return importExpr(Identifiers.reference).callFn([
literal(slot),
]);
}
function nextContext(steps) {
return importExpr(Identifiers.nextContext).callFn(steps === 1 ? [] : [literal(steps)]);
}
function getCurrentView() {
return importExpr(Identifiers.getCurrentView).callFn([]);
}
function restoreView(savedView) {
return importExpr(Identifiers.restoreView).callFn([
savedView,
]);
}
function resetView(returnValue) {
return importExpr(Identifiers.resetView).callFn([
returnValue,
]);
}
function text(slot, initialValue, sourceSpan) {
const args = [literal(slot, null)];
if (initialValue !== '') {
args.push(literal(initialValue));
}
return call(Identifiers.text, args, sourceSpan);
}
function defer(selfSlot, primarySlot, dependencyResolverFn, loadingSlot, placeholderSlot, errorSlot, loadingConfig, placeholderConfig, enableTimerScheduling, sourceSpan) {
const args = [
literal(selfSlot),
literal(primarySlot),
dependencyResolverFn ?? literal(null),
literal(loadingSlot),
literal(placeholderSlot),
literal(errorSlot),
loadingConfig ?? literal(null),
placeholderConfig ?? literal(null),
enableTimerScheduling ? importExpr(Identifiers.deferEnableTimerScheduling) : literal(null),
];
let expr;
while ((expr = args[args.length - 1]) !== null && expr instanceof LiteralExpr &&
expr.value === null) {
args.pop();
}
return call(Identifiers.defer, args, sourceSpan);
}
const deferTriggerToR3TriggerInstructionsMap = new Map([
[DeferTriggerKind.Idle, [Identifiers.deferOnIdle, Identifiers.deferPrefetchOnIdle]],
[
DeferTriggerKind.Immediate,
[Identifiers.deferOnImmediate, Identifiers.deferPrefetchOnImmediate]
],
[DeferTriggerKind.Timer, [Identifiers.deferOnTimer, Identifiers.deferPrefetchOnTimer]],
[DeferTriggerKind.Hover, [Identifiers.deferOnHover, Identifiers.deferPrefetchOnHover]],
[
DeferTriggerKind.Interaction,
[Identifiers.deferOnInteraction, Identifiers.deferPrefetchOnInteraction]
],
[
DeferTriggerKind.Viewport, [Identifiers.deferOnViewport, Identifiers.deferPrefetchOnViewport]
],
]);
function deferOn(trigger, args, prefetch, sourceSpan) {
const instructions = deferTriggerToR3TriggerInstructionsMap.get(trigger);
if (instructions === undefined) {
throw new Error(`Unable to determine instruction for trigger ${trigger}`);
}
const instructionToCall = prefetch ? instructions[1] : instructions[0];
return call(instructionToCall, args.map(a => literal(a)), sourceSpan);
}
function projectionDef(def) {
return call(Identifiers.projectionDef, def ? [def] : [], null);
}
function projection(slot, projectionSlotIndex, attributes, sourceSpan) {
const args = [literal(slot)];
if (projectionSlotIndex !== 0 || attributes !== null) {
args.push(literal(projectionSlotIndex));
if (attributes !== null) {
args.push(attributes);
}
}
return call(Identifiers.projection, args, sourceSpan);
}
function i18nStart(slot, constIndex, subTemplateIndex, sourceSpan) {
const args = [literal(slot), literal(constIndex)];
if (subTemplateIndex !== null) {
args.push(literal(subTemplateIndex));
}
return call(Identifiers.i18nStart, args, sourceSpan);
}
function repeaterCreate(slot, viewFnName, decls, vars, tag, constIndex, trackByFn, trackByUsesComponentInstance, emptyViewFnName, emptyDecls, emptyVars, emptyTag, emptyConstIndex, sourceSpan) {
const args = [
literal(slot),
variable(viewFnName),
literal(decls),
literal(vars),
literal(tag),
literal(constIndex),
trackByFn,
];
if (trackByUsesComponentInstance || emptyViewFnName !== null) {
args.push(literal(trackByUsesComponentInstance));
if (emptyViewFnName !== null) {
args.push(variable(emptyViewFnName), literal(emptyDecls), literal(emptyVars));
if (emptyTag !== null || emptyConstIndex !== null) {
args.push(literal(emptyTag));
}
if (emptyConstIndex !== null) {
args.push(literal(emptyConstIndex));
}
}
}
return call(Identifiers.repeaterCreate, args, sourceSpan);
}
function repeater(collection, sourceSpan) {
return call(Identifiers.repeater, [collection], sourceSpan);
}
function deferWhen(prefetch, expr, sourceSpan) {
return call(prefetch ? Identifiers.deferPrefetchWhen : Identifiers.deferWhen, [expr], sourceSpan);
}
function i18n(slot, constIndex, subTemplateIndex, sourceSpan) {
const args = [literal(slot), literal(constIndex)];
if (subTemplateIndex) {
args.push(literal(subTemplateIndex));
}
return call(Identifiers.i18n, args, sourceSpan);
}
function i18nEnd(endSourceSpan) {
return call(Identifiers.i18nEnd, [], endSourceSpan);
}
function i18nAttributes(slot, i18nAttributesConfig) {
const args = [literal(slot), literal(i18nAttributesConfig)];
return call(Identifiers.i18nAttributes, args, null);
}
function property(name, expression, sanitizer, sourceSpan) {
const args = [literal(name), expression];
if (sanitizer !== null) {
args.push(sanitizer);
}
return call(Identifiers.property, args, sourceSpan);
}
function attribute(name, expression, sanitizer, namespace) {
const args = [literal(name), expression];
if (sanitizer !== null || namespace !== null) {
args.push(sanitizer ?? literal(null));
}
if (namespace !== null) {
args.push(literal(namespace));
}
return call(Identifiers.attribute, args, null);
}
function styleProp(name, expression, unit, sourceSpan) {
const args = [literal(name), expression];
if (unit !== null) {
args.push(literal(unit));
}
return call(Identifiers.styleProp, args, sourceSpan);
}
function classProp(name, expression, sourceSpan) {
return call(Identifiers.classProp, [literal(name), expression], sourceSpan);
}
function styleMap(expression, sourceSpan) {
return call(Identifiers.styleMap, [expression], sourceSpan);
}
function classMap(expression, sourceSpan) {
return call(Identifiers.classMap, [expression], sourceSpan);
}
const PIPE_BINDINGS = [
Identifiers.pipeBind1,
Identifiers.pipeBind2,
Identifiers.pipeBind3,
Identifiers.pipeBind4,
];
function pipeBind(slot, varOffset, args) {
if (args.length < 1 || args.length > PIPE_BINDINGS.length) {
throw new Error(`pipeBind() argument count out of bounds`);
}
const instruction = PIPE_BINDINGS[args.length - 1];
return importExpr(instruction).callFn([
literal(slot),
literal(varOffset),
...args,
]);
}
function pipeBindV(slot, varOffset, args) {
return importExpr(Identifiers.pipeBindV).callFn([
literal(slot),
literal(varOffset),
args,
]);
}
function textInterpolate(strings, expressions, sourceSpan) {
if (strings.length < 1 || expressions.length !== strings.length - 1) {
throw new Error(`AssertionError: expected specific shape of args for strings/expressions in interpolation`);
}
const interpolationArgs = [];
if (expressions.length === 1 && strings[0] === '' && strings[1] === '') {
interpolationArgs.push(expressions[0]);
}
else {
let idx;
for (idx = 0; idx < expressions.length; idx++) {
interpolationArgs.push(literal(strings[idx]), expressions[idx]);
}
// idx points at the last string.
interpolationArgs.push(literal(strings[idx]));
}
return callVariadicInstruction(TEXT_INTERPOLATE_CONFIG, [], interpolationArgs, [], sourceSpan);
}
function i18nExp(expr, sourceSpan) {
return call(Identifiers.i18nExp, [expr], sourceSpan);
}
function i18nApply(slot, sourceSpan) {
return call(Identifiers.i18nApply, [literal(slot)], sourceSpan);
}
function propertyInterpolate(name, strings, expressions, sanitizer, sourceSpan) {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
const extraArgs = [];
if (sanitizer !== null) {
extraArgs.push(sanitizer);
}
return callVariadicInstruction(PROPERTY_INTERPOLATE_CONFIG, [literal(name)], interpolationArgs, extraArgs, sourceSpan);
}
function attributeInterpolate(name, strings, expressions, sanitizer, sourceSpan) {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
const extraArgs = [];
if (sanitizer !== null) {
extraArgs.push(sanitizer);
}
return callVariadicInstruction(ATTRIBUTE_INTERPOLATE_CONFIG, [literal(name)], interpolationArgs, extraArgs, sourceSpan);
}
function stylePropInterpolate(name, strings, expressions, unit, sourceSpan) {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
const extraArgs = [];
if (unit !== null) {
extraArgs.push(literal(unit));
}
return callVariadicInstruction(STYLE_PROP_INTERPOLATE_CONFIG, [literal(name)], interpolationArgs, extraArgs, sourceSpan);
}
function styleMapInterpolate(strings, expressions, sourceSpan) {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
return callVariadicInstruction(STYLE_MAP_INTERPOLATE_CONFIG, [], interpolationArgs, [], sourceSpan);
}
function classMapInterpolate(strings, expressions, sourceSpan) {
const interpolationArgs = collateInterpolationArgs(strings, expressions);
return callVariadicInstruction(CLASS_MAP_INTERPOLATE_CONFIG, [], interpolationArgs, [], sourceSpan);
}
function hostProperty(name, expression, sanitizer, sourceSpan) {
const args = [literal(name), expression];
if (sanitizer !== null) {
args.push(sanitizer);
}
return call(Identifiers.hostProperty, args, sourceSpan);
}
function syntheticHostProperty(name, expression, sourceSpan) {
return call(Identifiers.syntheticHostProperty, [literal(name), expression], sourceSpan);
}
function pureFunction(varOffset, fn, args) {
return callVariadicInstructionExpr(PURE_FUNCTION_CONFIG, [
literal(varOffset),
fn,
], args, [], null);
}
/**
* Collates the string an expression arguments for an interpolation instruction.
*/
function collateInterpolationArgs(strings, expressions) {
if (strings.length < 1 || expressions.length !== strings.length - 1) {
throw new Error(`AssertionError: expected specific shape of args for strings/expressions in interpolation`);
}
const interpolationArgs = [];
if (expressions.length === 1 && strings[0] === '' && strings[1] === '') {
interpolationArgs.push(expressions[0]);
}
else {
let idx;
for (idx = 0; idx < expressions.length; idx++) {
interpolationArgs.push(literal(strings[idx]), expressions[idx]);
}
// idx points at the last string.
interpolationArgs.push(literal(strings[idx]));
}
return interpolationArgs;
}
function call(instruction, args, sourceSpan) {
const expr = importExpr(instruction).callFn(args, sourceSpan);
return createStatementOp(new ExpressionStatement(expr, sourceSpan));
}
function conditional(slot, condition, contextValue, sourceSpan) {
const args = [literal(slot), condition];
if (contextValue !== null) {
args.push(contextValue);
}
return call(Identifiers.conditional, args, sourceSpan);
}
/**
* `InterpolationConfig` for the `textInterpolate` instruction.
*/
const TEXT_INTERPOLATE_CONFIG = {
constant: [
Identifiers.textInterpolate,
Identifiers.textInterpolate1,
Identifiers.textInterpolate2,
Identifiers.textInterpolate3,
Identifiers.textInterpolate4,
Identifiers.textInterpolate5,
Identifiers.textInterpolate6,
Identifiers.textInterpolate7,
Identifiers.textInterpolate8,
],
variable: Identifiers.textInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
/**
* `InterpolationConfig` for the `propertyInterpolate` instruction.
*/
const PROPERTY_INTERPOLATE_CONFIG = {
constant: [
Identifiers.propertyInterpolate,
Identifiers.propertyInterpolate1,
Identifiers.propertyInterpolate2,
Identifiers.propertyInterpolate3,
Identifiers.propertyInterpolate4,
Identifiers.propertyInterpolate5,
Identifiers.propertyInterpolate6,
Identifiers.propertyInterpolate7,
Identifiers.propertyInterpolate8,
],
variable: Identifiers.propertyInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
/**
* `InterpolationConfig` for the `stylePropInterpolate` instruction.
*/
const STYLE_PROP_INTERPOLATE_CONFIG = {
constant: [
Identifiers.styleProp,
Identifiers.stylePropInterpolate1,
Identifiers.stylePropInterpolate2,
Identifiers.stylePropInterpolate3,
Identifiers.stylePropInterpolate4,
Identifiers.stylePropInterpolate5,
Identifiers.stylePropInterpolate6,
Identifiers.stylePropInterpolate7,
Identifiers.stylePropInterpolate8,
],
variable: Identifiers.stylePropInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
/**
* `InterpolationConfig` for the `attributeInterpolate` instruction.
*/
const ATTRIBUTE_INTERPOLATE_CONFIG = {
constant: [
Identifiers.attribute,
Identifiers.attributeInterpolate1,
Identifiers.attributeInterpolate2,
Identifiers.attributeInterpolate3,
Identifiers.attributeInterpolate4,
Identifiers.attributeInterpolate5,
Identifiers.attributeInterpolate6,
Identifiers.attributeInterpolate7,
Identifiers.attributeInterpolate8,
],
variable: Identifiers.attributeInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
/**
* `InterpolationConfig` for the `styleMapInterpolate` instruction.
*/
const STYLE_MAP_INTERPOLATE_CONFIG = {
constant: [
Identifiers.styleMap,
Identifiers.styleMapInterpolate1,
Identifiers.styleMapInterpolate2,
Identifiers.styleMapInterpolate3,
Identifiers.styleMapInterpolate4,
Identifiers.styleMapInterpolate5,
Identifiers.styleMapInterpolate6,
Identifiers.styleMapInterpolate7,
Identifiers.styleMapInterpolate8,
],
variable: Identifiers.styleMapInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
/**
* `InterpolationConfig` for the `classMapInterpolate` instruction.
*/
const CLASS_MAP_INTERPOLATE_CONFIG = {
constant: [
Identifiers.classMap,
Identifiers.classMapInterpolate1,
Identifiers.classMapInterpolate2,
Identifiers.classMapInterpolate3,
Identifiers.classMapInterpolate4,
Identifiers.classMapInterpolate5,
Identifiers.classMapInterpolate6,
Identifiers.classMapInterpolate7,
Identifiers.classMapInterpolate8,
],
variable: Identifiers.classMapInterpolateV,
mapping: n => {
if (n % 2 === 0) {
throw new Error(`Expected odd number of arguments`);
}
return (n - 1) / 2;
},
};
const PURE_FUNCTION_CONFIG = {
constant: [
Identifiers.pureFunction0,
Identifiers.pureFunction1,
Identifiers.pureFunction2,
Identifiers.pureFunction3,
Identifiers.pureFunction4,
Identifiers.pureFunction5,
Identifiers.pureFunction6,
Identifiers.pureFunction7,
Identifiers.pureFunction8,
],
variable: Identifiers.pureFunctionV,
mapping: n => n,
};
function callVariadicInstructionExpr(config, baseArgs, interpolationArgs, extraArgs, sourceSpan) {
const n = config.mapping(interpolationArgs.length);
if (n < config.constant.length) {
// Constant calling pattern.
return importExpr(config.constant[n])
.callFn([...baseArgs, ...interpolationArgs, ...extraArgs], sourceSpan);
}
else if (config.variable !== null) {
// Variable calling pattern.
return importExpr(config.variable)
.callFn([...baseArgs, literalArr(interpolationArgs), ...extraArgs], sourceSpan);
}
else {
throw new Error(`AssertionError: unable to call variadic function`);
}
}
function callVariadicInstruction(config, baseArgs, interpolationArgs, extraArgs, sourceSpan) {
return createStatementOp(callVariadicInstructionExpr(config, baseArgs, interpolationArgs, extraArgs, sourceSpan)
.toStmt());
}
/**
* Map of target resolvers for event listeners.
*/
const GLOBAL_TARGET_RESOLVERS$1 = new Map([
['window', Identifiers.resolveWindow],
['document', Identifiers.resolveDocument],
['body', Identifiers.resolveBody],
]);
/**
* Compiles semantic operations across all views and generates output `o.Statement`s with actual
* runtime calls in their place.
*
* Reification replaces semantic operations with selected Ivy instructions and other generated code
* structures. After reification, the create/update operation lists of all views should only contain
* `ir.StatementOp`s (which wrap generated `o.Statement`s).
*/
function reify(job) {
for (const unit of job.units) {
reifyCreateOperations(unit, unit.create);
reifyUpdateOperations(unit, unit.update);
}
}
/**
* This function can be used a sanity check -- it walks every expression in the const pool, and
* every expression reachable from an op, and makes sure that there are no IR expressions
* left. This is nice to use for debugging mysterious failures where an IR expression cannot be
* output from the output AST code.
*/
function ensureNoIrForDebug(job) {
for (const stmt of job.pool.statements) {
transformExpressionsInStatement(stmt, expr => {
if (isIrExpression(expr)) {
throw new Error(`AssertionError: IR expression found during reify: ${ExpressionKind[expr.kind]}`);
}
return expr;
}, VisitorContextFlag.None);
}
for (const unit of job.units) {
for (const op of unit.ops()) {
visitExpressionsInOp(op, expr => {
if (isIrExpression(expr)) {
throw new Error(`AssertionError: IR expression found during reify: ${ExpressionKind[expr.kind]}`);
}
});
}
}
}
function reifyCreateOperations(unit, ops) {
for (const op of ops) {
transformExpressionsInOp(op, reifyIrExpression, VisitorContextFlag.None);
switch (op.kind) {
case OpKind.Text:
OpList.replace(op, text(op.handle.slot, op.initialValue, op.sourceSpan));
break;
case OpKind.ElementStart:
OpList.replace(op, elementStart(op.handle.slot, op.tag, op.attributes, op.localRefs, op.startSourceSpan));
break;
case OpKind.Element:
OpList.replace(op, element(op.handle.slot, op.tag, op.attributes, op.localRefs, op.wholeSourceSpan));
break;
case OpKind.ElementEnd:
OpList.replace(op, elementEnd(op.sourceSpan));
break;
case OpKind.ContainerStart:
OpList.replace(op, elementContainerStart(op.handle.slot, op.attributes, op.localRefs, op.startSourceSpan));
break;
case OpKind.Container:
OpList.replace(op, elementContainer(op.handle.slot, op.attributes, op.localRefs, op.wholeSourceSpan));
break;
case OpKind.ContainerEnd:
OpList.replace(op, elementContainerEnd());
break;
case OpKind.I18nStart:
OpList.replace(op, i18nStart(op.handle.slot, op.messageIndex, op.subTemplateIndex, op.sourceSpan));
break;
case OpKind.I18nEnd:
OpList.replace(op, i18nEnd(op.sourceSpan));
break;
case OpKind.I18n:
OpList.replace(op, i18n(op.handle.slot, op.messageIndex, op.subTemplateIndex, op.sourceSpan));
break;
case OpKind.I18nAttributes:
if (op.i18nAttributesConfig === null) {
throw new Error(`AssertionError: i18nAttributesConfig was not set`);
}
OpList.replace(op, i18nAttributes(op.handle.slot, op.i18nAttributesConfig));
break;
case OpKind.Template:
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
if (Array.isArray(op.localRefs)) {
throw new Error(`AssertionError: local refs array should have been extracted into a constant`);
}
const childView = unit.job.views.get(op.xref);
OpList.replace(op, template(op.handle.slot, variable(childView.fnName), childView.decls, childView.vars, op.tag, op.attributes, op.localRefs, op.startSourceSpan));
break;
case OpKind.DisableBindings:
OpList.replace(op, disableBindings());
break;
case OpKind.EnableBindings:
OpList.replace(op, enableBindings());
break;
case OpKind.Pipe:
OpList.replace(op, pipe(op.handle.slot, op.name));
break;
case OpKind.Listener:
const listenerFn = reifyListenerHandler(unit, op.handlerFnName, op.handlerOps, op.consumesDollarEvent);
const eventTargetResolver = op.eventTarget ? GLOBAL_TARGET_RESOLVERS$1.get(op.eventTarget) : null;
if (eventTargetResolver === undefined) {
throw new Error(`Unexpected global target '${op.eventTarget}' defined for '${op.name}' event. Supported list of global targets: window,document,body.`);
}
OpList.replace(op, listener(op.name, listenerFn, eventTargetResolver, op.hostListener && op.isAnimationListener, op.sourceSpan));
break;
case OpKind.Variable:
if (op.variable.name === null) {
throw new Error(`AssertionError: unnamed variable ${op.xref}`);
}
OpList.replace(op, createStatementOp(new DeclareVarStmt(op.variable.name, op.initializer, undefined, StmtModifier.Final)));
break;
case OpKind.Namespace:
switch (op.active) {
case Namespace.HTML:
OpList.replace(op, namespaceHTML());
break;
case Namespace.SVG:
OpList.replace(op, namespaceSVG());
break;
case Namespace.Math:
OpList.replace(op, namespaceMath());
break;
}
break;
case OpKind.Defer:
const timerScheduling = !!op.loadingMinimumTime || !!op.loadingAfterTime || !!op.placeholderMinimumTime;
OpList.replace(op, defer(op.handle.slot, op.mainSlot.slot, op.resolverFn, op.loadingSlot?.slot ?? null, op.placeholderSlot?.slot ?? null, op.errorSlot?.slot ?? null, op.loadingConfig, op.placeholderConfig, timerScheduling, op.sourceSpan));
break;
case OpKind.DeferOn:
let args = [];
switch (op.trigger.kind) {
case DeferTriggerKind.Idle:
case DeferTriggerKind.Immediate:
break;
case DeferTriggerKind.Timer:
args = [op.trigger.delay];
break;
case DeferTriggerKind.Interaction:
case DeferTriggerKind.Hover:
case DeferTriggerKind.Viewport:
if (op.trigger.targetSlot?.slot == null || op.trigger.targetSlotViewSteps === null) {
throw new Error(`Slot or view steps not set in trigger reification for trigger kind ${op.trigger.kind}`);
}
args = [op.trigger.targetSlot.slot];
if (op.trigger.targetSlotViewSteps !== 0) {
args.push(op.trigger.targetSlotViewSteps);
}
break;
default:
throw new Error(`AssertionError: Unsupported reification of defer trigger kind ${op.trigger.kind}`);
}
OpList.replace(op, deferOn(op.trigger.kind, args, op.prefetch, op.sourceSpan));
break;
case OpKind.ProjectionDef:
OpList.replace(op, projectionDef(op.def));
break;
case OpKind.Projection:
if (op.handle.slot === null) {
throw new Error('No slot was assigned for project instruction');
}
OpList.replace(op, projection(op.handle.slot, op.projectionSlotIndex, op.attributes, op.sourceSpan));
break;
case OpKind.RepeaterCreate:
if (op.handle.slot === null) {
throw new Error('No slot was assigned for repeater instruction');
}
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
const repeaterView = unit.job.views.get(op.xref);
if (repeaterView.fnName === null) {
throw new Error(`AssertionError: expected repeater primary view to have been named`);
}
let emptyViewFnName = null;
let emptyDecls = null;
let emptyVars = null;
if (op.emptyView !== null) {
const emptyView = unit.job.views.get(op.emptyView);
if (emptyView === undefined) {
throw new Error('AssertionError: repeater had empty view xref, but empty view was not found');
}
if (emptyView.fnName === null || emptyView.decls === null || emptyView.vars === null) {
throw new Error(`AssertionError: expected repeater empty view to have been named and counted`);
}
emptyViewFnName = emptyView.fnName;
emptyDecls = emptyView.decls;
emptyVars = emptyView.vars;
}
OpList.replace(op, repeaterCreate(op.handle.slot, repeaterView.fnName, op.decls, op.vars, op.tag, op.attributes, op.trackByFn, op.usesComponentInstance, emptyViewFnName, emptyDecls, emptyVars, op.emptyTag, op.emptyAttributes, op.wholeSourceSpan));
break;
case OpKind.Statement:
// Pass statement operations directly through.
break;
default:
throw new Error(`AssertionError: Unsupported reification of create op ${OpKind[op.kind]}`);
}
}
}
function reifyUpdateOperations(_unit, ops) {
for (const op of ops) {
transformExpressionsInOp(op, reifyIrExpression, VisitorContextFlag.None);
switch (op.kind) {
case OpKind.Advance:
OpList.replace(op, advance(op.delta, op.sourceSpan));
break;
case OpKind.Property:
if (op.expression instanceof Interpolation) {
OpList.replace(op, propertyInterpolate(op.name, op.expression.strings, op.expression.expressions, op.sanitizer, op.sourceSpan));
}
else {
OpList.replace(op, property(op.name, op.expression, op.sanitizer, op.sourceSpan));
}
break;
case OpKind.StyleProp:
if (op.expression instanceof Interpolation) {
OpList.replace(op, stylePropInterpolate(op.name, op.expression.strings, op.expression.expressions, op.unit, op.sourceSpan));
}
else {
OpList.replace(op, styleProp(op.name, op.expression, op.unit, op.sourceSpan));
}
break;
case OpKind.ClassProp:
OpList.replace(op, classProp(op.name, op.expression, op.sourceSpan));
break;
case OpKind.StyleMap:
if (op.expression instanceof Interpolation) {
OpList.replace(op, styleMapInterpolate(op.expression.strings, op.expression.expressions, op.sourceSpan));
}
else {
OpList.replace(op, styleMap(op.expression, op.sourceSpan));
}
break;
case OpKind.ClassMap:
if (op.expression instanceof Interpolation) {
OpList.replace(op, classMapInterpolate(op.expression.strings, op.expression.expressions, op.sourceSpan));
}
else {
OpList.replace(op, classMap(op.expression, op.sourceSpan));
}
break;
case OpKind.I18nExpression:
OpList.replace(op, i18nExp(op.expression, op.sourceSpan));
break;
case OpKind.I18nApply:
OpList.replace(op, i18nApply(op.handle.slot, op.sourceSpan));
break;
case OpKind.InterpolateText:
OpList.replace(op, textInterpolate(op.interpolation.strings, op.interpolation.expressions, op.sourceSpan));
break;
case OpKind.Attribute:
if (op.expression instanceof Interpolation) {
OpList.replace(op, attributeInterpolate(op.name, op.expression.strings, op.expression.expressions, op.sanitizer, op.sourceSpan));
}
else {
OpList.replace(op, attribute(op.name, op.expression, op.sanitizer, op.namespace));
}
break;
case OpKind.HostProperty:
if (op.expression instanceof Interpolation) {
throw new Error('not yet handled');
}
else {
if (op.isAnimationTrigger) {
OpList.replace(op, syntheticHostProperty(op.name, op.expression, op.sourceSpan));
}
else {
OpList.replace(op, hostProperty(op.name, op.expression, op.sanitizer, op.sourceSpan));
}
}
break;
case OpKind.Variable:
if (op.variable.name === null) {
throw new Error(`AssertionError: unnamed variable ${op.xref}`);
}
OpList.replace(op, createStatementOp(new DeclareVarStmt(op.variable.name, op.initializer, undefined, StmtModifier.Final)));
break;
case OpKind.Conditional:
if (op.processed === null) {
throw new Error(`Conditional test was not set.`);
}
if (op.targetSlot.slot === null) {
throw new Error(`Conditional slot was not set.`);
}
OpList.replace(op, conditional(op.targetSlot.slot, op.processed, op.contextValue, op.sourceSpan));
break;
case OpKind.Repeater:
OpList.replace(op, repeater(op.collection, op.sourceSpan));
break;
case OpKind.DeferWhen:
OpList.replace(op, deferWhen(op.prefetch, op.expr, op.sourceSpan));
break;
case OpKind.Statement:
// Pass statement operations directly through.
break;
default:
throw new Error(`AssertionError: Unsupported reification of update op ${OpKind[op.kind]}`);
}
}
}
function reifyIrExpression(expr) {
if (!isIrExpression(expr)) {
return expr;
}
switch (expr.kind) {
case ExpressionKind.NextContext:
return nextContext(expr.steps);
case ExpressionKind.Reference:
return reference(expr.targetSlot.slot + 1 + expr.offset);
case ExpressionKind.LexicalRead:
throw new Error(`AssertionError: unresolved LexicalRead of ${expr.name}`);
case ExpressionKind.RestoreView:
if (typeof expr.view === 'number') {
throw new Error(`AssertionError: unresolved RestoreView`);
}
return restoreView(expr.view);
case ExpressionKind.ResetView:
return resetView(expr.expr);
case ExpressionKind.GetCurrentView:
return getCurrentView();
case ExpressionKind.ReadVariable:
if (expr.name === null) {
throw new Error(`Read of unnamed variable ${expr.xref}`);
}
return variable(expr.name);
case ExpressionKind.ReadTemporaryExpr:
if (expr.name === null) {
throw new Error(`Read of unnamed temporary ${expr.xref}`);
}
return variable(expr.name);
case ExpressionKind.AssignTemporaryExpr:
if (expr.name === null) {
throw new Error(`Assign of unnamed temporary ${expr.xref}`);
}
return variable(expr.name).set(expr.expr);
case ExpressionKind.PureFunctionExpr:
if (expr.fn === null) {
throw new Error(`AssertionError: expected PureFunctions to have been extracted`);
}
return pureFunction(expr.varOffset, expr.fn, expr.args);
case ExpressionKind.PureFunctionParameterExpr:
throw new Error(`AssertionError: expected PureFunctionParameterExpr to have been extracted`);
case ExpressionKind.PipeBinding:
return pipeBind(expr.targetSlot.slot, expr.varOffset, expr.args);
case ExpressionKind.PipeBindingVariadic:
return pipeBindV(expr.targetSlot.slot, expr.varOffset, expr.args);
case ExpressionKind.SlotLiteralExpr:
return literal(expr.slot.slot);
default:
throw new Error(`AssertionError: Unsupported reification of ir.Expression kind: ${ExpressionKind[expr.kind]}`);
}
}
/**
* Listeners get turned into a function expression, which may or may not have the `$event`
* parameter defined.
*/
function reifyListenerHandler(unit, name, handlerOps, consumesDollarEvent) {
// First, reify all instruction calls within `handlerOps`.
reifyUpdateOperations(unit, handlerOps);
// Next, extract all the `o.Statement`s from the reified operations. We can expect that at this
// point, all operations have been converted to statements.
const handlerStmts = [];
for (const op of handlerOps) {
if (op.kind !== OpKind.Statement) {
throw new Error(`AssertionError: expected reified statements, but found op ${OpKind[op.kind]}`);
}
handlerStmts.push(op.statement);
}
// If `$event` is referenced, we need to generate it as a parameter.
const params = [];
if (consumesDollarEvent) {
// We need the `$event` parameter.
params.push(new FnParam('$event'));
}
return fn(params, handlerStmts, undefined, undefined, name);
}
/**
* Bidningd with no content can be safely deleted.
*/
function removeEmptyBindings(job) {
for (const unit of job.units) {
for (const op of unit.update) {
switch (op.kind) {
case OpKind.Attribute:
case OpKind.Binding:
case OpKind.ClassProp:
case OpKind.ClassMap:
case OpKind.Property:
case OpKind.StyleProp:
case OpKind.StyleMap:
if (op.expression instanceof EmptyExpr) {
OpList.remove(op);
}
break;
}
}
}
}
/**
* Remove the i18n context ops after they are no longer needed, and null out references to them to
* be safe.
*/
function removeI18nContexts(job) {
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nContext:
OpList.remove(op);
break;
case OpKind.I18nStart:
op.context = null;
break;
}
}
}
}
/**
* i18nAttributes ops will be generated for each i18n attribute. However, not all i18n attribues
* will contain dynamic content, and so some of these i18nAttributes ops may be unnecessary.
*/
function removeUnusedI18nAttributesOps(job) {
for (const unit of job.units) {
const ownersWithI18nExpressions = new Set();
for (const op of unit.update) {
switch (op.kind) {
case OpKind.I18nExpression:
ownersWithI18nExpressions.add(op.i18nOwner);
}
}
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nAttributes:
if (ownersWithI18nExpressions.has(op.xref)) {
continue;
}
OpList.remove(op);
}
}
}
}
/**
* Resolves `ir.ContextExpr` expressions (which represent embedded view or component contexts) to
* either the `ctx` parameter to component functions (for the current view context) or to variables
* that store those contexts (for contexts accessed via the `nextContext()` instruction).
*/
function resolveContexts(job) {
for (const unit of job.units) {
processLexicalScope$1(unit, unit.create);
processLexicalScope$1(unit, unit.update);
}
}
function processLexicalScope$1(view, ops) {
// Track the expressions used to access all available contexts within the current view, by the
// view `ir.XrefId`.
const scope = new Map();
// The current view's context is accessible via the `ctx` parameter.
scope.set(view.xref, variable('ctx'));
for (const op of ops) {
switch (op.kind) {
case OpKind.Variable:
switch (op.variable.kind) {
case SemanticVariableKind.Context:
scope.set(op.variable.view, new ReadVariableExpr(op.xref));
break;
}
break;
case OpKind.Listener:
processLexicalScope$1(view, op.handlerOps);
break;
}
}
if (view === view.job.root) {
// Prefer `ctx` of the root view to any variables which happen to contain the root context.
scope.set(view.xref, variable('ctx'));
}
for (const op of ops) {
transformExpressionsInOp(op, expr => {
if (expr instanceof ContextExpr) {
if (!scope.has(expr.view)) {
throw new Error(`No context found for reference to view ${expr.view} from view ${view.xref}`);
}
return scope.get(expr.view);
}
else {
return expr;
}
}, VisitorContextFlag.None);
}
}
/**
* Any variable inside a listener with the name `$event` will be transformed into a output lexical
* read immediately, and does not participate in any of the normal logic for handling variables.
*/
function resolveDollarEvent(job) {
for (const unit of job.units) {
transformDollarEvent(unit, unit.create);
transformDollarEvent(unit, unit.update);
}
}
function transformDollarEvent(unit, ops) {
for (const op of ops) {
if (op.kind === OpKind.Listener) {
transformExpressionsInOp(op, (expr) => {
if (expr instanceof LexicalReadExpr && expr.name === '$event') {
op.consumesDollarEvent = true;
return new ReadVarExpr(expr.name);
}
return expr;
}, VisitorContextFlag.InChildOperation);
}
}
}
/**
* Resolve the element placeholders in i18n messages.
*/
function resolveI18nElementPlaceholders(job) {
// Record all of the element and i18n context ops for use later.
const i18nContexts = new Map();
const elements = new Map();
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nContext:
i18nContexts.set(op.xref, op);
break;
case OpKind.ElementStart:
elements.set(op.xref, op);
break;
}
}
}
resolvePlaceholdersForView(job, job.root, i18nContexts, elements);
}
/**
* Recursively resolves element and template tag placeholders in the given view.
*/
function resolvePlaceholdersForView(job, unit, i18nContexts, elements, pendingStructuralDirective) {
// Track the current i18n op and corresponding i18n context op as we step through the creation
// IR.
let currentOps = null;
let pendingStructuralDirectiveCloses = new Map();
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nStart:
if (!op.context) {
throw Error('Could not find i18n context for i18n op');
}
currentOps = { i18nBlock: op, i18nContext: i18nContexts.get(op.context) };
break;
case OpKind.I18nEnd:
currentOps = null;
break;
case OpKind.ElementStart:
// For elements with i18n placeholders, record its slot value in the params map under the
// corresponding tag start placeholder.
if (op.i18nPlaceholder !== undefined) {
if (currentOps === null) {
throw Error('i18n tag placeholder should only occur inside an i18n block');
}
recordElementStart(op, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
// If there is a separate close tag placeholder for this element, save the pending
// structural directive so we can pass it to the closing tag as well.
if (pendingStructuralDirective && op.i18nPlaceholder.closeName) {
pendingStructuralDirectiveCloses.set(op.xref, pendingStructuralDirective);
}
// Clear out the pending structural directive now that its been accounted for.
pendingStructuralDirective = undefined;
}
break;
case OpKind.ElementEnd:
// For elements with i18n placeholders, record its slot value in the params map under the
// corresponding tag close placeholder.
const startOp = elements.get(op.xref);
if (startOp && startOp.i18nPlaceholder !== undefined) {
if (currentOps === null) {
throw Error('AssertionError: i18n tag placeholder should only occur inside an i18n block');
}
recordElementClose(startOp, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirectiveCloses.get(op.xref));
// Clear out the pending structural directive close that was accounted for.
pendingStructuralDirectiveCloses.delete(op.xref);
}
break;
case OpKind.Projection:
// For content projections with i18n placeholders, record its slot value in the params map
// under the corresponding tag start and close placeholders.
if (op.i18nPlaceholder !== undefined) {
if (currentOps === null) {
throw Error('i18n tag placeholder should only occur inside an i18n block');
}
recordElementStart(op, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
recordElementClose(op, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
// Clear out the pending structural directive now that its been accounted for.
pendingStructuralDirective = undefined;
}
break;
case OpKind.Template:
const view = job.views.get(op.xref);
if (op.i18nPlaceholder === undefined) {
// If there is no i18n placeholder, just recurse into the view in case it contains i18n
// blocks.
resolvePlaceholdersForView(job, view, i18nContexts, elements);
}
else {
if (currentOps === null) {
throw Error('i18n tag placeholder should only occur inside an i18n block');
}
if (op.templateKind === TemplateKind.Structural) {
// If this is a structural directive template, don't record anything yet. Instead pass
// the current template as a pending structural directive to be recorded when we find
// the element, content, or template it belongs to. This allows us to create combined
// values that represent, e.g. the start of a template and element at the same time.
resolvePlaceholdersForView(job, view, i18nContexts, elements, op);
}
else {
// If this is some other kind of template, we can record its start, recurse into its
// view, and then record its end.
recordTemplateStart(job, view, op.handle.slot, op.i18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
resolvePlaceholdersForView(job, view, i18nContexts, elements);
recordTemplateClose(job, view, op.handle.slot, op.i18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
pendingStructuralDirective = undefined;
}
}
break;
case OpKind.RepeaterCreate:
if (pendingStructuralDirective !== undefined) {
throw Error('AssertionError: Unexpected structural directive associated with @for block');
}
// RepeaterCreate has 3 slots: the first is for the op itself, the second is for the @for
// template and the (optional) third is for the @empty template.
const forSlot = op.handle.slot + 1;
const forView = job.views.get(op.xref);
// First record all of the placeholders for the @for template.
if (op.i18nPlaceholder === undefined) {
// If there is no i18n placeholder, just recurse into the view in case it contains i18n
// blocks.
resolvePlaceholdersForView(job, forView, i18nContexts, elements);
}
else {
if (currentOps === null) {
throw Error('i18n tag placeholder should only occur inside an i18n block');
}
recordTemplateStart(job, forView, forSlot, op.i18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
resolvePlaceholdersForView(job, forView, i18nContexts, elements);
recordTemplateClose(job, forView, forSlot, op.i18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
pendingStructuralDirective = undefined;
}
// Then if there's an @empty template, add its placeholders as well.
if (op.emptyView !== null) {
// RepeaterCreate has 3 slots: the first is for the op itself, the second is for the @for
// template and the (optional) third is for the @empty template.
const emptySlot = op.handle.slot + 2;
const emptyView = job.views.get(op.emptyView);
if (op.emptyI18nPlaceholder === undefined) {
// If there is no i18n placeholder, just recurse into the view in case it contains i18n
// blocks.
resolvePlaceholdersForView(job, emptyView, i18nContexts, elements);
}
else {
if (currentOps === null) {
throw Error('i18n tag placeholder should only occur inside an i18n block');
}
recordTemplateStart(job, emptyView, emptySlot, op.emptyI18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
resolvePlaceholdersForView(job, emptyView, i18nContexts, elements);
recordTemplateClose(job, emptyView, emptySlot, op.emptyI18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
pendingStructuralDirective = undefined;
}
}
break;
}
}
}
/**
* Records an i18n param value for the start of an element.
*/
function recordElementStart(op, i18nContext, i18nBlock, structuralDirective) {
const { startName, closeName } = op.i18nPlaceholder;
let flags = I18nParamValueFlags.ElementTag | I18nParamValueFlags.OpenTag;
let value = op.handle.slot;
// If the element is associated with a structural directive, start it as well.
if (structuralDirective !== undefined) {
flags |= I18nParamValueFlags.TemplateTag;
value = { element: value, template: structuralDirective.handle.slot };
}
// For self-closing tags, there is no close tag placeholder. Instead, the start tag
// placeholder accounts for the start and close of the element.
if (!closeName) {
flags |= I18nParamValueFlags.CloseTag;
}
addParam(i18nContext.params, startName, value, i18nBlock.subTemplateIndex, flags);
}
/**
* Records an i18n param value for the closing of an element.
*/
function recordElementClose(op, i18nContext, i18nBlock, structuralDirective) {
const { closeName } = op.i18nPlaceholder;
// Self-closing tags don't have a closing tag placeholder, instead the element closing is
// recorded via an additional flag on the element start value.
if (closeName) {
let flags = I18nParamValueFlags.ElementTag | I18nParamValueFlags.CloseTag;
let value = op.handle.slot;
// If the element is associated with a structural directive, close it as well.
if (structuralDirective !== undefined) {
flags |= I18nParamValueFlags.TemplateTag;
value = { element: value, template: structuralDirective.handle.slot };
}
addParam(i18nContext.params, closeName, value, i18nBlock.subTemplateIndex, flags);
}
}
/**
* Records an i18n param value for the start of a template.
*/
function recordTemplateStart(job, view, slot, i18nPlaceholder, i18nContext, i18nBlock, structuralDirective) {
let { startName, closeName } = i18nPlaceholder;
let flags = I18nParamValueFlags.TemplateTag | I18nParamValueFlags.OpenTag;
// For self-closing tags, there is no close tag placeholder. Instead, the start tag
// placeholder accounts for the start and close of the element.
if (!closeName) {
flags |= I18nParamValueFlags.CloseTag;
}
// If the template is associated with a structural directive, record the structural directive's
// start first. Since this template must be in the structural directive's view, we can just
// directly use the current i18n block's sub-template index.
if (structuralDirective !== undefined) {
addParam(i18nContext.params, startName, structuralDirective.handle.slot, i18nBlock.subTemplateIndex, flags);
}
// Record the start of the template. For the sub-template index, pass the index for the template's
// view, rather than the current i18n block's index.
addParam(i18nContext.params, startName, slot, getSubTemplateIndexForTemplateTag(job, i18nBlock, view), flags);
}
/**
* Records an i18n param value for the closing of a template.
*/
function recordTemplateClose(job, view, slot, i18nPlaceholder, i18nContext, i18nBlock, structuralDirective) {
const { startName, closeName } = i18nPlaceholder;
const flags = I18nParamValueFlags.TemplateTag | I18nParamValueFlags.CloseTag;
// Self-closing tags don't have a closing tag placeholder, instead the template's closing is
// recorded via an additional flag on the template start value.
if (closeName) {
// Record the closing of the template. For the sub-template index, pass the index for the
// template's view, rather than the current i18n block's index.
addParam(i18nContext.params, closeName, slot, getSubTemplateIndexForTemplateTag(job, i18nBlock, view), flags);
// If the template is associated with a structural directive, record the structural directive's
// closing after. Since this template must be in the structural directive's view, we can just
// directly use the current i18n block's sub-template index.
if (structuralDirective !== undefined) {
addParam(i18nContext.params, closeName, structuralDirective.handle.slot, i18nBlock.subTemplateIndex, flags);
}
}
}
/**
* Get the subTemplateIndex for the given template op. For template ops, use the subTemplateIndex of
* the child i18n block inside the template.
*/
function getSubTemplateIndexForTemplateTag(job, i18nOp, view) {
for (const childOp of view.create) {
if (childOp.kind === OpKind.I18nStart) {
return childOp.subTemplateIndex;
}
}
return i18nOp.subTemplateIndex;
}
/**
* Add a param value to the given params map.
*/
function addParam(params, placeholder, value, subTemplateIndex, flags) {
const values = params.get(placeholder) ?? [];
values.push({ value, subTemplateIndex, flags });
params.set(placeholder, values);
}
/**
* Resolve the i18n expression placeholders in i18n messages.
*/
function resolveI18nExpressionPlaceholders(job) {
// Record all of the i18n context ops, and the sub-template index for each i18n op.
const subTemplateIndicies = new Map();
const i18nContexts = new Map();
const icuPlaceholders = new Map();
for (const unit of job.units) {
for (const op of unit.create) {
switch (op.kind) {
case OpKind.I18nStart:
subTemplateIndicies.set(op.xref, op.subTemplateIndex);
break;
case OpKind.I18nContext:
i18nContexts.set(op.xref, op);
break;
case OpKind.IcuPlaceholder:
icuPlaceholders.set(op.xref, op);
break;
}
}
}
// Keep track of the next available expression index for each i18n message.
const expressionIndices = new Map();
// Keep track of a reference index for each expression.
// We use different references for normal i18n expressio and attribute i18n expressions. This is
// because child i18n blocks in templates don't get their own context, since they're rolled into
// the translated message of the parent, but they may target a different slot.
const referenceIndex = (op) => op.usage === I18nExpressionFor.I18nText ? op.i18nOwner : op.context;
for (const unit of job.units) {
for (const op of unit.update) {
if (op.kind === OpKind.I18nExpression) {
const index = expressionIndices.get(referenceIndex(op)) || 0;
const subTemplateIndex = subTemplateIndicies.get(op.i18nOwner) ?? null;
const value = {
value: index,
subTemplateIndex: subTemplateIndex,
flags: I18nParamValueFlags.ExpressionIndex
};
updatePlaceholder(op, value, i18nContexts, icuPlaceholders);
expressionIndices.set(referenceIndex(op), index + 1);
}
}
}
}
function updatePlaceholder(op, value, i18nContexts, icuPlaceholders) {
if (op.i18nPlaceholder !== null) {
const i18nContext = i18nContexts.get(op.context);
const params = op.resolutionTime === I18nParamResolutionTime.Creation ?
i18nContext.params :
i18nContext.postprocessingParams;
const values = params.get(op.i18nPlaceholder) || [];
values.push(value);
params.set(op.i18nPlaceholder, values);
}
if (op.icuPlaceholder !== null) {
const icuPlaceholderOp = icuPlaceholders.get(op.icuPlaceholder);
icuPlaceholderOp?.expressionPlaceholders.push(value);
}
}
/**
* Resolves lexical references in views (`ir.LexicalReadExpr`) to either a target variable or to
* property reads on the top-level component context.
*
* Also matches `ir.RestoreViewExpr` expressions with the variables of their corresponding saved
* views.
*/
function resolveNames(job) {
for (const unit of job.units) {
processLexicalScope(unit, unit.create, null);
processLexicalScope(unit, unit.update, null);
}
}
function processLexicalScope(unit, ops, savedView) {
// Maps names defined in the lexical scope of this template to the `ir.XrefId`s of the variable
// declarations which represent those values.
//
// Since variables are generated in each view for the entire lexical scope (including any
// identifiers from parent templates) only local variables need be considered here.
const scope = new Map();
// First, step through the operations list and:
// 1) build up the `scope` mapping
// 2) recurse into any listener functions
for (const op of ops) {
switch (op.kind) {
case OpKind.Variable:
switch (op.variable.kind) {
case SemanticVariableKind.Identifier:
case SemanticVariableKind.Alias:
// This variable represents some kind of identifier which can be used in the template.
if (scope.has(op.variable.identifier)) {
continue;
}
scope.set(op.variable.identifier, op.xref);
break;
case SemanticVariableKind.SavedView:
// This variable represents a snapshot of the current view context, and can be used to
// restore that context within listener functions.
savedView = {
view: op.variable.view,
variable: op.xref,
};
break;
}
break;
case OpKind.Listener:
// Listener functions have separate variable declarations, so process them as a separate
// lexical scope.
processLexicalScope(unit, op.handlerOps, savedView);
break;
}
}
// Next, use the `scope` mapping to match `ir.LexicalReadExpr` with defined names in the lexical
// scope. Also, look for `ir.RestoreViewExpr`s and match them with the snapshotted view context
// variable.
for (const op of ops) {
if (op.kind == OpKind.Listener) {
// Listeners were already processed above with their own scopes.
continue;
}
transformExpressionsInOp(op, (expr, flags) => {
if (expr instanceof LexicalReadExpr) {
// `expr` is a read of a name within the lexical scope of this view.
// Either that name is defined within the current view, or it represents a property from the
// main component context.
if (scope.has(expr.name)) {
// This was a defined variable in the current scope.
return new ReadVariableExpr(scope.get(expr.name));
}
else {
// Reading from the component context.
return new ReadPropExpr(new ContextExpr(unit.job.root.xref), expr.name);
}
}
else if (expr instanceof RestoreViewExpr && typeof expr.view === 'number') {
// `ir.RestoreViewExpr` happens in listener functions and restores a saved view from the
// parent creation list. We expect to find that we captured the `savedView` previously, and
// that it matches the expected view to be restored.
if (savedView === null || savedView.view !== expr.view) {
throw new Error(`AssertionError: no saved view ${expr.view} from view ${unit.xref}`);
}
expr.view = new ReadVariableExpr(savedView.variable);
return expr;
}
else {
return expr;
}
}, VisitorContextFlag.None);
}
for (const op of ops) {
visitExpressionsInOp(op, expr => {
if (expr instanceof LexicalReadExpr) {
throw new Error(`AssertionError: no lexical reads should remain, but found read of ${expr.name}`);
}
});
}
}
/**
* Map of security contexts to their sanitizer function.
*/
const sanitizerFns = new Map([
[SecurityContext.HTML, Identifiers.sanitizeHtml],
[SecurityContext.RESOURCE_URL, Identifiers.sanitizeResourceUrl],
[SecurityContext.SCRIPT, Identifiers.sanitizeScript],
[SecurityContext.STYLE, Identifiers.sanitizeStyle], [SecurityContext.URL, Identifiers.sanitizeUrl]
]);
/**
* Map of security contexts to their trusted value function.
*/
const trustedValueFns = new Map([
[SecurityContext.HTML, Identifiers.trustConstantHtml],
[SecurityContext.RESOURCE_URL, Identifiers.trustConstantResourceUrl],
]);
/**
* Resolves sanitization functions for ops that need them.
*/
function resolveSanitizers(job) {
for (const unit of job.units) {
const elements = createOpXrefMap(unit);
// For normal element bindings we create trusted values for security sensitive constant
// attributes. However, for host bindings we skip this step (this matches what
// TemplateDefinitionBuilder does).
// TODO: Is the TDB behavior correct here?
if (job.kind !== CompilationJobKind.Host) {
for (const op of unit.create) {
if (op.kind === OpKind.ExtractedAttribute) {
const trustedValueFn = trustedValueFns.get(getOnlySecurityContext(op.securityContext)) ?? null;
op.trustedValueFn = trustedValueFn !== null ? importExpr(trustedValueFn) : null;
}
}
}
for (const op of unit.update) {
switch (op.kind) {
case OpKind.Property:
case OpKind.Attribute:
case OpKind.HostProperty:
let sanitizerFn = null;
if (Array.isArray(op.securityContext) && op.securityContext.length === 2 &&
op.securityContext.indexOf(SecurityContext.URL) > -1 &&
op.securityContext.indexOf(SecurityContext.RESOURCE_URL) > -1) {
// When the host element isn't known, some URL attributes (such as "src" and "href") may
// be part of multiple different security contexts. In this case we use special
// sanitization function and select the actual sanitizer at runtime based on a tag name
// that is provided while invoking sanitization function.
sanitizerFn = Identifiers.sanitizeUrlOrResourceUrl;
}
else {
sanitizerFn = sanitizerFns.get(getOnlySecurityContext(op.securityContext)) ?? null;
}
op.sanitizer = sanitizerFn !== null ? importExpr(sanitizerFn) : null;
// If there was no sanitization function found based on the security context of an
// attribute/property, check whether this attribute/property is one of the
// security-sensitive