35193 lines
1.4 MiB
Executable File
35193 lines
1.4 MiB
Executable File
/**
|
|
* @license Angular v20.3.11
|
|
* (c) 2010-2025 Google LLC. https://angular.dev/
|
|
* License: MIT
|
|
*/
|
|
|
|
const _SELECTOR_REGEXP = new RegExp('(\\:not\\()|' + // 1: ":not("
|
|
'(([\\.\\#]?)[-\\w]+)|' + // 2: "tag"; 3: "."/"#";
|
|
// "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range
|
|
// 4: attribute; 5: attribute_string; 6: attribute_value
|
|
'(?:\\[([-.\\w*\\\\$]+)(?:=(["\']?)([^\\]"\']*)\\5)?\\])|' + // "[name]", "[name=value]",
|
|
// "[name="value"]",
|
|
// "[name='value']"
|
|
'(\\))|' + // 7: ")"
|
|
'(\\s*,\\s*)', // 8: ","
|
|
'g');
|
|
/**
|
|
* A css selector contains an element name,
|
|
* css classes and attribute/value pairs with the purpose
|
|
* of selecting subsets out of them.
|
|
*/
|
|
class CssSelector {
|
|
element = null;
|
|
classNames = [];
|
|
/**
|
|
* The selectors are encoded in pairs where:
|
|
* - even locations are attribute names
|
|
* - odd locations are attribute values.
|
|
*
|
|
* Example:
|
|
* Selector: `[key1=value1][key2]` would parse to:
|
|
* ```
|
|
* ['key1', 'value1', 'key2', '']
|
|
* ```
|
|
*/
|
|
attrs = [];
|
|
notSelectors = [];
|
|
static parse(selector) {
|
|
const results = [];
|
|
const _addResult = (res, cssSel) => {
|
|
if (cssSel.notSelectors.length > 0 &&
|
|
!cssSel.element &&
|
|
cssSel.classNames.length == 0 &&
|
|
cssSel.attrs.length == 0) {
|
|
cssSel.element = '*';
|
|
}
|
|
res.push(cssSel);
|
|
};
|
|
let cssSelector = new CssSelector();
|
|
let match;
|
|
let current = cssSelector;
|
|
let inNot = false;
|
|
_SELECTOR_REGEXP.lastIndex = 0;
|
|
while ((match = _SELECTOR_REGEXP.exec(selector))) {
|
|
if (match[1 /* SelectorRegexp.NOT */]) {
|
|
if (inNot) {
|
|
throw new Error('Nesting :not in a selector is not allowed');
|
|
}
|
|
inNot = true;
|
|
current = new CssSelector();
|
|
cssSelector.notSelectors.push(current);
|
|
}
|
|
const tag = match[2 /* SelectorRegexp.TAG */];
|
|
if (tag) {
|
|
const prefix = match[3 /* SelectorRegexp.PREFIX */];
|
|
if (prefix === '#') {
|
|
// #hash
|
|
current.addAttribute('id', tag.slice(1));
|
|
}
|
|
else if (prefix === '.') {
|
|
// Class
|
|
current.addClassName(tag.slice(1));
|
|
}
|
|
else {
|
|
// Element
|
|
current.setElement(tag);
|
|
}
|
|
}
|
|
const attribute = match[4 /* SelectorRegexp.ATTRIBUTE */];
|
|
if (attribute) {
|
|
current.addAttribute(current.unescapeAttribute(attribute), match[6 /* SelectorRegexp.ATTRIBUTE_VALUE */]);
|
|
}
|
|
if (match[7 /* SelectorRegexp.NOT_END */]) {
|
|
inNot = false;
|
|
current = cssSelector;
|
|
}
|
|
if (match[8 /* SelectorRegexp.SEPARATOR */]) {
|
|
if (inNot) {
|
|
throw new Error('Multiple selectors in :not are not supported');
|
|
}
|
|
_addResult(results, cssSelector);
|
|
cssSelector = current = new CssSelector();
|
|
}
|
|
}
|
|
_addResult(results, cssSelector);
|
|
return results;
|
|
}
|
|
/**
|
|
* Unescape `\$` sequences from the CSS attribute selector.
|
|
*
|
|
* This is needed because `$` can have a special meaning in CSS selectors,
|
|
* but we might want to match an attribute that contains `$`.
|
|
* [MDN web link for more
|
|
* info](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors).
|
|
* @param attr the attribute to unescape.
|
|
* @returns the unescaped string.
|
|
*/
|
|
unescapeAttribute(attr) {
|
|
let result = '';
|
|
let escaping = false;
|
|
for (let i = 0; i < attr.length; i++) {
|
|
const char = attr.charAt(i);
|
|
if (char === '\\') {
|
|
escaping = true;
|
|
continue;
|
|
}
|
|
if (char === '$' && !escaping) {
|
|
throw new Error(`Error in attribute selector "${attr}". ` +
|
|
`Unescaped "$" is not supported. Please escape with "\\$".`);
|
|
}
|
|
escaping = false;
|
|
result += char;
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Escape `$` sequences from the CSS attribute selector.
|
|
*
|
|
* This is needed because `$` can have a special meaning in CSS selectors,
|
|
* with this method we are escaping `$` with `\$'.
|
|
* [MDN web link for more
|
|
* info](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors).
|
|
* @param attr the attribute to escape.
|
|
* @returns the escaped string.
|
|
*/
|
|
escapeAttribute(attr) {
|
|
return attr.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
|
|
}
|
|
isElementSelector() {
|
|
return (this.hasElementSelector() &&
|
|
this.classNames.length == 0 &&
|
|
this.attrs.length == 0 &&
|
|
this.notSelectors.length === 0);
|
|
}
|
|
hasElementSelector() {
|
|
return !!this.element;
|
|
}
|
|
setElement(element = null) {
|
|
this.element = element;
|
|
}
|
|
getAttrs() {
|
|
const result = [];
|
|
if (this.classNames.length > 0) {
|
|
result.push('class', this.classNames.join(' '));
|
|
}
|
|
return result.concat(this.attrs);
|
|
}
|
|
addAttribute(name, value = '') {
|
|
this.attrs.push(name, (value && value.toLowerCase()) || '');
|
|
}
|
|
addClassName(name) {
|
|
this.classNames.push(name.toLowerCase());
|
|
}
|
|
toString() {
|
|
let res = this.element || '';
|
|
if (this.classNames) {
|
|
this.classNames.forEach((klass) => (res += `.${klass}`));
|
|
}
|
|
if (this.attrs) {
|
|
for (let i = 0; i < this.attrs.length; i += 2) {
|
|
const name = this.escapeAttribute(this.attrs[i]);
|
|
const value = this.attrs[i + 1];
|
|
res += `[${name}${value ? '=' + value : ''}]`;
|
|
}
|
|
}
|
|
this.notSelectors.forEach((notSelector) => (res += `:not(${notSelector})`));
|
|
return res;
|
|
}
|
|
}
|
|
/**
|
|
* Reads a list of CssSelectors and allows to calculate which ones
|
|
* are contained in a given CssSelector.
|
|
*/
|
|
class SelectorMatcher {
|
|
static createNotMatcher(notSelectors) {
|
|
const notMatcher = new SelectorMatcher();
|
|
notMatcher.addSelectables(notSelectors, null);
|
|
return notMatcher;
|
|
}
|
|
_elementMap = new Map();
|
|
_elementPartialMap = new Map();
|
|
_classMap = new Map();
|
|
_classPartialMap = new Map();
|
|
_attrValueMap = new Map();
|
|
_attrValuePartialMap = new Map();
|
|
_listContexts = [];
|
|
addSelectables(cssSelectors, callbackCtxt) {
|
|
let listContext = null;
|
|
if (cssSelectors.length > 1) {
|
|
listContext = new SelectorListContext(cssSelectors);
|
|
this._listContexts.push(listContext);
|
|
}
|
|
for (let i = 0; i < cssSelectors.length; i++) {
|
|
this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
|
|
}
|
|
}
|
|
/**
|
|
* Add an object that can be found later on by calling `match`.
|
|
* @param cssSelector A css selector
|
|
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
|
|
*/
|
|
_addSelectable(cssSelector, callbackCtxt, listContext) {
|
|
let matcher = this;
|
|
const element = cssSelector.element;
|
|
const classNames = cssSelector.classNames;
|
|
const attrs = cssSelector.attrs;
|
|
const selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
|
|
if (element) {
|
|
const isTerminal = attrs.length === 0 && classNames.length === 0;
|
|
if (isTerminal) {
|
|
this._addTerminal(matcher._elementMap, element, selectable);
|
|
}
|
|
else {
|
|
matcher = this._addPartial(matcher._elementPartialMap, element);
|
|
}
|
|
}
|
|
if (classNames) {
|
|
for (let i = 0; i < classNames.length; i++) {
|
|
const isTerminal = attrs.length === 0 && i === classNames.length - 1;
|
|
const className = classNames[i];
|
|
if (isTerminal) {
|
|
this._addTerminal(matcher._classMap, className, selectable);
|
|
}
|
|
else {
|
|
matcher = this._addPartial(matcher._classPartialMap, className);
|
|
}
|
|
}
|
|
}
|
|
if (attrs) {
|
|
for (let i = 0; i < attrs.length; i += 2) {
|
|
const isTerminal = i === attrs.length - 2;
|
|
const name = attrs[i];
|
|
const value = attrs[i + 1];
|
|
if (isTerminal) {
|
|
const terminalMap = matcher._attrValueMap;
|
|
let terminalValuesMap = terminalMap.get(name);
|
|
if (!terminalValuesMap) {
|
|
terminalValuesMap = new Map();
|
|
terminalMap.set(name, terminalValuesMap);
|
|
}
|
|
this._addTerminal(terminalValuesMap, value, selectable);
|
|
}
|
|
else {
|
|
const partialMap = matcher._attrValuePartialMap;
|
|
let partialValuesMap = partialMap.get(name);
|
|
if (!partialValuesMap) {
|
|
partialValuesMap = new Map();
|
|
partialMap.set(name, partialValuesMap);
|
|
}
|
|
matcher = this._addPartial(partialValuesMap, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_addTerminal(map, name, selectable) {
|
|
let terminalList = map.get(name);
|
|
if (!terminalList) {
|
|
terminalList = [];
|
|
map.set(name, terminalList);
|
|
}
|
|
terminalList.push(selectable);
|
|
}
|
|
_addPartial(map, name) {
|
|
let matcher = map.get(name);
|
|
if (!matcher) {
|
|
matcher = new SelectorMatcher();
|
|
map.set(name, matcher);
|
|
}
|
|
return matcher;
|
|
}
|
|
/**
|
|
* Find the objects that have been added via `addSelectable`
|
|
* whose css selector is contained in the given css selector.
|
|
* @param cssSelector A css selector
|
|
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
|
|
* @return boolean true if a match was found
|
|
*/
|
|
match(cssSelector, matchedCallback) {
|
|
let result = false;
|
|
const element = cssSelector.element;
|
|
const classNames = cssSelector.classNames;
|
|
const attrs = cssSelector.attrs;
|
|
for (let i = 0; i < this._listContexts.length; i++) {
|
|
this._listContexts[i].alreadyMatched = false;
|
|
}
|
|
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
|
|
result =
|
|
this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result;
|
|
if (classNames) {
|
|
for (let i = 0; i < classNames.length; i++) {
|
|
const className = classNames[i];
|
|
result =
|
|
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
|
|
result =
|
|
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
|
|
result;
|
|
}
|
|
}
|
|
if (attrs) {
|
|
for (let i = 0; i < attrs.length; i += 2) {
|
|
const name = attrs[i];
|
|
const value = attrs[i + 1];
|
|
const terminalValuesMap = this._attrValueMap.get(name);
|
|
if (value) {
|
|
result =
|
|
this._matchTerminal(terminalValuesMap, '', cssSelector, matchedCallback) || result;
|
|
}
|
|
result =
|
|
this._matchTerminal(terminalValuesMap, value, cssSelector, matchedCallback) || result;
|
|
const partialValuesMap = this._attrValuePartialMap.get(name);
|
|
if (value) {
|
|
result = this._matchPartial(partialValuesMap, '', cssSelector, matchedCallback) || result;
|
|
}
|
|
result =
|
|
this._matchPartial(partialValuesMap, value, cssSelector, matchedCallback) || result;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/** @internal */
|
|
_matchTerminal(map, name, cssSelector, matchedCallback) {
|
|
if (!map || typeof name !== 'string') {
|
|
return false;
|
|
}
|
|
let selectables = map.get(name) || [];
|
|
const starSelectables = map.get('*');
|
|
if (starSelectables) {
|
|
selectables = selectables.concat(starSelectables);
|
|
}
|
|
if (selectables.length === 0) {
|
|
return false;
|
|
}
|
|
let selectable;
|
|
let result = false;
|
|
for (let i = 0; i < selectables.length; i++) {
|
|
selectable = selectables[i];
|
|
result = selectable.finalize(cssSelector, matchedCallback) || result;
|
|
}
|
|
return result;
|
|
}
|
|
/** @internal */
|
|
_matchPartial(map, name, cssSelector, matchedCallback) {
|
|
if (!map || typeof name !== 'string') {
|
|
return false;
|
|
}
|
|
const nestedSelector = map.get(name);
|
|
if (!nestedSelector) {
|
|
return false;
|
|
}
|
|
// TODO(perf): get rid of recursion and measure again
|
|
// TODO(perf): don't pass the whole selector into the recursion,
|
|
// but only the not processed parts
|
|
return nestedSelector.match(cssSelector, matchedCallback);
|
|
}
|
|
}
|
|
class SelectorListContext {
|
|
selectors;
|
|
alreadyMatched = false;
|
|
constructor(selectors) {
|
|
this.selectors = selectors;
|
|
}
|
|
}
|
|
// Store context to pass back selector and context when a selector is matched
|
|
class SelectorContext {
|
|
selector;
|
|
cbContext;
|
|
listContext;
|
|
notSelectors;
|
|
constructor(selector, cbContext, listContext) {
|
|
this.selector = selector;
|
|
this.cbContext = cbContext;
|
|
this.listContext = listContext;
|
|
this.notSelectors = selector.notSelectors;
|
|
}
|
|
finalize(cssSelector, callback) {
|
|
let result = true;
|
|
if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) {
|
|
const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
|
|
result = !notMatcher.match(cssSelector, null);
|
|
}
|
|
if (result && callback && (!this.listContext || !this.listContext.alreadyMatched)) {
|
|
if (this.listContext) {
|
|
this.listContext.alreadyMatched = true;
|
|
}
|
|
callback(this.selector, this.cbContext);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
class SelectorlessMatcher {
|
|
registry;
|
|
constructor(registry) {
|
|
this.registry = registry;
|
|
}
|
|
match(name) {
|
|
return this.registry.has(name) ? this.registry.get(name) : [];
|
|
}
|
|
}
|
|
|
|
// Attention:
|
|
// This file duplicates types and values from @angular/core
|
|
// so that we are able to make @angular/compiler independent of @angular/core.
|
|
// This is important to prevent a build cycle, as @angular/core needs to
|
|
// be compiled with the compiler.
|
|
// Stores the default value of `emitDistinctChangesOnly` when the `emitDistinctChangesOnly` is not
|
|
// explicitly set.
|
|
const emitDistinctChangesOnlyDefaultValue = true;
|
|
var ViewEncapsulation$1;
|
|
(function (ViewEncapsulation) {
|
|
ViewEncapsulation[ViewEncapsulation["Emulated"] = 0] = "Emulated";
|
|
// Historically the 1 value was for `Native` encapsulation which has been removed as of v11.
|
|
ViewEncapsulation[ViewEncapsulation["None"] = 2] = "None";
|
|
ViewEncapsulation[ViewEncapsulation["ShadowDom"] = 3] = "ShadowDom";
|
|
})(ViewEncapsulation$1 || (ViewEncapsulation$1 = {}));
|
|
var ChangeDetectionStrategy;
|
|
(function (ChangeDetectionStrategy) {
|
|
ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
|
|
ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
|
|
})(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
|
|
/** Flags describing an input for a directive. */
|
|
var InputFlags;
|
|
(function (InputFlags) {
|
|
InputFlags[InputFlags["None"] = 0] = "None";
|
|
InputFlags[InputFlags["SignalBased"] = 1] = "SignalBased";
|
|
InputFlags[InputFlags["HasDecoratorInputTransform"] = 2] = "HasDecoratorInputTransform";
|
|
})(InputFlags || (InputFlags = {}));
|
|
const CUSTOM_ELEMENTS_SCHEMA = {
|
|
name: 'custom-elements',
|
|
};
|
|
const NO_ERRORS_SCHEMA = {
|
|
name: 'no-errors-schema',
|
|
};
|
|
const Type$1 = Function;
|
|
var SecurityContext;
|
|
(function (SecurityContext) {
|
|
SecurityContext[SecurityContext["NONE"] = 0] = "NONE";
|
|
SecurityContext[SecurityContext["HTML"] = 1] = "HTML";
|
|
SecurityContext[SecurityContext["STYLE"] = 2] = "STYLE";
|
|
SecurityContext[SecurityContext["SCRIPT"] = 3] = "SCRIPT";
|
|
SecurityContext[SecurityContext["URL"] = 4] = "URL";
|
|
SecurityContext[SecurityContext["RESOURCE_URL"] = 5] = "RESOURCE_URL";
|
|
})(SecurityContext || (SecurityContext = {}));
|
|
var MissingTranslationStrategy;
|
|
(function (MissingTranslationStrategy) {
|
|
MissingTranslationStrategy[MissingTranslationStrategy["Error"] = 0] = "Error";
|
|
MissingTranslationStrategy[MissingTranslationStrategy["Warning"] = 1] = "Warning";
|
|
MissingTranslationStrategy[MissingTranslationStrategy["Ignore"] = 2] = "Ignore";
|
|
})(MissingTranslationStrategy || (MissingTranslationStrategy = {}));
|
|
function parserSelectorToSimpleSelector(selector) {
|
|
const classes = selector.classNames && selector.classNames.length
|
|
? [8 /* SelectorFlags.CLASS */, ...selector.classNames]
|
|
: [];
|
|
const elementName = selector.element && selector.element !== '*' ? selector.element : '';
|
|
return [elementName, ...selector.attrs, ...classes];
|
|
}
|
|
function parserSelectorToNegativeSelector(selector) {
|
|
const classes = selector.classNames && selector.classNames.length
|
|
? [8 /* SelectorFlags.CLASS */, ...selector.classNames]
|
|
: [];
|
|
if (selector.element) {
|
|
return [
|
|
1 /* SelectorFlags.NOT */ | 4 /* SelectorFlags.ELEMENT */,
|
|
selector.element,
|
|
...selector.attrs,
|
|
...classes,
|
|
];
|
|
}
|
|
else if (selector.attrs.length) {
|
|
return [1 /* SelectorFlags.NOT */ | 2 /* SelectorFlags.ATTRIBUTE */, ...selector.attrs, ...classes];
|
|
}
|
|
else {
|
|
return selector.classNames && selector.classNames.length
|
|
? [1 /* SelectorFlags.NOT */ | 8 /* SelectorFlags.CLASS */, ...selector.classNames]
|
|
: [];
|
|
}
|
|
}
|
|
function parserSelectorToR3Selector(selector) {
|
|
const positive = parserSelectorToSimpleSelector(selector);
|
|
const negative = selector.notSelectors && selector.notSelectors.length
|
|
? selector.notSelectors.map((notSelector) => parserSelectorToNegativeSelector(notSelector))
|
|
: [];
|
|
return positive.concat(...negative);
|
|
}
|
|
function parseSelectorToR3Selector(selector) {
|
|
return selector ? CssSelector.parse(selector).map(parserSelectorToR3Selector) : [];
|
|
}
|
|
|
|
var core = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
CUSTOM_ELEMENTS_SCHEMA: CUSTOM_ELEMENTS_SCHEMA,
|
|
get ChangeDetectionStrategy () { return ChangeDetectionStrategy; },
|
|
get InputFlags () { return InputFlags; },
|
|
get MissingTranslationStrategy () { return MissingTranslationStrategy; },
|
|
NO_ERRORS_SCHEMA: NO_ERRORS_SCHEMA,
|
|
get SecurityContext () { return SecurityContext; },
|
|
Type: Type$1,
|
|
get ViewEncapsulation () { return ViewEncapsulation$1; },
|
|
emitDistinctChangesOnlyDefaultValue: emitDistinctChangesOnlyDefaultValue,
|
|
parseSelectorToR3Selector: parseSelectorToR3Selector
|
|
});
|
|
|
|
var FactoryTarget;
|
|
(function (FactoryTarget) {
|
|
FactoryTarget[FactoryTarget["Directive"] = 0] = "Directive";
|
|
FactoryTarget[FactoryTarget["Component"] = 1] = "Component";
|
|
FactoryTarget[FactoryTarget["Injectable"] = 2] = "Injectable";
|
|
FactoryTarget[FactoryTarget["Pipe"] = 3] = "Pipe";
|
|
FactoryTarget[FactoryTarget["NgModule"] = 4] = "NgModule";
|
|
})(FactoryTarget || (FactoryTarget = {}));
|
|
var R3TemplateDependencyKind$1;
|
|
(function (R3TemplateDependencyKind) {
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["Directive"] = 0] = "Directive";
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["Pipe"] = 1] = "Pipe";
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["NgModule"] = 2] = "NgModule";
|
|
})(R3TemplateDependencyKind$1 || (R3TemplateDependencyKind$1 = {}));
|
|
var ViewEncapsulation;
|
|
(function (ViewEncapsulation) {
|
|
ViewEncapsulation[ViewEncapsulation["Emulated"] = 0] = "Emulated";
|
|
// Historically the 1 value was for `Native` encapsulation which has been removed as of v11.
|
|
ViewEncapsulation[ViewEncapsulation["None"] = 2] = "None";
|
|
ViewEncapsulation[ViewEncapsulation["ShadowDom"] = 3] = "ShadowDom";
|
|
})(ViewEncapsulation || (ViewEncapsulation = {}));
|
|
|
|
/**
|
|
* A lazily created TextEncoder instance for converting strings into UTF-8 bytes
|
|
*/
|
|
let textEncoder;
|
|
/**
|
|
* Return the message id or compute it using the XLIFF1 digest.
|
|
*/
|
|
function digest$1(message) {
|
|
return message.id || computeDigest(message);
|
|
}
|
|
/**
|
|
* Compute the message id using the XLIFF1 digest.
|
|
*/
|
|
function computeDigest(message) {
|
|
return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
|
|
}
|
|
/**
|
|
* Return the message id or compute it using the XLIFF2/XMB/$localize digest.
|
|
*/
|
|
function decimalDigest(message) {
|
|
return message.id || computeDecimalDigest(message);
|
|
}
|
|
/**
|
|
* Compute the message id using the XLIFF2/XMB/$localize digest.
|
|
*/
|
|
function computeDecimalDigest(message) {
|
|
const visitor = new _SerializerIgnoreIcuExpVisitor();
|
|
const parts = message.nodes.map((a) => a.visit(visitor, null));
|
|
return computeMsgId(parts.join(''), message.meaning);
|
|
}
|
|
/**
|
|
* Serialize the i18n ast to something xml-like in order to generate an UID.
|
|
*
|
|
* The visitor is also used in the i18n parser tests
|
|
*
|
|
* @internal
|
|
*/
|
|
class _SerializerVisitor {
|
|
visitText(text, context) {
|
|
return text.value;
|
|
}
|
|
visitContainer(container, context) {
|
|
return `[${container.children.map((child) => child.visit(this)).join(', ')}]`;
|
|
}
|
|
visitIcu(icu, context) {
|
|
const strCases = Object.keys(icu.cases).map((k) => `${k} {${icu.cases[k].visit(this)}}`);
|
|
return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
return ph.isVoid
|
|
? `<ph tag name="${ph.startName}"/>`
|
|
: `<ph tag name="${ph.startName}">${ph.children
|
|
.map((child) => child.visit(this))
|
|
.join(', ')}</ph name="${ph.closeName}">`;
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
return ph.value ? `<ph name="${ph.name}">${ph.value}</ph>` : `<ph name="${ph.name}"/>`;
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
return `<ph block name="${ph.startName}">${ph.children
|
|
.map((child) => child.visit(this))
|
|
.join(', ')}</ph name="${ph.closeName}">`;
|
|
}
|
|
}
|
|
const serializerVisitor$1 = new _SerializerVisitor();
|
|
function serializeNodes(nodes) {
|
|
return nodes.map((a) => a.visit(serializerVisitor$1, null));
|
|
}
|
|
/**
|
|
* Serialize the i18n ast to something xml-like in order to generate an UID.
|
|
*
|
|
* Ignore the ICU expressions so that message IDs stays identical if only the expression changes.
|
|
*
|
|
* @internal
|
|
*/
|
|
class _SerializerIgnoreIcuExpVisitor extends _SerializerVisitor {
|
|
visitIcu(icu) {
|
|
let strCases = Object.keys(icu.cases).map((k) => `${k} {${icu.cases[k].visit(this)}}`);
|
|
// Do not take the expression into account
|
|
return `{${icu.type}, ${strCases.join(', ')}}`;
|
|
}
|
|
}
|
|
/**
|
|
* Compute the SHA1 of the given string
|
|
*
|
|
* see https://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
|
|
*
|
|
* WARNING: this function has not been designed not tested with security in mind.
|
|
* DO NOT USE IT IN A SECURITY SENSITIVE CONTEXT.
|
|
*/
|
|
function sha1(str) {
|
|
textEncoder ??= new TextEncoder();
|
|
const utf8 = [...textEncoder.encode(str)];
|
|
const words32 = bytesToWords32(utf8, Endian.Big);
|
|
const len = utf8.length * 8;
|
|
const w = new Uint32Array(80);
|
|
let a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476, e = 0xc3d2e1f0;
|
|
words32[len >> 5] |= 0x80 << (24 - (len % 32));
|
|
words32[(((len + 64) >> 9) << 4) + 15] = len;
|
|
for (let i = 0; i < words32.length; i += 16) {
|
|
const h0 = a, h1 = b, h2 = c, h3 = d, h4 = e;
|
|
for (let j = 0; j < 80; j++) {
|
|
if (j < 16) {
|
|
w[j] = words32[i + j];
|
|
}
|
|
else {
|
|
w[j] = rol32(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
|
|
}
|
|
const fkVal = fk(j, b, c, d);
|
|
const f = fkVal[0];
|
|
const k = fkVal[1];
|
|
const temp = [rol32(a, 5), f, e, k, w[j]].reduce(add32);
|
|
e = d;
|
|
d = c;
|
|
c = rol32(b, 30);
|
|
b = a;
|
|
a = temp;
|
|
}
|
|
a = add32(a, h0);
|
|
b = add32(b, h1);
|
|
c = add32(c, h2);
|
|
d = add32(d, h3);
|
|
e = add32(e, h4);
|
|
}
|
|
// Convert the output parts to a 160-bit hexadecimal string
|
|
return toHexU32(a) + toHexU32(b) + toHexU32(c) + toHexU32(d) + toHexU32(e);
|
|
}
|
|
/**
|
|
* Convert and format a number as a string representing a 32-bit unsigned hexadecimal number.
|
|
* @param value The value to format as a string.
|
|
* @returns A hexadecimal string representing the value.
|
|
*/
|
|
function toHexU32(value) {
|
|
// unsigned right shift of zero ensures an unsigned 32-bit number
|
|
return (value >>> 0).toString(16).padStart(8, '0');
|
|
}
|
|
function fk(index, b, c, d) {
|
|
if (index < 20) {
|
|
return [(b & c) | (~b & d), 0x5a827999];
|
|
}
|
|
if (index < 40) {
|
|
return [b ^ c ^ d, 0x6ed9eba1];
|
|
}
|
|
if (index < 60) {
|
|
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
|
|
}
|
|
return [b ^ c ^ d, 0xca62c1d6];
|
|
}
|
|
/**
|
|
* Compute the fingerprint of the given string
|
|
*
|
|
* The output is 64 bit number encoded as a decimal string
|
|
*
|
|
* based on:
|
|
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
|
|
*/
|
|
function fingerprint(str) {
|
|
textEncoder ??= new TextEncoder();
|
|
const utf8 = textEncoder.encode(str);
|
|
const view = new DataView(utf8.buffer, utf8.byteOffset, utf8.byteLength);
|
|
let hi = hash32(view, utf8.length, 0);
|
|
let lo = hash32(view, utf8.length, 102072);
|
|
if (hi == 0 && (lo == 0 || lo == 1)) {
|
|
hi = hi ^ 0x130f9bef;
|
|
lo = lo ^ -0x6b5f56d8;
|
|
}
|
|
return (BigInt.asUintN(32, BigInt(hi)) << BigInt(32)) | BigInt.asUintN(32, BigInt(lo));
|
|
}
|
|
function computeMsgId(msg, meaning = '') {
|
|
let msgFingerprint = fingerprint(msg);
|
|
if (meaning) {
|
|
// Rotate the 64-bit message fingerprint one bit to the left and then add the meaning
|
|
// fingerprint.
|
|
msgFingerprint =
|
|
BigInt.asUintN(64, msgFingerprint << BigInt(1)) |
|
|
((msgFingerprint >> BigInt(63)) & BigInt(1));
|
|
msgFingerprint += fingerprint(meaning);
|
|
}
|
|
return BigInt.asUintN(63, msgFingerprint).toString();
|
|
}
|
|
function hash32(view, length, c) {
|
|
let a = 0x9e3779b9, b = 0x9e3779b9;
|
|
let index = 0;
|
|
const end = length - 12;
|
|
for (; index <= end; index += 12) {
|
|
a += view.getUint32(index, true);
|
|
b += view.getUint32(index + 4, true);
|
|
c += view.getUint32(index + 8, true);
|
|
const res = mix(a, b, c);
|
|
(a = res[0]), (b = res[1]), (c = res[2]);
|
|
}
|
|
const remainder = length - index;
|
|
// the first byte of c is reserved for the length
|
|
c += length;
|
|
if (remainder >= 4) {
|
|
a += view.getUint32(index, true);
|
|
index += 4;
|
|
if (remainder >= 8) {
|
|
b += view.getUint32(index, true);
|
|
index += 4;
|
|
// Partial 32-bit word for c
|
|
if (remainder >= 9) {
|
|
c += view.getUint8(index++) << 8;
|
|
}
|
|
if (remainder >= 10) {
|
|
c += view.getUint8(index++) << 16;
|
|
}
|
|
if (remainder === 11) {
|
|
c += view.getUint8(index++) << 24;
|
|
}
|
|
}
|
|
else {
|
|
// Partial 32-bit word for b
|
|
if (remainder >= 5) {
|
|
b += view.getUint8(index++);
|
|
}
|
|
if (remainder >= 6) {
|
|
b += view.getUint8(index++) << 8;
|
|
}
|
|
if (remainder === 7) {
|
|
b += view.getUint8(index++) << 16;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Partial 32-bit word for a
|
|
if (remainder >= 1) {
|
|
a += view.getUint8(index++);
|
|
}
|
|
if (remainder >= 2) {
|
|
a += view.getUint8(index++) << 8;
|
|
}
|
|
if (remainder === 3) {
|
|
a += view.getUint8(index++) << 16;
|
|
}
|
|
}
|
|
return mix(a, b, c)[2];
|
|
}
|
|
function mix(a, b, c) {
|
|
a -= b;
|
|
a -= c;
|
|
a ^= c >>> 13;
|
|
b -= c;
|
|
b -= a;
|
|
b ^= a << 8;
|
|
c -= a;
|
|
c -= b;
|
|
c ^= b >>> 13;
|
|
a -= b;
|
|
a -= c;
|
|
a ^= c >>> 12;
|
|
b -= c;
|
|
b -= a;
|
|
b ^= a << 16;
|
|
c -= a;
|
|
c -= b;
|
|
c ^= b >>> 5;
|
|
a -= b;
|
|
a -= c;
|
|
a ^= c >>> 3;
|
|
b -= c;
|
|
b -= a;
|
|
b ^= a << 10;
|
|
c -= a;
|
|
c -= b;
|
|
c ^= b >>> 15;
|
|
return [a, b, c];
|
|
}
|
|
// Utils
|
|
var Endian;
|
|
(function (Endian) {
|
|
Endian[Endian["Little"] = 0] = "Little";
|
|
Endian[Endian["Big"] = 1] = "Big";
|
|
})(Endian || (Endian = {}));
|
|
function add32(a, b) {
|
|
return add32to64(a, b)[1];
|
|
}
|
|
function add32to64(a, b) {
|
|
const low = (a & 0xffff) + (b & 0xffff);
|
|
const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
|
|
return [high >>> 16, (high << 16) | (low & 0xffff)];
|
|
}
|
|
// Rotate a 32b number left `count` position
|
|
function rol32(a, count) {
|
|
return (a << count) | (a >>> (32 - count));
|
|
}
|
|
function bytesToWords32(bytes, endian) {
|
|
const size = (bytes.length + 3) >>> 2;
|
|
const words32 = [];
|
|
for (let i = 0; i < size; i++) {
|
|
words32[i] = wordAt(bytes, i * 4, endian);
|
|
}
|
|
return words32;
|
|
}
|
|
function byteAt(bytes, index) {
|
|
return index >= bytes.length ? 0 : bytes[index];
|
|
}
|
|
function wordAt(bytes, index, endian) {
|
|
let word = 0;
|
|
if (endian === Endian.Big) {
|
|
for (let i = 0; i < 4; i++) {
|
|
word += byteAt(bytes, index + i) << (24 - 8 * i);
|
|
}
|
|
}
|
|
else {
|
|
for (let i = 0; i < 4; i++) {
|
|
word += byteAt(bytes, index + i) << (8 * i);
|
|
}
|
|
}
|
|
return word;
|
|
}
|
|
|
|
//// Types
|
|
var TypeModifier;
|
|
(function (TypeModifier) {
|
|
TypeModifier[TypeModifier["None"] = 0] = "None";
|
|
TypeModifier[TypeModifier["Const"] = 1] = "Const";
|
|
})(TypeModifier || (TypeModifier = {}));
|
|
class Type {
|
|
modifiers;
|
|
constructor(modifiers = TypeModifier.None) {
|
|
this.modifiers = modifiers;
|
|
}
|
|
hasModifier(modifier) {
|
|
return (this.modifiers & modifier) !== 0;
|
|
}
|
|
}
|
|
var BuiltinTypeName;
|
|
(function (BuiltinTypeName) {
|
|
BuiltinTypeName[BuiltinTypeName["Dynamic"] = 0] = "Dynamic";
|
|
BuiltinTypeName[BuiltinTypeName["Bool"] = 1] = "Bool";
|
|
BuiltinTypeName[BuiltinTypeName["String"] = 2] = "String";
|
|
BuiltinTypeName[BuiltinTypeName["Int"] = 3] = "Int";
|
|
BuiltinTypeName[BuiltinTypeName["Number"] = 4] = "Number";
|
|
BuiltinTypeName[BuiltinTypeName["Function"] = 5] = "Function";
|
|
BuiltinTypeName[BuiltinTypeName["Inferred"] = 6] = "Inferred";
|
|
BuiltinTypeName[BuiltinTypeName["None"] = 7] = "None";
|
|
})(BuiltinTypeName || (BuiltinTypeName = {}));
|
|
class BuiltinType extends Type {
|
|
name;
|
|
constructor(name, modifiers) {
|
|
super(modifiers);
|
|
this.name = name;
|
|
}
|
|
visitType(visitor, context) {
|
|
return visitor.visitBuiltinType(this, context);
|
|
}
|
|
}
|
|
class ExpressionType extends Type {
|
|
value;
|
|
typeParams;
|
|
constructor(value, modifiers, typeParams = null) {
|
|
super(modifiers);
|
|
this.value = value;
|
|
this.typeParams = typeParams;
|
|
}
|
|
visitType(visitor, context) {
|
|
return visitor.visitExpressionType(this, context);
|
|
}
|
|
}
|
|
class ArrayType extends Type {
|
|
of;
|
|
constructor(of, modifiers) {
|
|
super(modifiers);
|
|
this.of = of;
|
|
}
|
|
visitType(visitor, context) {
|
|
return visitor.visitArrayType(this, context);
|
|
}
|
|
}
|
|
class MapType extends Type {
|
|
valueType;
|
|
constructor(valueType, modifiers) {
|
|
super(modifiers);
|
|
this.valueType = valueType || null;
|
|
}
|
|
visitType(visitor, context) {
|
|
return visitor.visitMapType(this, context);
|
|
}
|
|
}
|
|
class TransplantedType extends Type {
|
|
type;
|
|
constructor(type, modifiers) {
|
|
super(modifiers);
|
|
this.type = type;
|
|
}
|
|
visitType(visitor, context) {
|
|
return visitor.visitTransplantedType(this, context);
|
|
}
|
|
}
|
|
const DYNAMIC_TYPE = new BuiltinType(BuiltinTypeName.Dynamic);
|
|
const INFERRED_TYPE = new BuiltinType(BuiltinTypeName.Inferred);
|
|
const BOOL_TYPE = new BuiltinType(BuiltinTypeName.Bool);
|
|
const INT_TYPE = new BuiltinType(BuiltinTypeName.Int);
|
|
const NUMBER_TYPE = new BuiltinType(BuiltinTypeName.Number);
|
|
const STRING_TYPE = new BuiltinType(BuiltinTypeName.String);
|
|
const FUNCTION_TYPE = new BuiltinType(BuiltinTypeName.Function);
|
|
const NONE_TYPE = new BuiltinType(BuiltinTypeName.None);
|
|
///// Expressions
|
|
var UnaryOperator;
|
|
(function (UnaryOperator) {
|
|
UnaryOperator[UnaryOperator["Minus"] = 0] = "Minus";
|
|
UnaryOperator[UnaryOperator["Plus"] = 1] = "Plus";
|
|
})(UnaryOperator || (UnaryOperator = {}));
|
|
var BinaryOperator;
|
|
(function (BinaryOperator) {
|
|
BinaryOperator[BinaryOperator["Equals"] = 0] = "Equals";
|
|
BinaryOperator[BinaryOperator["NotEquals"] = 1] = "NotEquals";
|
|
BinaryOperator[BinaryOperator["Assign"] = 2] = "Assign";
|
|
BinaryOperator[BinaryOperator["Identical"] = 3] = "Identical";
|
|
BinaryOperator[BinaryOperator["NotIdentical"] = 4] = "NotIdentical";
|
|
BinaryOperator[BinaryOperator["Minus"] = 5] = "Minus";
|
|
BinaryOperator[BinaryOperator["Plus"] = 6] = "Plus";
|
|
BinaryOperator[BinaryOperator["Divide"] = 7] = "Divide";
|
|
BinaryOperator[BinaryOperator["Multiply"] = 8] = "Multiply";
|
|
BinaryOperator[BinaryOperator["Modulo"] = 9] = "Modulo";
|
|
BinaryOperator[BinaryOperator["And"] = 10] = "And";
|
|
BinaryOperator[BinaryOperator["Or"] = 11] = "Or";
|
|
BinaryOperator[BinaryOperator["BitwiseOr"] = 12] = "BitwiseOr";
|
|
BinaryOperator[BinaryOperator["BitwiseAnd"] = 13] = "BitwiseAnd";
|
|
BinaryOperator[BinaryOperator["Lower"] = 14] = "Lower";
|
|
BinaryOperator[BinaryOperator["LowerEquals"] = 15] = "LowerEquals";
|
|
BinaryOperator[BinaryOperator["Bigger"] = 16] = "Bigger";
|
|
BinaryOperator[BinaryOperator["BiggerEquals"] = 17] = "BiggerEquals";
|
|
BinaryOperator[BinaryOperator["NullishCoalesce"] = 18] = "NullishCoalesce";
|
|
BinaryOperator[BinaryOperator["Exponentiation"] = 19] = "Exponentiation";
|
|
BinaryOperator[BinaryOperator["In"] = 20] = "In";
|
|
BinaryOperator[BinaryOperator["AdditionAssignment"] = 21] = "AdditionAssignment";
|
|
BinaryOperator[BinaryOperator["SubtractionAssignment"] = 22] = "SubtractionAssignment";
|
|
BinaryOperator[BinaryOperator["MultiplicationAssignment"] = 23] = "MultiplicationAssignment";
|
|
BinaryOperator[BinaryOperator["DivisionAssignment"] = 24] = "DivisionAssignment";
|
|
BinaryOperator[BinaryOperator["RemainderAssignment"] = 25] = "RemainderAssignment";
|
|
BinaryOperator[BinaryOperator["ExponentiationAssignment"] = 26] = "ExponentiationAssignment";
|
|
BinaryOperator[BinaryOperator["AndAssignment"] = 27] = "AndAssignment";
|
|
BinaryOperator[BinaryOperator["OrAssignment"] = 28] = "OrAssignment";
|
|
BinaryOperator[BinaryOperator["NullishCoalesceAssignment"] = 29] = "NullishCoalesceAssignment";
|
|
})(BinaryOperator || (BinaryOperator = {}));
|
|
function nullSafeIsEquivalent(base, other) {
|
|
if (base == null || other == null) {
|
|
return base == other;
|
|
}
|
|
return base.isEquivalent(other);
|
|
}
|
|
function areAllEquivalentPredicate(base, other, equivalentPredicate) {
|
|
const len = base.length;
|
|
if (len !== other.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < len; i++) {
|
|
if (!equivalentPredicate(base[i], other[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function areAllEquivalent(base, other) {
|
|
return areAllEquivalentPredicate(base, other, (baseElement, otherElement) => baseElement.isEquivalent(otherElement));
|
|
}
|
|
class Expression {
|
|
type;
|
|
sourceSpan;
|
|
constructor(type, sourceSpan) {
|
|
this.type = type || null;
|
|
this.sourceSpan = sourceSpan || null;
|
|
}
|
|
prop(name, sourceSpan) {
|
|
return new ReadPropExpr(this, name, null, sourceSpan);
|
|
}
|
|
key(index, type, sourceSpan) {
|
|
return new ReadKeyExpr(this, index, type, sourceSpan);
|
|
}
|
|
callFn(params, sourceSpan, pure) {
|
|
return new InvokeFunctionExpr(this, params, null, sourceSpan, pure);
|
|
}
|
|
instantiate(params, type, sourceSpan) {
|
|
return new InstantiateExpr(this, params, type, sourceSpan);
|
|
}
|
|
conditional(trueCase, falseCase = null, sourceSpan) {
|
|
return new ConditionalExpr(this, trueCase, falseCase, null, sourceSpan);
|
|
}
|
|
equals(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Equals, this, rhs, null, sourceSpan);
|
|
}
|
|
notEquals(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.NotEquals, this, rhs, null, sourceSpan);
|
|
}
|
|
identical(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Identical, this, rhs, null, sourceSpan);
|
|
}
|
|
notIdentical(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.NotIdentical, this, rhs, null, sourceSpan);
|
|
}
|
|
minus(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Minus, this, rhs, null, sourceSpan);
|
|
}
|
|
plus(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Plus, this, rhs, null, sourceSpan);
|
|
}
|
|
divide(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Divide, this, rhs, null, sourceSpan);
|
|
}
|
|
multiply(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Multiply, this, rhs, null, sourceSpan);
|
|
}
|
|
modulo(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Modulo, this, rhs, null, sourceSpan);
|
|
}
|
|
power(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Exponentiation, this, rhs, null, sourceSpan);
|
|
}
|
|
and(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.And, this, rhs, null, sourceSpan);
|
|
}
|
|
bitwiseOr(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.BitwiseOr, this, rhs, null, sourceSpan);
|
|
}
|
|
bitwiseAnd(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.BitwiseAnd, this, rhs, null, sourceSpan);
|
|
}
|
|
or(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Or, this, rhs, null, sourceSpan);
|
|
}
|
|
lower(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Lower, this, rhs, null, sourceSpan);
|
|
}
|
|
lowerEquals(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.LowerEquals, this, rhs, null, sourceSpan);
|
|
}
|
|
bigger(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Bigger, this, rhs, null, sourceSpan);
|
|
}
|
|
biggerEquals(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.BiggerEquals, this, rhs, null, sourceSpan);
|
|
}
|
|
isBlank(sourceSpan) {
|
|
// Note: We use equals by purpose here to compare to null and undefined in JS.
|
|
// We use the typed null to allow strictNullChecks to narrow types.
|
|
return this.equals(TYPED_NULL_EXPR, sourceSpan);
|
|
}
|
|
nullishCoalesce(rhs, sourceSpan) {
|
|
return new BinaryOperatorExpr(BinaryOperator.NullishCoalesce, this, rhs, null, sourceSpan);
|
|
}
|
|
toStmt() {
|
|
return new ExpressionStatement(this, null);
|
|
}
|
|
}
|
|
class ReadVarExpr extends Expression {
|
|
name;
|
|
constructor(name, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.name = name;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof ReadVarExpr && this.name === e.name;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitReadVarExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new ReadVarExpr(this.name, this.type, this.sourceSpan);
|
|
}
|
|
set(value) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Assign, this, value, null, this.sourceSpan);
|
|
}
|
|
}
|
|
class TypeofExpr extends Expression {
|
|
expr;
|
|
constructor(expr, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.expr = expr;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitTypeofExpr(this, context);
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof TypeofExpr && e.expr.isEquivalent(this.expr);
|
|
}
|
|
isConstant() {
|
|
return this.expr.isConstant();
|
|
}
|
|
clone() {
|
|
return new TypeofExpr(this.expr.clone());
|
|
}
|
|
}
|
|
class VoidExpr extends Expression {
|
|
expr;
|
|
constructor(expr, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.expr = expr;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitVoidExpr(this, context);
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof VoidExpr && e.expr.isEquivalent(this.expr);
|
|
}
|
|
isConstant() {
|
|
return this.expr.isConstant();
|
|
}
|
|
clone() {
|
|
return new VoidExpr(this.expr.clone());
|
|
}
|
|
}
|
|
class WrappedNodeExpr extends Expression {
|
|
node;
|
|
constructor(node, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.node = node;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof WrappedNodeExpr && this.node === e.node;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitWrappedNodeExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new WrappedNodeExpr(this.node, this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class InvokeFunctionExpr extends Expression {
|
|
fn;
|
|
args;
|
|
pure;
|
|
constructor(fn, args, type, sourceSpan, pure = false) {
|
|
super(type, sourceSpan);
|
|
this.fn = fn;
|
|
this.args = args;
|
|
this.pure = pure;
|
|
}
|
|
// An alias for fn, which allows other logic to handle calls and property reads together.
|
|
get receiver() {
|
|
return this.fn;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof InvokeFunctionExpr &&
|
|
this.fn.isEquivalent(e.fn) &&
|
|
areAllEquivalent(this.args, e.args) &&
|
|
this.pure === e.pure);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitInvokeFunctionExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new InvokeFunctionExpr(this.fn.clone(), this.args.map((arg) => arg.clone()), this.type, this.sourceSpan, this.pure);
|
|
}
|
|
}
|
|
class TaggedTemplateLiteralExpr extends Expression {
|
|
tag;
|
|
template;
|
|
constructor(tag, template, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.tag = tag;
|
|
this.template = template;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof TaggedTemplateLiteralExpr &&
|
|
this.tag.isEquivalent(e.tag) &&
|
|
this.template.isEquivalent(e.template));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitTaggedTemplateLiteralExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new TaggedTemplateLiteralExpr(this.tag.clone(), this.template.clone(), this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class InstantiateExpr extends Expression {
|
|
classExpr;
|
|
args;
|
|
constructor(classExpr, args, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.classExpr = classExpr;
|
|
this.args = args;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof InstantiateExpr &&
|
|
this.classExpr.isEquivalent(e.classExpr) &&
|
|
areAllEquivalent(this.args, e.args));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitInstantiateExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new InstantiateExpr(this.classExpr.clone(), this.args.map((arg) => arg.clone()), this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class LiteralExpr extends Expression {
|
|
value;
|
|
constructor(value, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.value = value;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof LiteralExpr && this.value === e.value;
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitLiteralExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new LiteralExpr(this.value, this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class TemplateLiteralExpr extends Expression {
|
|
elements;
|
|
expressions;
|
|
constructor(elements, expressions, sourceSpan) {
|
|
super(null, sourceSpan);
|
|
this.elements = elements;
|
|
this.expressions = expressions;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof TemplateLiteralExpr &&
|
|
areAllEquivalentPredicate(this.elements, e.elements, (a, b) => a.text === b.text) &&
|
|
areAllEquivalent(this.expressions, e.expressions));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitTemplateLiteralExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new TemplateLiteralExpr(this.elements.map((el) => el.clone()), this.expressions.map((expr) => expr.clone()));
|
|
}
|
|
}
|
|
class TemplateLiteralElementExpr extends Expression {
|
|
text;
|
|
rawText;
|
|
constructor(text, sourceSpan, rawText) {
|
|
super(STRING_TYPE, sourceSpan);
|
|
this.text = text;
|
|
// If `rawText` is not provided, "fake" the raw string by escaping the following sequences:
|
|
// - "\" would otherwise indicate that the next character is a control character.
|
|
// - "`" and "${" are template string control sequences that would otherwise prematurely
|
|
// indicate the end of the template literal element.
|
|
// Note that we can't rely on the `sourceSpan` here, because it may be incorrect (see
|
|
// https://github.com/angular/angular/pull/60267#discussion_r1986402524).
|
|
this.rawText = rawText ?? escapeForTemplateLiteral(escapeSlashes(text));
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitTemplateLiteralElementExpr(this, context);
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof TemplateLiteralElementExpr && e.text === this.text && e.rawText === this.rawText);
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
clone() {
|
|
return new TemplateLiteralElementExpr(this.text, this.sourceSpan, this.rawText);
|
|
}
|
|
}
|
|
class LiteralPiece {
|
|
text;
|
|
sourceSpan;
|
|
constructor(text, sourceSpan) {
|
|
this.text = text;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
}
|
|
class PlaceholderPiece {
|
|
text;
|
|
sourceSpan;
|
|
associatedMessage;
|
|
/**
|
|
* Create a new instance of a `PlaceholderPiece`.
|
|
*
|
|
* @param text the name of this placeholder (e.g. `PH_1`).
|
|
* @param sourceSpan the location of this placeholder in its localized message the source code.
|
|
* @param associatedMessage reference to another message that this placeholder is associated with.
|
|
* The `associatedMessage` is mainly used to provide a relationship to an ICU message that has
|
|
* been extracted out from the message containing the placeholder.
|
|
*/
|
|
constructor(text, sourceSpan, associatedMessage) {
|
|
this.text = text;
|
|
this.sourceSpan = sourceSpan;
|
|
this.associatedMessage = associatedMessage;
|
|
}
|
|
}
|
|
const MEANING_SEPARATOR$1 = '|';
|
|
const ID_SEPARATOR$1 = '@@';
|
|
const LEGACY_ID_INDICATOR = '␟';
|
|
class LocalizedString extends Expression {
|
|
metaBlock;
|
|
messageParts;
|
|
placeHolderNames;
|
|
expressions;
|
|
constructor(metaBlock, messageParts, placeHolderNames, expressions, sourceSpan) {
|
|
super(STRING_TYPE, sourceSpan);
|
|
this.metaBlock = metaBlock;
|
|
this.messageParts = messageParts;
|
|
this.placeHolderNames = placeHolderNames;
|
|
this.expressions = expressions;
|
|
}
|
|
isEquivalent(e) {
|
|
// return e instanceof LocalizedString && this.message === e.message;
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitLocalizedString(this, context);
|
|
}
|
|
clone() {
|
|
return new LocalizedString(this.metaBlock, this.messageParts, this.placeHolderNames, this.expressions.map((expr) => expr.clone()), this.sourceSpan);
|
|
}
|
|
/**
|
|
* Serialize the given `meta` and `messagePart` into "cooked" and "raw" strings that can be used
|
|
* in a `$localize` tagged string. The format of the metadata is the same as that parsed by
|
|
* `parseI18nMeta()`.
|
|
*
|
|
* @param meta The metadata to serialize
|
|
* @param messagePart The first part of the tagged string
|
|
*/
|
|
serializeI18nHead() {
|
|
let metaBlock = this.metaBlock.description || '';
|
|
if (this.metaBlock.meaning) {
|
|
metaBlock = `${this.metaBlock.meaning}${MEANING_SEPARATOR$1}${metaBlock}`;
|
|
}
|
|
if (this.metaBlock.customId) {
|
|
metaBlock = `${metaBlock}${ID_SEPARATOR$1}${this.metaBlock.customId}`;
|
|
}
|
|
if (this.metaBlock.legacyIds) {
|
|
this.metaBlock.legacyIds.forEach((legacyId) => {
|
|
metaBlock = `${metaBlock}${LEGACY_ID_INDICATOR}${legacyId}`;
|
|
});
|
|
}
|
|
return createCookedRawString(metaBlock, this.messageParts[0].text, this.getMessagePartSourceSpan(0));
|
|
}
|
|
getMessagePartSourceSpan(i) {
|
|
return this.messageParts[i]?.sourceSpan ?? this.sourceSpan;
|
|
}
|
|
getPlaceholderSourceSpan(i) {
|
|
return (this.placeHolderNames[i]?.sourceSpan ?? this.expressions[i]?.sourceSpan ?? this.sourceSpan);
|
|
}
|
|
/**
|
|
* Serialize the given `placeholderName` and `messagePart` into "cooked" and "raw" strings that
|
|
* can be used in a `$localize` tagged string.
|
|
*
|
|
* The format is `:<placeholder-name>[@@<associated-id>]:`.
|
|
*
|
|
* The `associated-id` is the message id of the (usually an ICU) message to which this placeholder
|
|
* refers.
|
|
*
|
|
* @param partIndex The index of the message part to serialize.
|
|
*/
|
|
serializeI18nTemplatePart(partIndex) {
|
|
const placeholder = this.placeHolderNames[partIndex - 1];
|
|
const messagePart = this.messageParts[partIndex];
|
|
let metaBlock = placeholder.text;
|
|
if (placeholder.associatedMessage?.legacyIds.length === 0) {
|
|
metaBlock += `${ID_SEPARATOR$1}${computeMsgId(placeholder.associatedMessage.messageString, placeholder.associatedMessage.meaning)}`;
|
|
}
|
|
return createCookedRawString(metaBlock, messagePart.text, this.getMessagePartSourceSpan(partIndex));
|
|
}
|
|
}
|
|
const escapeSlashes = (str) => str.replace(/\\/g, '\\\\');
|
|
const escapeStartingColon = (str) => str.replace(/^:/, '\\:');
|
|
const escapeColons = (str) => str.replace(/:/g, '\\:');
|
|
const escapeForTemplateLiteral = (str) => str.replace(/`/g, '\\`').replace(/\${/g, '$\\{');
|
|
/**
|
|
* Creates a `{cooked, raw}` object from the `metaBlock` and `messagePart`.
|
|
*
|
|
* The `raw` text must have various character sequences escaped:
|
|
* * "\" would otherwise indicate that the next character is a control character.
|
|
* * "`" and "${" are template string control sequences that would otherwise prematurely indicate
|
|
* the end of a message part.
|
|
* * ":" inside a metablock would prematurely indicate the end of the metablock.
|
|
* * ":" at the start of a messagePart with no metablock would erroneously indicate the start of a
|
|
* metablock.
|
|
*
|
|
* @param metaBlock Any metadata that should be prepended to the string
|
|
* @param messagePart The message part of the string
|
|
*/
|
|
function createCookedRawString(metaBlock, messagePart, range) {
|
|
if (metaBlock === '') {
|
|
return {
|
|
cooked: messagePart,
|
|
raw: escapeForTemplateLiteral(escapeStartingColon(escapeSlashes(messagePart))),
|
|
range,
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
cooked: `:${metaBlock}:${messagePart}`,
|
|
raw: escapeForTemplateLiteral(`:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`),
|
|
range,
|
|
};
|
|
}
|
|
}
|
|
class ExternalExpr extends Expression {
|
|
value;
|
|
typeParams;
|
|
constructor(value, type, typeParams = null, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.value = value;
|
|
this.typeParams = typeParams;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof ExternalExpr &&
|
|
this.value.name === e.value.name &&
|
|
this.value.moduleName === e.value.moduleName);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitExternalExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new ExternalExpr(this.value, this.type, this.typeParams, this.sourceSpan);
|
|
}
|
|
}
|
|
class ExternalReference {
|
|
moduleName;
|
|
name;
|
|
constructor(moduleName, name) {
|
|
this.moduleName = moduleName;
|
|
this.name = name;
|
|
}
|
|
}
|
|
class ConditionalExpr extends Expression {
|
|
condition;
|
|
falseCase;
|
|
trueCase;
|
|
constructor(condition, trueCase, falseCase = null, type, sourceSpan) {
|
|
super(type || trueCase.type, sourceSpan);
|
|
this.condition = condition;
|
|
this.falseCase = falseCase;
|
|
this.trueCase = trueCase;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof ConditionalExpr &&
|
|
this.condition.isEquivalent(e.condition) &&
|
|
this.trueCase.isEquivalent(e.trueCase) &&
|
|
nullSafeIsEquivalent(this.falseCase, e.falseCase));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitConditionalExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new ConditionalExpr(this.condition.clone(), this.trueCase.clone(), this.falseCase?.clone(), this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class DynamicImportExpr extends Expression {
|
|
url;
|
|
urlComment;
|
|
constructor(url, sourceSpan, urlComment) {
|
|
super(null, sourceSpan);
|
|
this.url = url;
|
|
this.urlComment = urlComment;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof DynamicImportExpr && this.url === e.url && this.urlComment === e.urlComment;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitDynamicImportExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new DynamicImportExpr(typeof this.url === 'string' ? this.url : this.url.clone(), this.sourceSpan, this.urlComment);
|
|
}
|
|
}
|
|
class NotExpr extends Expression {
|
|
condition;
|
|
constructor(condition, sourceSpan) {
|
|
super(BOOL_TYPE, sourceSpan);
|
|
this.condition = condition;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof NotExpr && this.condition.isEquivalent(e.condition);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitNotExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new NotExpr(this.condition.clone(), this.sourceSpan);
|
|
}
|
|
}
|
|
class FnParam {
|
|
name;
|
|
type;
|
|
constructor(name, type = null) {
|
|
this.name = name;
|
|
this.type = type;
|
|
}
|
|
isEquivalent(param) {
|
|
return this.name === param.name;
|
|
}
|
|
clone() {
|
|
return new FnParam(this.name, this.type);
|
|
}
|
|
}
|
|
class FunctionExpr extends Expression {
|
|
params;
|
|
statements;
|
|
name;
|
|
constructor(params, statements, type, sourceSpan, name) {
|
|
super(type, sourceSpan);
|
|
this.params = params;
|
|
this.statements = statements;
|
|
this.name = name;
|
|
}
|
|
isEquivalent(e) {
|
|
return ((e instanceof FunctionExpr || e instanceof DeclareFunctionStmt) &&
|
|
areAllEquivalent(this.params, e.params) &&
|
|
areAllEquivalent(this.statements, e.statements));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitFunctionExpr(this, context);
|
|
}
|
|
toDeclStmt(name, modifiers) {
|
|
return new DeclareFunctionStmt(name, this.params, this.statements, this.type, modifiers, this.sourceSpan);
|
|
}
|
|
clone() {
|
|
// TODO: Should we deep clone statements?
|
|
return new FunctionExpr(this.params.map((p) => p.clone()), this.statements, this.type, this.sourceSpan, this.name);
|
|
}
|
|
}
|
|
class ArrowFunctionExpr extends Expression {
|
|
params;
|
|
body;
|
|
// Note that `body: Expression` represents `() => expr` whereas
|
|
// `body: Statement[]` represents `() => { expr }`.
|
|
constructor(params, body, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.params = params;
|
|
this.body = body;
|
|
}
|
|
isEquivalent(e) {
|
|
if (!(e instanceof ArrowFunctionExpr) || !areAllEquivalent(this.params, e.params)) {
|
|
return false;
|
|
}
|
|
if (this.body instanceof Expression && e.body instanceof Expression) {
|
|
return this.body.isEquivalent(e.body);
|
|
}
|
|
if (Array.isArray(this.body) && Array.isArray(e.body)) {
|
|
return areAllEquivalent(this.body, e.body);
|
|
}
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitArrowFunctionExpr(this, context);
|
|
}
|
|
clone() {
|
|
// TODO: Should we deep clone statements?
|
|
return new ArrowFunctionExpr(this.params.map((p) => p.clone()), Array.isArray(this.body) ? this.body : this.body.clone(), this.type, this.sourceSpan);
|
|
}
|
|
toDeclStmt(name, modifiers) {
|
|
return new DeclareVarStmt(name, this, INFERRED_TYPE, modifiers, this.sourceSpan);
|
|
}
|
|
}
|
|
class UnaryOperatorExpr extends Expression {
|
|
operator;
|
|
expr;
|
|
parens;
|
|
constructor(operator, expr, type, sourceSpan, parens = true) {
|
|
super(type || NUMBER_TYPE, sourceSpan);
|
|
this.operator = operator;
|
|
this.expr = expr;
|
|
this.parens = parens;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof UnaryOperatorExpr &&
|
|
this.operator === e.operator &&
|
|
this.expr.isEquivalent(e.expr));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitUnaryOperatorExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new UnaryOperatorExpr(this.operator, this.expr.clone(), this.type, this.sourceSpan, this.parens);
|
|
}
|
|
}
|
|
class ParenthesizedExpr extends Expression {
|
|
expr;
|
|
constructor(expr, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.expr = expr;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitParenthesizedExpr(this, context);
|
|
}
|
|
isEquivalent(e) {
|
|
// TODO: should this ignore paren depth? i.e. is `(1)` equivalent to `1`?
|
|
return e instanceof ParenthesizedExpr && e.expr.isEquivalent(this.expr);
|
|
}
|
|
isConstant() {
|
|
return this.expr.isConstant();
|
|
}
|
|
clone() {
|
|
return new ParenthesizedExpr(this.expr.clone());
|
|
}
|
|
}
|
|
class BinaryOperatorExpr extends Expression {
|
|
operator;
|
|
rhs;
|
|
lhs;
|
|
constructor(operator, lhs, rhs, type, sourceSpan) {
|
|
super(type || lhs.type, sourceSpan);
|
|
this.operator = operator;
|
|
this.rhs = rhs;
|
|
this.lhs = lhs;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof BinaryOperatorExpr &&
|
|
this.operator === e.operator &&
|
|
this.lhs.isEquivalent(e.lhs) &&
|
|
this.rhs.isEquivalent(e.rhs));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitBinaryOperatorExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new BinaryOperatorExpr(this.operator, this.lhs.clone(), this.rhs.clone(), this.type, this.sourceSpan);
|
|
}
|
|
isAssignment() {
|
|
const op = this.operator;
|
|
return (op === BinaryOperator.Assign ||
|
|
op === BinaryOperator.AdditionAssignment ||
|
|
op === BinaryOperator.SubtractionAssignment ||
|
|
op === BinaryOperator.MultiplicationAssignment ||
|
|
op === BinaryOperator.DivisionAssignment ||
|
|
op === BinaryOperator.RemainderAssignment ||
|
|
op === BinaryOperator.ExponentiationAssignment ||
|
|
op === BinaryOperator.AndAssignment ||
|
|
op === BinaryOperator.OrAssignment ||
|
|
op === BinaryOperator.NullishCoalesceAssignment);
|
|
}
|
|
}
|
|
class ReadPropExpr extends Expression {
|
|
receiver;
|
|
name;
|
|
constructor(receiver, name, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.name = name;
|
|
}
|
|
// An alias for name, which allows other logic to handle property reads and keyed reads together.
|
|
get index() {
|
|
return this.name;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) && this.name === e.name);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitReadPropExpr(this, context);
|
|
}
|
|
set(value) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Assign, this.receiver.prop(this.name), value, null, this.sourceSpan);
|
|
}
|
|
clone() {
|
|
return new ReadPropExpr(this.receiver.clone(), this.name, this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class ReadKeyExpr extends Expression {
|
|
receiver;
|
|
index;
|
|
constructor(receiver, index, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.index = index;
|
|
}
|
|
isEquivalent(e) {
|
|
return (e instanceof ReadKeyExpr &&
|
|
this.receiver.isEquivalent(e.receiver) &&
|
|
this.index.isEquivalent(e.index));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitReadKeyExpr(this, context);
|
|
}
|
|
set(value) {
|
|
return new BinaryOperatorExpr(BinaryOperator.Assign, this.receiver.key(this.index), value, null, this.sourceSpan);
|
|
}
|
|
clone() {
|
|
return new ReadKeyExpr(this.receiver.clone(), this.index.clone(), this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class LiteralArrayExpr extends Expression {
|
|
entries;
|
|
constructor(entries, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.entries = entries;
|
|
}
|
|
isConstant() {
|
|
return this.entries.every((e) => e.isConstant());
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries);
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitLiteralArrayExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new LiteralArrayExpr(this.entries.map((e) => e.clone()), this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class LiteralMapEntry {
|
|
key;
|
|
value;
|
|
quoted;
|
|
constructor(key, value, quoted) {
|
|
this.key = key;
|
|
this.value = value;
|
|
this.quoted = quoted;
|
|
}
|
|
isEquivalent(e) {
|
|
return this.key === e.key && this.value.isEquivalent(e.value);
|
|
}
|
|
clone() {
|
|
return new LiteralMapEntry(this.key, this.value.clone(), this.quoted);
|
|
}
|
|
}
|
|
class LiteralMapExpr extends Expression {
|
|
entries;
|
|
valueType = null;
|
|
constructor(entries, type, sourceSpan) {
|
|
super(type, sourceSpan);
|
|
this.entries = entries;
|
|
if (type) {
|
|
this.valueType = type.valueType;
|
|
}
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries);
|
|
}
|
|
isConstant() {
|
|
return this.entries.every((e) => e.value.isConstant());
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitLiteralMapExpr(this, context);
|
|
}
|
|
clone() {
|
|
const entriesClone = this.entries.map((entry) => entry.clone());
|
|
return new LiteralMapExpr(entriesClone, this.type, this.sourceSpan);
|
|
}
|
|
}
|
|
class CommaExpr extends Expression {
|
|
parts;
|
|
constructor(parts, sourceSpan) {
|
|
super(parts[parts.length - 1].type, sourceSpan);
|
|
this.parts = parts;
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
return visitor.visitCommaExpr(this, context);
|
|
}
|
|
clone() {
|
|
return new CommaExpr(this.parts.map((p) => p.clone()));
|
|
}
|
|
}
|
|
const NULL_EXPR = new LiteralExpr(null, null, null);
|
|
const TYPED_NULL_EXPR = new LiteralExpr(null, INFERRED_TYPE, null);
|
|
//// Statements
|
|
var StmtModifier;
|
|
(function (StmtModifier) {
|
|
StmtModifier[StmtModifier["None"] = 0] = "None";
|
|
StmtModifier[StmtModifier["Final"] = 1] = "Final";
|
|
StmtModifier[StmtModifier["Private"] = 2] = "Private";
|
|
StmtModifier[StmtModifier["Exported"] = 4] = "Exported";
|
|
StmtModifier[StmtModifier["Static"] = 8] = "Static";
|
|
})(StmtModifier || (StmtModifier = {}));
|
|
class LeadingComment {
|
|
text;
|
|
multiline;
|
|
trailingNewline;
|
|
constructor(text, multiline, trailingNewline) {
|
|
this.text = text;
|
|
this.multiline = multiline;
|
|
this.trailingNewline = trailingNewline;
|
|
}
|
|
toString() {
|
|
return this.multiline ? ` ${this.text} ` : this.text;
|
|
}
|
|
}
|
|
class JSDocComment extends LeadingComment {
|
|
tags;
|
|
constructor(tags) {
|
|
super('', /* multiline */ true, /* trailingNewline */ true);
|
|
this.tags = tags;
|
|
}
|
|
toString() {
|
|
return serializeTags(this.tags);
|
|
}
|
|
}
|
|
class Statement {
|
|
modifiers;
|
|
sourceSpan;
|
|
leadingComments;
|
|
constructor(modifiers = StmtModifier.None, sourceSpan = null, leadingComments) {
|
|
this.modifiers = modifiers;
|
|
this.sourceSpan = sourceSpan;
|
|
this.leadingComments = leadingComments;
|
|
}
|
|
hasModifier(modifier) {
|
|
return (this.modifiers & modifier) !== 0;
|
|
}
|
|
addLeadingComment(leadingComment) {
|
|
this.leadingComments = this.leadingComments ?? [];
|
|
this.leadingComments.push(leadingComment);
|
|
}
|
|
}
|
|
class DeclareVarStmt extends Statement {
|
|
name;
|
|
value;
|
|
type;
|
|
constructor(name, value, type, modifiers, sourceSpan, leadingComments) {
|
|
super(modifiers, sourceSpan, leadingComments);
|
|
this.name = name;
|
|
this.value = value;
|
|
this.type = type || (value && value.type) || null;
|
|
}
|
|
isEquivalent(stmt) {
|
|
return (stmt instanceof DeclareVarStmt &&
|
|
this.name === stmt.name &&
|
|
(this.value ? !!stmt.value && this.value.isEquivalent(stmt.value) : !stmt.value));
|
|
}
|
|
visitStatement(visitor, context) {
|
|
return visitor.visitDeclareVarStmt(this, context);
|
|
}
|
|
}
|
|
class DeclareFunctionStmt extends Statement {
|
|
name;
|
|
params;
|
|
statements;
|
|
type;
|
|
constructor(name, params, statements, type, modifiers, sourceSpan, leadingComments) {
|
|
super(modifiers, sourceSpan, leadingComments);
|
|
this.name = name;
|
|
this.params = params;
|
|
this.statements = statements;
|
|
this.type = type || null;
|
|
}
|
|
isEquivalent(stmt) {
|
|
return (stmt instanceof DeclareFunctionStmt &&
|
|
areAllEquivalent(this.params, stmt.params) &&
|
|
areAllEquivalent(this.statements, stmt.statements));
|
|
}
|
|
visitStatement(visitor, context) {
|
|
return visitor.visitDeclareFunctionStmt(this, context);
|
|
}
|
|
}
|
|
class ExpressionStatement extends Statement {
|
|
expr;
|
|
constructor(expr, sourceSpan, leadingComments) {
|
|
super(StmtModifier.None, sourceSpan, leadingComments);
|
|
this.expr = expr;
|
|
}
|
|
isEquivalent(stmt) {
|
|
return stmt instanceof ExpressionStatement && this.expr.isEquivalent(stmt.expr);
|
|
}
|
|
visitStatement(visitor, context) {
|
|
return visitor.visitExpressionStmt(this, context);
|
|
}
|
|
}
|
|
class ReturnStatement extends Statement {
|
|
value;
|
|
constructor(value, sourceSpan = null, leadingComments) {
|
|
super(StmtModifier.None, sourceSpan, leadingComments);
|
|
this.value = value;
|
|
}
|
|
isEquivalent(stmt) {
|
|
return stmt instanceof ReturnStatement && this.value.isEquivalent(stmt.value);
|
|
}
|
|
visitStatement(visitor, context) {
|
|
return visitor.visitReturnStmt(this, context);
|
|
}
|
|
}
|
|
class IfStmt extends Statement {
|
|
condition;
|
|
trueCase;
|
|
falseCase;
|
|
constructor(condition, trueCase, falseCase = [], sourceSpan, leadingComments) {
|
|
super(StmtModifier.None, sourceSpan, leadingComments);
|
|
this.condition = condition;
|
|
this.trueCase = trueCase;
|
|
this.falseCase = falseCase;
|
|
}
|
|
isEquivalent(stmt) {
|
|
return (stmt instanceof IfStmt &&
|
|
this.condition.isEquivalent(stmt.condition) &&
|
|
areAllEquivalent(this.trueCase, stmt.trueCase) &&
|
|
areAllEquivalent(this.falseCase, stmt.falseCase));
|
|
}
|
|
visitStatement(visitor, context) {
|
|
return visitor.visitIfStmt(this, context);
|
|
}
|
|
}
|
|
let RecursiveAstVisitor$1 = class RecursiveAstVisitor {
|
|
visitType(ast, context) {
|
|
return ast;
|
|
}
|
|
visitExpression(ast, context) {
|
|
if (ast.type) {
|
|
ast.type.visitType(this, context);
|
|
}
|
|
return ast;
|
|
}
|
|
visitBuiltinType(type, context) {
|
|
return this.visitType(type, context);
|
|
}
|
|
visitExpressionType(type, context) {
|
|
type.value.visitExpression(this, context);
|
|
if (type.typeParams !== null) {
|
|
type.typeParams.forEach((param) => this.visitType(param, context));
|
|
}
|
|
return this.visitType(type, context);
|
|
}
|
|
visitArrayType(type, context) {
|
|
return this.visitType(type, context);
|
|
}
|
|
visitMapType(type, context) {
|
|
return this.visitType(type, context);
|
|
}
|
|
visitTransplantedType(type, context) {
|
|
return type;
|
|
}
|
|
visitWrappedNodeExpr(ast, context) {
|
|
return ast;
|
|
}
|
|
visitReadVarExpr(ast, context) {
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitDynamicImportExpr(ast, context) {
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitInvokeFunctionExpr(ast, context) {
|
|
ast.fn.visitExpression(this, context);
|
|
this.visitAllExpressions(ast.args, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitTaggedTemplateLiteralExpr(ast, context) {
|
|
ast.tag.visitExpression(this, context);
|
|
ast.template.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitInstantiateExpr(ast, context) {
|
|
ast.classExpr.visitExpression(this, context);
|
|
this.visitAllExpressions(ast.args, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitLiteralExpr(ast, context) {
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitLocalizedString(ast, context) {
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitExternalExpr(ast, context) {
|
|
if (ast.typeParams) {
|
|
ast.typeParams.forEach((type) => type.visitType(this, context));
|
|
}
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitConditionalExpr(ast, context) {
|
|
ast.condition.visitExpression(this, context);
|
|
ast.trueCase.visitExpression(this, context);
|
|
ast.falseCase.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitNotExpr(ast, context) {
|
|
ast.condition.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitFunctionExpr(ast, context) {
|
|
this.visitAllStatements(ast.statements, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitArrowFunctionExpr(ast, context) {
|
|
if (Array.isArray(ast.body)) {
|
|
this.visitAllStatements(ast.body, context);
|
|
}
|
|
else {
|
|
// Note: `body.visitExpression`, rather than `this.visitExpressiont(body)`,
|
|
// because the latter won't recurse into the sub-expressions.
|
|
ast.body.visitExpression(this, context);
|
|
}
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitUnaryOperatorExpr(ast, context) {
|
|
ast.expr.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitTypeofExpr(ast, context) {
|
|
ast.expr.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitVoidExpr(ast, context) {
|
|
ast.expr.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitBinaryOperatorExpr(ast, context) {
|
|
ast.lhs.visitExpression(this, context);
|
|
ast.rhs.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitReadPropExpr(ast, context) {
|
|
ast.receiver.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitReadKeyExpr(ast, context) {
|
|
ast.receiver.visitExpression(this, context);
|
|
ast.index.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitLiteralArrayExpr(ast, context) {
|
|
this.visitAllExpressions(ast.entries, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitLiteralMapExpr(ast, context) {
|
|
ast.entries.forEach((entry) => entry.value.visitExpression(this, context));
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitCommaExpr(ast, context) {
|
|
this.visitAllExpressions(ast.parts, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitTemplateLiteralExpr(ast, context) {
|
|
this.visitAllExpressions(ast.elements, context);
|
|
this.visitAllExpressions(ast.expressions, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitTemplateLiteralElementExpr(ast, context) {
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitParenthesizedExpr(ast, context) {
|
|
ast.expr.visitExpression(this, context);
|
|
return this.visitExpression(ast, context);
|
|
}
|
|
visitAllExpressions(exprs, context) {
|
|
exprs.forEach((expr) => expr.visitExpression(this, context));
|
|
}
|
|
visitDeclareVarStmt(stmt, context) {
|
|
if (stmt.value) {
|
|
stmt.value.visitExpression(this, context);
|
|
}
|
|
if (stmt.type) {
|
|
stmt.type.visitType(this, context);
|
|
}
|
|
return stmt;
|
|
}
|
|
visitDeclareFunctionStmt(stmt, context) {
|
|
this.visitAllStatements(stmt.statements, context);
|
|
if (stmt.type) {
|
|
stmt.type.visitType(this, context);
|
|
}
|
|
return stmt;
|
|
}
|
|
visitExpressionStmt(stmt, context) {
|
|
stmt.expr.visitExpression(this, context);
|
|
return stmt;
|
|
}
|
|
visitReturnStmt(stmt, context) {
|
|
stmt.value.visitExpression(this, context);
|
|
return stmt;
|
|
}
|
|
visitIfStmt(stmt, context) {
|
|
stmt.condition.visitExpression(this, context);
|
|
this.visitAllStatements(stmt.trueCase, context);
|
|
this.visitAllStatements(stmt.falseCase, context);
|
|
return stmt;
|
|
}
|
|
visitAllStatements(stmts, context) {
|
|
stmts.forEach((stmt) => stmt.visitStatement(this, context));
|
|
}
|
|
};
|
|
function leadingComment(text, multiline = false, trailingNewline = true) {
|
|
return new LeadingComment(text, multiline, trailingNewline);
|
|
}
|
|
function jsDocComment(tags = []) {
|
|
return new JSDocComment(tags);
|
|
}
|
|
function variable(name, type, sourceSpan) {
|
|
return new ReadVarExpr(name, type, sourceSpan);
|
|
}
|
|
function importExpr(id, typeParams = null, sourceSpan) {
|
|
return new ExternalExpr(id, null, typeParams, sourceSpan);
|
|
}
|
|
function importType(id, typeParams, typeModifiers) {
|
|
return id != null ? expressionType(importExpr(id, typeParams, null), typeModifiers) : null;
|
|
}
|
|
function expressionType(expr, typeModifiers, typeParams) {
|
|
return new ExpressionType(expr, typeModifiers, typeParams);
|
|
}
|
|
function transplantedType(type, typeModifiers) {
|
|
return new TransplantedType(type, typeModifiers);
|
|
}
|
|
function typeofExpr(expr) {
|
|
return new TypeofExpr(expr);
|
|
}
|
|
function literalArr(values, type, sourceSpan) {
|
|
return new LiteralArrayExpr(values, type, sourceSpan);
|
|
}
|
|
function literalMap(values, type = null) {
|
|
return new LiteralMapExpr(values.map((e) => new LiteralMapEntry(e.key, e.value, e.quoted)), type, null);
|
|
}
|
|
function unary(operator, expr, type, sourceSpan) {
|
|
return new UnaryOperatorExpr(operator, expr, type, sourceSpan);
|
|
}
|
|
function not(expr, sourceSpan) {
|
|
return new NotExpr(expr, sourceSpan);
|
|
}
|
|
function fn(params, body, type, sourceSpan, name) {
|
|
return new FunctionExpr(params, body, type, sourceSpan, name);
|
|
}
|
|
function arrowFn(params, body, type, sourceSpan) {
|
|
return new ArrowFunctionExpr(params, body, type, sourceSpan);
|
|
}
|
|
function ifStmt(condition, thenClause, elseClause, sourceSpan, leadingComments) {
|
|
return new IfStmt(condition, thenClause, elseClause, sourceSpan, leadingComments);
|
|
}
|
|
function taggedTemplate(tag, template, type, sourceSpan) {
|
|
return new TaggedTemplateLiteralExpr(tag, template, type, sourceSpan);
|
|
}
|
|
function literal(value, type, sourceSpan) {
|
|
return new LiteralExpr(value, type, sourceSpan);
|
|
}
|
|
function localizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan) {
|
|
return new LocalizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan);
|
|
}
|
|
function isNull(exp) {
|
|
return exp instanceof LiteralExpr && exp.value === null;
|
|
}
|
|
/*
|
|
* Serializes a `Tag` into a string.
|
|
* Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`).
|
|
*/
|
|
function tagToString(tag) {
|
|
let out = '';
|
|
if (tag.tagName) {
|
|
out += ` @${tag.tagName}`;
|
|
}
|
|
if (tag.text) {
|
|
if (tag.text.match(/\/\*|\*\//)) {
|
|
throw new Error('JSDoc text cannot contain "/*" and "*/"');
|
|
}
|
|
out += ' ' + tag.text.replace(/@/g, '\\@');
|
|
}
|
|
return out;
|
|
}
|
|
function serializeTags(tags) {
|
|
if (tags.length === 0)
|
|
return '';
|
|
if (tags.length === 1 && tags[0].tagName && !tags[0].text) {
|
|
// The JSDOC comment is a single simple tag: e.g `/** @tagname */`.
|
|
return `*${tagToString(tags[0])} `;
|
|
}
|
|
let out = '*\n';
|
|
for (const tag of tags) {
|
|
out += ' *';
|
|
// If the tagToString is multi-line, insert " * " prefixes on lines.
|
|
out += tagToString(tag).replace(/\n/g, '\n * ');
|
|
out += '\n';
|
|
}
|
|
out += ' ';
|
|
return out;
|
|
}
|
|
|
|
var output_ast = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
ArrayType: ArrayType,
|
|
ArrowFunctionExpr: ArrowFunctionExpr,
|
|
BOOL_TYPE: BOOL_TYPE,
|
|
get BinaryOperator () { return BinaryOperator; },
|
|
BinaryOperatorExpr: BinaryOperatorExpr,
|
|
BuiltinType: BuiltinType,
|
|
get BuiltinTypeName () { return BuiltinTypeName; },
|
|
CommaExpr: CommaExpr,
|
|
ConditionalExpr: ConditionalExpr,
|
|
DYNAMIC_TYPE: DYNAMIC_TYPE,
|
|
DeclareFunctionStmt: DeclareFunctionStmt,
|
|
DeclareVarStmt: DeclareVarStmt,
|
|
DynamicImportExpr: DynamicImportExpr,
|
|
Expression: Expression,
|
|
ExpressionStatement: ExpressionStatement,
|
|
ExpressionType: ExpressionType,
|
|
ExternalExpr: ExternalExpr,
|
|
ExternalReference: ExternalReference,
|
|
FUNCTION_TYPE: FUNCTION_TYPE,
|
|
FnParam: FnParam,
|
|
FunctionExpr: FunctionExpr,
|
|
INFERRED_TYPE: INFERRED_TYPE,
|
|
INT_TYPE: INT_TYPE,
|
|
IfStmt: IfStmt,
|
|
InstantiateExpr: InstantiateExpr,
|
|
InvokeFunctionExpr: InvokeFunctionExpr,
|
|
JSDocComment: JSDocComment,
|
|
LeadingComment: LeadingComment,
|
|
LiteralArrayExpr: LiteralArrayExpr,
|
|
LiteralExpr: LiteralExpr,
|
|
LiteralMapEntry: LiteralMapEntry,
|
|
LiteralMapExpr: LiteralMapExpr,
|
|
LiteralPiece: LiteralPiece,
|
|
LocalizedString: LocalizedString,
|
|
MapType: MapType,
|
|
NONE_TYPE: NONE_TYPE,
|
|
NULL_EXPR: NULL_EXPR,
|
|
NUMBER_TYPE: NUMBER_TYPE,
|
|
NotExpr: NotExpr,
|
|
ParenthesizedExpr: ParenthesizedExpr,
|
|
PlaceholderPiece: PlaceholderPiece,
|
|
ReadKeyExpr: ReadKeyExpr,
|
|
ReadPropExpr: ReadPropExpr,
|
|
ReadVarExpr: ReadVarExpr,
|
|
RecursiveAstVisitor: RecursiveAstVisitor$1,
|
|
ReturnStatement: ReturnStatement,
|
|
STRING_TYPE: STRING_TYPE,
|
|
Statement: Statement,
|
|
get StmtModifier () { return StmtModifier; },
|
|
TYPED_NULL_EXPR: TYPED_NULL_EXPR,
|
|
TaggedTemplateLiteralExpr: TaggedTemplateLiteralExpr,
|
|
TemplateLiteralElementExpr: TemplateLiteralElementExpr,
|
|
TemplateLiteralExpr: TemplateLiteralExpr,
|
|
TransplantedType: TransplantedType,
|
|
Type: Type,
|
|
get TypeModifier () { return TypeModifier; },
|
|
TypeofExpr: TypeofExpr,
|
|
get UnaryOperator () { return UnaryOperator; },
|
|
UnaryOperatorExpr: UnaryOperatorExpr,
|
|
VoidExpr: VoidExpr,
|
|
WrappedNodeExpr: WrappedNodeExpr,
|
|
areAllEquivalent: areAllEquivalent,
|
|
arrowFn: arrowFn,
|
|
expressionType: expressionType,
|
|
fn: fn,
|
|
ifStmt: ifStmt,
|
|
importExpr: importExpr,
|
|
importType: importType,
|
|
isNull: isNull,
|
|
jsDocComment: jsDocComment,
|
|
leadingComment: leadingComment,
|
|
literal: literal,
|
|
literalArr: literalArr,
|
|
literalMap: literalMap,
|
|
localizedString: localizedString,
|
|
not: not,
|
|
nullSafeIsEquivalent: nullSafeIsEquivalent,
|
|
taggedTemplate: taggedTemplate,
|
|
transplantedType: transplantedType,
|
|
typeofExpr: typeofExpr,
|
|
unary: unary,
|
|
variable: variable
|
|
});
|
|
|
|
const CONSTANT_PREFIX = '_c';
|
|
/**
|
|
* `ConstantPool` tries to reuse literal factories when two or more literals are identical.
|
|
* We determine whether literals are identical by creating a key out of their AST using the
|
|
* `KeyVisitor`. This constant is used to replace dynamic expressions which can't be safely
|
|
* converted into a key. E.g. given an expression `{foo: bar()}`, since we don't know what
|
|
* the result of `bar` will be, we create a key that looks like `{foo: <unknown>}`. Note
|
|
* that we use a variable, rather than something like `null` in order to avoid collisions.
|
|
*/
|
|
const UNKNOWN_VALUE_KEY = variable('<unknown>');
|
|
/**
|
|
* Context to use when producing a key.
|
|
*
|
|
* This ensures we see the constant not the reference variable when producing
|
|
* a key.
|
|
*/
|
|
const KEY_CONTEXT = {};
|
|
/**
|
|
* Generally all primitive values are excluded from the `ConstantPool`, but there is an exclusion
|
|
* for strings that reach a certain length threshold. This constant defines the length threshold for
|
|
* strings.
|
|
*/
|
|
const POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS = 50;
|
|
/**
|
|
* A node that is a place-holder that allows the node to be replaced when the actual
|
|
* node is known.
|
|
*
|
|
* This allows the constant pool to change an expression from a direct reference to
|
|
* a constant to a shared constant. It returns a fix-up node that is later allowed to
|
|
* change the referenced expression.
|
|
*/
|
|
class FixupExpression extends Expression {
|
|
resolved;
|
|
original;
|
|
shared = false;
|
|
constructor(resolved) {
|
|
super(resolved.type);
|
|
this.resolved = resolved;
|
|
this.original = resolved;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
if (context === KEY_CONTEXT) {
|
|
// When producing a key we want to traverse the constant not the
|
|
// variable used to refer to it.
|
|
return this.original.visitExpression(visitor, context);
|
|
}
|
|
else {
|
|
return this.resolved.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved);
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
clone() {
|
|
throw new Error(`Not supported.`);
|
|
}
|
|
fixup(expression) {
|
|
this.resolved = expression;
|
|
this.shared = true;
|
|
}
|
|
}
|
|
/**
|
|
* A constant pool allows a code emitter to share constant in an output context.
|
|
*
|
|
* The constant pool also supports sharing access to ivy definitions references.
|
|
*/
|
|
class ConstantPool {
|
|
isClosureCompilerEnabled;
|
|
statements = [];
|
|
literals = new Map();
|
|
literalFactories = new Map();
|
|
sharedConstants = new Map();
|
|
/**
|
|
* Constant pool also tracks claimed names from {@link uniqueName}.
|
|
* This is useful to avoid collisions if variables are intended to be
|
|
* named a certain way- but may conflict. We wouldn't want to always suffix
|
|
* them with unique numbers.
|
|
*/
|
|
_claimedNames = new Map();
|
|
nextNameIndex = 0;
|
|
constructor(isClosureCompilerEnabled = false) {
|
|
this.isClosureCompilerEnabled = isClosureCompilerEnabled;
|
|
}
|
|
getConstLiteral(literal, forceShared) {
|
|
if ((literal instanceof LiteralExpr && !isLongStringLiteral(literal)) ||
|
|
literal instanceof FixupExpression) {
|
|
// Do no put simple literals into the constant pool or try to produce a constant for a
|
|
// reference to a constant.
|
|
return literal;
|
|
}
|
|
const key = GenericKeyFn.INSTANCE.keyOf(literal);
|
|
let fixup = this.literals.get(key);
|
|
let newValue = false;
|
|
if (!fixup) {
|
|
fixup = new FixupExpression(literal);
|
|
this.literals.set(key, fixup);
|
|
newValue = true;
|
|
}
|
|
if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
|
|
// Replace the expression with a variable
|
|
const name = this.freshName();
|
|
let value;
|
|
let usage;
|
|
if (this.isClosureCompilerEnabled && isLongStringLiteral(literal)) {
|
|
// For string literals, Closure will **always** inline the string at
|
|
// **all** usages, duplicating it each time. For large strings, this
|
|
// unnecessarily bloats bundle size. To work around this restriction, we
|
|
// wrap the string in a function, and call that function for each usage.
|
|
// This tricks Closure into using inline logic for functions instead of
|
|
// string literals. Function calls are only inlined if the body is small
|
|
// enough to be worth it. By doing this, very large strings will be
|
|
// shared across multiple usages, rather than duplicating the string at
|
|
// each usage site.
|
|
//
|
|
// const myStr = function() { return "very very very long string"; };
|
|
// const usage1 = myStr();
|
|
// const usage2 = myStr();
|
|
value = new FunctionExpr([], // Params.
|
|
[
|
|
// Statements.
|
|
new ReturnStatement(literal),
|
|
]);
|
|
usage = variable(name).callFn([]);
|
|
}
|
|
else {
|
|
// Just declare and use the variable directly, without a function call
|
|
// indirection. This saves a few bytes and avoids an unnecessary call.
|
|
value = literal;
|
|
usage = variable(name);
|
|
}
|
|
this.statements.push(new DeclareVarStmt(name, value, INFERRED_TYPE, StmtModifier.Final));
|
|
fixup.fixup(usage);
|
|
}
|
|
return fixup;
|
|
}
|
|
getSharedConstant(def, expr) {
|
|
const key = def.keyOf(expr);
|
|
if (!this.sharedConstants.has(key)) {
|
|
const id = this.freshName();
|
|
this.sharedConstants.set(key, variable(id));
|
|
this.statements.push(def.toSharedConstantDeclaration(id, expr));
|
|
}
|
|
return this.sharedConstants.get(key);
|
|
}
|
|
getLiteralFactory(literal) {
|
|
// Create a pure function that builds an array of a mix of constant and variable expressions
|
|
if (literal instanceof LiteralArrayExpr) {
|
|
const argumentsForKey = literal.entries.map((e) => (e.isConstant() ? e : UNKNOWN_VALUE_KEY));
|
|
const key = GenericKeyFn.INSTANCE.keyOf(literalArr(argumentsForKey));
|
|
return this._getLiteralFactory(key, literal.entries, (entries) => literalArr(entries));
|
|
}
|
|
else {
|
|
const expressionForKey = literalMap(literal.entries.map((e) => ({
|
|
key: e.key,
|
|
value: e.value.isConstant() ? e.value : UNKNOWN_VALUE_KEY,
|
|
quoted: e.quoted,
|
|
})));
|
|
const key = GenericKeyFn.INSTANCE.keyOf(expressionForKey);
|
|
return this._getLiteralFactory(key, literal.entries.map((e) => e.value), (entries) => literalMap(entries.map((value, index) => ({
|
|
key: literal.entries[index].key,
|
|
value,
|
|
quoted: literal.entries[index].quoted,
|
|
}))));
|
|
}
|
|
}
|
|
// TODO: useUniqueName(false) is necessary for naming compatibility with
|
|
// TemplateDefinitionBuilder, but should be removed once Template Pipeline is the default.
|
|
getSharedFunctionReference(fn, prefix, useUniqueName = true) {
|
|
const isArrow = fn instanceof ArrowFunctionExpr;
|
|
for (const current of this.statements) {
|
|
// Arrow functions are saved as variables so we check if the
|
|
// value of the variable is the same as the arrow function.
|
|
if (isArrow && current instanceof DeclareVarStmt && current.value?.isEquivalent(fn)) {
|
|
return variable(current.name);
|
|
}
|
|
// Function declarations are saved as function statements
|
|
// so we compare them directly to the passed-in function.
|
|
if (!isArrow &&
|
|
current instanceof DeclareFunctionStmt &&
|
|
fn instanceof FunctionExpr &&
|
|
fn.isEquivalent(current)) {
|
|
return variable(current.name);
|
|
}
|
|
}
|
|
// Otherwise declare the function.
|
|
const name = useUniqueName ? this.uniqueName(prefix) : prefix;
|
|
this.statements.push(fn instanceof FunctionExpr
|
|
? fn.toDeclStmt(name, StmtModifier.Final)
|
|
: new DeclareVarStmt(name, fn, INFERRED_TYPE, StmtModifier.Final, fn.sourceSpan));
|
|
return variable(name);
|
|
}
|
|
_getLiteralFactory(key, values, resultMap) {
|
|
let literalFactory = this.literalFactories.get(key);
|
|
const literalFactoryArguments = values.filter((e) => !e.isConstant());
|
|
if (!literalFactory) {
|
|
const resultExpressions = values.map((e, index) => e.isConstant() ? this.getConstLiteral(e, true) : variable(`a${index}`));
|
|
const parameters = resultExpressions
|
|
.filter(isVariable)
|
|
.map((e) => new FnParam(e.name, DYNAMIC_TYPE));
|
|
const pureFunctionDeclaration = arrowFn(parameters, resultMap(resultExpressions), INFERRED_TYPE);
|
|
const name = this.freshName();
|
|
this.statements.push(new DeclareVarStmt(name, pureFunctionDeclaration, INFERRED_TYPE, StmtModifier.Final));
|
|
literalFactory = variable(name);
|
|
this.literalFactories.set(key, literalFactory);
|
|
}
|
|
return { literalFactory, literalFactoryArguments };
|
|
}
|
|
/**
|
|
* Produce a unique name in the context of this pool.
|
|
*
|
|
* The name might be unique among different prefixes if any of the prefixes end in
|
|
* a digit so the prefix should be a constant string (not based on user input) and
|
|
* must not end in a digit.
|
|
*/
|
|
uniqueName(name, alwaysIncludeSuffix = true) {
|
|
const count = this._claimedNames.get(name) ?? 0;
|
|
const result = count === 0 && !alwaysIncludeSuffix ? `${name}` : `${name}${count}`;
|
|
this._claimedNames.set(name, count + 1);
|
|
return result;
|
|
}
|
|
freshName() {
|
|
return this.uniqueName(CONSTANT_PREFIX);
|
|
}
|
|
}
|
|
class GenericKeyFn {
|
|
static INSTANCE = new GenericKeyFn();
|
|
keyOf(expr) {
|
|
if (expr instanceof LiteralExpr && typeof expr.value === 'string') {
|
|
return `"${expr.value}"`;
|
|
}
|
|
else if (expr instanceof LiteralExpr) {
|
|
return String(expr.value);
|
|
}
|
|
else if (expr instanceof LiteralArrayExpr) {
|
|
const entries = [];
|
|
for (const entry of expr.entries) {
|
|
entries.push(this.keyOf(entry));
|
|
}
|
|
return `[${entries.join(',')}]`;
|
|
}
|
|
else if (expr instanceof LiteralMapExpr) {
|
|
const entries = [];
|
|
for (const entry of expr.entries) {
|
|
let key = entry.key;
|
|
if (entry.quoted) {
|
|
key = `"${key}"`;
|
|
}
|
|
entries.push(key + ':' + this.keyOf(entry.value));
|
|
}
|
|
return `{${entries.join(',')}}`;
|
|
}
|
|
else if (expr instanceof ExternalExpr) {
|
|
return `import("${expr.value.moduleName}", ${expr.value.name})`;
|
|
}
|
|
else if (expr instanceof ReadVarExpr) {
|
|
return `read(${expr.name})`;
|
|
}
|
|
else if (expr instanceof TypeofExpr) {
|
|
return `typeof(${this.keyOf(expr.expr)})`;
|
|
}
|
|
else {
|
|
throw new Error(`${this.constructor.name} does not handle expressions of type ${expr.constructor.name}`);
|
|
}
|
|
}
|
|
}
|
|
function isVariable(e) {
|
|
return e instanceof ReadVarExpr;
|
|
}
|
|
function isLongStringLiteral(expr) {
|
|
return (expr instanceof LiteralExpr &&
|
|
typeof expr.value === 'string' &&
|
|
expr.value.length >= POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS);
|
|
}
|
|
|
|
const CORE = '@angular/core';
|
|
class Identifiers {
|
|
/* Methods */
|
|
static NEW_METHOD = 'factory';
|
|
static TRANSFORM_METHOD = 'transform';
|
|
static PATCH_DEPS = 'patchedDeps';
|
|
static core = { name: null, moduleName: CORE };
|
|
/* Instructions */
|
|
static namespaceHTML = { name: 'ɵɵnamespaceHTML', moduleName: CORE };
|
|
static namespaceMathML = { name: 'ɵɵnamespaceMathML', moduleName: CORE };
|
|
static namespaceSVG = { name: 'ɵɵnamespaceSVG', moduleName: CORE };
|
|
static element = { name: 'ɵɵelement', moduleName: CORE };
|
|
static elementStart = { name: 'ɵɵelementStart', moduleName: CORE };
|
|
static elementEnd = { name: 'ɵɵelementEnd', moduleName: CORE };
|
|
static domElement = { name: 'ɵɵdomElement', moduleName: CORE };
|
|
static domElementStart = { name: 'ɵɵdomElementStart', moduleName: CORE };
|
|
static domElementEnd = { name: 'ɵɵdomElementEnd', moduleName: CORE };
|
|
static domElementContainer = {
|
|
name: 'ɵɵdomElementContainer',
|
|
moduleName: CORE,
|
|
};
|
|
static domElementContainerStart = {
|
|
name: 'ɵɵdomElementContainerStart',
|
|
moduleName: CORE,
|
|
};
|
|
static domElementContainerEnd = {
|
|
name: 'ɵɵdomElementContainerEnd',
|
|
moduleName: CORE,
|
|
};
|
|
static domTemplate = { name: 'ɵɵdomTemplate', moduleName: CORE };
|
|
static domListener = { name: 'ɵɵdomListener', moduleName: CORE };
|
|
static advance = { name: 'ɵɵadvance', moduleName: CORE };
|
|
static syntheticHostProperty = {
|
|
name: 'ɵɵsyntheticHostProperty',
|
|
moduleName: CORE,
|
|
};
|
|
static syntheticHostListener = {
|
|
name: 'ɵɵsyntheticHostListener',
|
|
moduleName: CORE,
|
|
};
|
|
static attribute = { name: 'ɵɵattribute', moduleName: CORE };
|
|
static classProp = { name: 'ɵɵclassProp', moduleName: CORE };
|
|
static elementContainerStart = {
|
|
name: 'ɵɵelementContainerStart',
|
|
moduleName: CORE,
|
|
};
|
|
static elementContainerEnd = {
|
|
name: 'ɵɵelementContainerEnd',
|
|
moduleName: CORE,
|
|
};
|
|
static elementContainer = { name: 'ɵɵelementContainer', moduleName: CORE };
|
|
static styleMap = { name: 'ɵɵstyleMap', moduleName: CORE };
|
|
static classMap = { name: 'ɵɵclassMap', moduleName: CORE };
|
|
static styleProp = { name: 'ɵɵstyleProp', moduleName: CORE };
|
|
static interpolate = {
|
|
name: 'ɵɵinterpolate',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate1 = {
|
|
name: 'ɵɵinterpolate1',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate2 = {
|
|
name: 'ɵɵinterpolate2',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate3 = {
|
|
name: 'ɵɵinterpolate3',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate4 = {
|
|
name: 'ɵɵinterpolate4',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate5 = {
|
|
name: 'ɵɵinterpolate5',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate6 = {
|
|
name: 'ɵɵinterpolate6',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate7 = {
|
|
name: 'ɵɵinterpolate7',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolate8 = {
|
|
name: 'ɵɵinterpolate8',
|
|
moduleName: CORE,
|
|
};
|
|
static interpolateV = {
|
|
name: 'ɵɵinterpolateV',
|
|
moduleName: CORE,
|
|
};
|
|
static nextContext = { name: 'ɵɵnextContext', moduleName: CORE };
|
|
static resetView = { name: 'ɵɵresetView', moduleName: CORE };
|
|
static templateCreate = { name: 'ɵɵtemplate', moduleName: CORE };
|
|
static defer = { name: 'ɵɵdefer', moduleName: CORE };
|
|
static deferWhen = { name: 'ɵɵdeferWhen', moduleName: CORE };
|
|
static deferOnIdle = { name: 'ɵɵdeferOnIdle', moduleName: CORE };
|
|
static deferOnImmediate = { name: 'ɵɵdeferOnImmediate', moduleName: CORE };
|
|
static deferOnTimer = { name: 'ɵɵdeferOnTimer', moduleName: CORE };
|
|
static deferOnHover = { name: 'ɵɵdeferOnHover', moduleName: CORE };
|
|
static deferOnInteraction = { name: 'ɵɵdeferOnInteraction', moduleName: CORE };
|
|
static deferOnViewport = { name: 'ɵɵdeferOnViewport', moduleName: CORE };
|
|
static deferPrefetchWhen = { name: 'ɵɵdeferPrefetchWhen', moduleName: CORE };
|
|
static deferPrefetchOnIdle = {
|
|
name: 'ɵɵdeferPrefetchOnIdle',
|
|
moduleName: CORE,
|
|
};
|
|
static deferPrefetchOnImmediate = {
|
|
name: 'ɵɵdeferPrefetchOnImmediate',
|
|
moduleName: CORE,
|
|
};
|
|
static deferPrefetchOnTimer = {
|
|
name: 'ɵɵdeferPrefetchOnTimer',
|
|
moduleName: CORE,
|
|
};
|
|
static deferPrefetchOnHover = {
|
|
name: 'ɵɵdeferPrefetchOnHover',
|
|
moduleName: CORE,
|
|
};
|
|
static deferPrefetchOnInteraction = {
|
|
name: 'ɵɵdeferPrefetchOnInteraction',
|
|
moduleName: CORE,
|
|
};
|
|
static deferPrefetchOnViewport = {
|
|
name: 'ɵɵdeferPrefetchOnViewport',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateWhen = { name: 'ɵɵdeferHydrateWhen', moduleName: CORE };
|
|
static deferHydrateNever = { name: 'ɵɵdeferHydrateNever', moduleName: CORE };
|
|
static deferHydrateOnIdle = {
|
|
name: 'ɵɵdeferHydrateOnIdle',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateOnImmediate = {
|
|
name: 'ɵɵdeferHydrateOnImmediate',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateOnTimer = {
|
|
name: 'ɵɵdeferHydrateOnTimer',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateOnHover = {
|
|
name: 'ɵɵdeferHydrateOnHover',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateOnInteraction = {
|
|
name: 'ɵɵdeferHydrateOnInteraction',
|
|
moduleName: CORE,
|
|
};
|
|
static deferHydrateOnViewport = {
|
|
name: 'ɵɵdeferHydrateOnViewport',
|
|
moduleName: CORE,
|
|
};
|
|
static deferEnableTimerScheduling = {
|
|
name: 'ɵɵdeferEnableTimerScheduling',
|
|
moduleName: CORE,
|
|
};
|
|
static conditionalCreate = { name: 'ɵɵconditionalCreate', moduleName: CORE };
|
|
static conditionalBranchCreate = {
|
|
name: 'ɵɵconditionalBranchCreate',
|
|
moduleName: CORE,
|
|
};
|
|
static conditional = { name: 'ɵɵconditional', moduleName: CORE };
|
|
static repeater = { name: 'ɵɵrepeater', moduleName: CORE };
|
|
static repeaterCreate = { name: 'ɵɵrepeaterCreate', moduleName: CORE };
|
|
static repeaterTrackByIndex = {
|
|
name: 'ɵɵrepeaterTrackByIndex',
|
|
moduleName: CORE,
|
|
};
|
|
static repeaterTrackByIdentity = {
|
|
name: 'ɵɵrepeaterTrackByIdentity',
|
|
moduleName: CORE,
|
|
};
|
|
static componentInstance = { name: 'ɵɵcomponentInstance', moduleName: CORE };
|
|
static text = { name: 'ɵɵtext', moduleName: CORE };
|
|
static enableBindings = { name: 'ɵɵenableBindings', moduleName: CORE };
|
|
static disableBindings = { name: 'ɵɵdisableBindings', moduleName: CORE };
|
|
static getCurrentView = { name: 'ɵɵgetCurrentView', moduleName: CORE };
|
|
static textInterpolate = { name: 'ɵɵtextInterpolate', moduleName: CORE };
|
|
static textInterpolate1 = { name: 'ɵɵtextInterpolate1', moduleName: CORE };
|
|
static textInterpolate2 = { name: 'ɵɵtextInterpolate2', moduleName: CORE };
|
|
static textInterpolate3 = { name: 'ɵɵtextInterpolate3', moduleName: CORE };
|
|
static textInterpolate4 = { name: 'ɵɵtextInterpolate4', moduleName: CORE };
|
|
static textInterpolate5 = { name: 'ɵɵtextInterpolate5', moduleName: CORE };
|
|
static textInterpolate6 = { name: 'ɵɵtextInterpolate6', moduleName: CORE };
|
|
static textInterpolate7 = { name: 'ɵɵtextInterpolate7', moduleName: CORE };
|
|
static textInterpolate8 = { name: 'ɵɵtextInterpolate8', moduleName: CORE };
|
|
static textInterpolateV = { name: 'ɵɵtextInterpolateV', moduleName: CORE };
|
|
static restoreView = { name: 'ɵɵrestoreView', moduleName: CORE };
|
|
static pureFunction0 = { name: 'ɵɵpureFunction0', moduleName: CORE };
|
|
static pureFunction1 = { name: 'ɵɵpureFunction1', moduleName: CORE };
|
|
static pureFunction2 = { name: 'ɵɵpureFunction2', moduleName: CORE };
|
|
static pureFunction3 = { name: 'ɵɵpureFunction3', moduleName: CORE };
|
|
static pureFunction4 = { name: 'ɵɵpureFunction4', moduleName: CORE };
|
|
static pureFunction5 = { name: 'ɵɵpureFunction5', moduleName: CORE };
|
|
static pureFunction6 = { name: 'ɵɵpureFunction6', moduleName: CORE };
|
|
static pureFunction7 = { name: 'ɵɵpureFunction7', moduleName: CORE };
|
|
static pureFunction8 = { name: 'ɵɵpureFunction8', moduleName: CORE };
|
|
static pureFunctionV = { name: 'ɵɵpureFunctionV', moduleName: CORE };
|
|
static pipeBind1 = { name: 'ɵɵpipeBind1', moduleName: CORE };
|
|
static pipeBind2 = { name: 'ɵɵpipeBind2', moduleName: CORE };
|
|
static pipeBind3 = { name: 'ɵɵpipeBind3', moduleName: CORE };
|
|
static pipeBind4 = { name: 'ɵɵpipeBind4', moduleName: CORE };
|
|
static pipeBindV = { name: 'ɵɵpipeBindV', moduleName: CORE };
|
|
static domProperty = { name: 'ɵɵdomProperty', moduleName: CORE };
|
|
static ariaProperty = { name: 'ɵɵariaProperty', moduleName: CORE };
|
|
static property = { name: 'ɵɵproperty', moduleName: CORE };
|
|
static animationEnterListener = {
|
|
name: 'ɵɵanimateEnterListener',
|
|
moduleName: CORE,
|
|
};
|
|
static animationLeaveListener = {
|
|
name: 'ɵɵanimateLeaveListener',
|
|
moduleName: CORE,
|
|
};
|
|
static animationEnter = { name: 'ɵɵanimateEnter', moduleName: CORE };
|
|
static animationLeave = { name: 'ɵɵanimateLeave', moduleName: CORE };
|
|
static i18n = { name: 'ɵɵi18n', moduleName: CORE };
|
|
static i18nAttributes = { name: 'ɵɵi18nAttributes', moduleName: CORE };
|
|
static i18nExp = { name: 'ɵɵi18nExp', moduleName: CORE };
|
|
static i18nStart = { name: 'ɵɵi18nStart', moduleName: CORE };
|
|
static i18nEnd = { name: 'ɵɵi18nEnd', moduleName: CORE };
|
|
static i18nApply = { name: 'ɵɵi18nApply', moduleName: CORE };
|
|
static i18nPostprocess = { name: 'ɵɵi18nPostprocess', moduleName: CORE };
|
|
static pipe = { name: 'ɵɵpipe', moduleName: CORE };
|
|
static projection = { name: 'ɵɵprojection', moduleName: CORE };
|
|
static projectionDef = { name: 'ɵɵprojectionDef', moduleName: CORE };
|
|
static reference = { name: 'ɵɵreference', moduleName: CORE };
|
|
static inject = { name: 'ɵɵinject', moduleName: CORE };
|
|
static injectAttribute = { name: 'ɵɵinjectAttribute', moduleName: CORE };
|
|
static directiveInject = { name: 'ɵɵdirectiveInject', moduleName: CORE };
|
|
static invalidFactory = { name: 'ɵɵinvalidFactory', moduleName: CORE };
|
|
static invalidFactoryDep = { name: 'ɵɵinvalidFactoryDep', moduleName: CORE };
|
|
static templateRefExtractor = {
|
|
name: 'ɵɵtemplateRefExtractor',
|
|
moduleName: CORE,
|
|
};
|
|
static forwardRef = { name: 'forwardRef', moduleName: CORE };
|
|
static resolveForwardRef = { name: 'resolveForwardRef', moduleName: CORE };
|
|
static replaceMetadata = { name: 'ɵɵreplaceMetadata', moduleName: CORE };
|
|
static getReplaceMetadataURL = {
|
|
name: 'ɵɵgetReplaceMetadataURL',
|
|
moduleName: CORE,
|
|
};
|
|
static ɵɵdefineInjectable = { name: 'ɵɵdefineInjectable', moduleName: CORE };
|
|
static declareInjectable = { name: 'ɵɵngDeclareInjectable', moduleName: CORE };
|
|
static InjectableDeclaration = {
|
|
name: 'ɵɵInjectableDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static resolveWindow = { name: 'ɵɵresolveWindow', moduleName: CORE };
|
|
static resolveDocument = { name: 'ɵɵresolveDocument', moduleName: CORE };
|
|
static resolveBody = { name: 'ɵɵresolveBody', moduleName: CORE };
|
|
static getComponentDepsFactory = {
|
|
name: 'ɵɵgetComponentDepsFactory',
|
|
moduleName: CORE,
|
|
};
|
|
static defineComponent = { name: 'ɵɵdefineComponent', moduleName: CORE };
|
|
static declareComponent = { name: 'ɵɵngDeclareComponent', moduleName: CORE };
|
|
static setComponentScope = { name: 'ɵɵsetComponentScope', moduleName: CORE };
|
|
static ChangeDetectionStrategy = {
|
|
name: 'ChangeDetectionStrategy',
|
|
moduleName: CORE,
|
|
};
|
|
static ViewEncapsulation = {
|
|
name: 'ViewEncapsulation',
|
|
moduleName: CORE,
|
|
};
|
|
static ComponentDeclaration = {
|
|
name: 'ɵɵComponentDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static FactoryDeclaration = {
|
|
name: 'ɵɵFactoryDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static declareFactory = { name: 'ɵɵngDeclareFactory', moduleName: CORE };
|
|
static FactoryTarget = { name: 'ɵɵFactoryTarget', moduleName: CORE };
|
|
static defineDirective = { name: 'ɵɵdefineDirective', moduleName: CORE };
|
|
static declareDirective = { name: 'ɵɵngDeclareDirective', moduleName: CORE };
|
|
static DirectiveDeclaration = {
|
|
name: 'ɵɵDirectiveDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static InjectorDef = { name: 'ɵɵInjectorDef', moduleName: CORE };
|
|
static InjectorDeclaration = {
|
|
name: 'ɵɵInjectorDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static defineInjector = { name: 'ɵɵdefineInjector', moduleName: CORE };
|
|
static declareInjector = { name: 'ɵɵngDeclareInjector', moduleName: CORE };
|
|
static NgModuleDeclaration = {
|
|
name: 'ɵɵNgModuleDeclaration',
|
|
moduleName: CORE,
|
|
};
|
|
static ModuleWithProviders = {
|
|
name: 'ModuleWithProviders',
|
|
moduleName: CORE,
|
|
};
|
|
static defineNgModule = { name: 'ɵɵdefineNgModule', moduleName: CORE };
|
|
static declareNgModule = { name: 'ɵɵngDeclareNgModule', moduleName: CORE };
|
|
static setNgModuleScope = { name: 'ɵɵsetNgModuleScope', moduleName: CORE };
|
|
static registerNgModuleType = {
|
|
name: 'ɵɵregisterNgModuleType',
|
|
moduleName: CORE,
|
|
};
|
|
static PipeDeclaration = { name: 'ɵɵPipeDeclaration', moduleName: CORE };
|
|
static definePipe = { name: 'ɵɵdefinePipe', moduleName: CORE };
|
|
static declarePipe = { name: 'ɵɵngDeclarePipe', moduleName: CORE };
|
|
static declareClassMetadata = {
|
|
name: 'ɵɵngDeclareClassMetadata',
|
|
moduleName: CORE,
|
|
};
|
|
static declareClassMetadataAsync = {
|
|
name: 'ɵɵngDeclareClassMetadataAsync',
|
|
moduleName: CORE,
|
|
};
|
|
static setClassMetadata = { name: 'ɵsetClassMetadata', moduleName: CORE };
|
|
static setClassMetadataAsync = {
|
|
name: 'ɵsetClassMetadataAsync',
|
|
moduleName: CORE,
|
|
};
|
|
static setClassDebugInfo = { name: 'ɵsetClassDebugInfo', moduleName: CORE };
|
|
static queryRefresh = { name: 'ɵɵqueryRefresh', moduleName: CORE };
|
|
static viewQuery = { name: 'ɵɵviewQuery', moduleName: CORE };
|
|
static loadQuery = { name: 'ɵɵloadQuery', moduleName: CORE };
|
|
static contentQuery = { name: 'ɵɵcontentQuery', moduleName: CORE };
|
|
// Signal queries
|
|
static viewQuerySignal = { name: 'ɵɵviewQuerySignal', moduleName: CORE };
|
|
static contentQuerySignal = { name: 'ɵɵcontentQuerySignal', moduleName: CORE };
|
|
static queryAdvance = { name: 'ɵɵqueryAdvance', moduleName: CORE };
|
|
// Two-way bindings
|
|
static twoWayProperty = { name: 'ɵɵtwoWayProperty', moduleName: CORE };
|
|
static twoWayBindingSet = { name: 'ɵɵtwoWayBindingSet', moduleName: CORE };
|
|
static twoWayListener = { name: 'ɵɵtwoWayListener', moduleName: CORE };
|
|
static declareLet = { name: 'ɵɵdeclareLet', moduleName: CORE };
|
|
static storeLet = { name: 'ɵɵstoreLet', moduleName: CORE };
|
|
static readContextLet = { name: 'ɵɵreadContextLet', moduleName: CORE };
|
|
static attachSourceLocations = {
|
|
name: 'ɵɵattachSourceLocations',
|
|
moduleName: CORE,
|
|
};
|
|
static NgOnChangesFeature = { name: 'ɵɵNgOnChangesFeature', moduleName: CORE };
|
|
static InheritDefinitionFeature = {
|
|
name: 'ɵɵInheritDefinitionFeature',
|
|
moduleName: CORE,
|
|
};
|
|
static CopyDefinitionFeature = {
|
|
name: 'ɵɵCopyDefinitionFeature',
|
|
moduleName: CORE,
|
|
};
|
|
static ProvidersFeature = { name: 'ɵɵProvidersFeature', moduleName: CORE };
|
|
static HostDirectivesFeature = {
|
|
name: 'ɵɵHostDirectivesFeature',
|
|
moduleName: CORE,
|
|
};
|
|
static ExternalStylesFeature = {
|
|
name: 'ɵɵExternalStylesFeature',
|
|
moduleName: CORE,
|
|
};
|
|
static listener = { name: 'ɵɵlistener', moduleName: CORE };
|
|
static getInheritedFactory = {
|
|
name: 'ɵɵgetInheritedFactory',
|
|
moduleName: CORE,
|
|
};
|
|
// sanitization-related functions
|
|
static sanitizeHtml = { name: 'ɵɵsanitizeHtml', moduleName: CORE };
|
|
static sanitizeStyle = { name: 'ɵɵsanitizeStyle', moduleName: CORE };
|
|
static sanitizeResourceUrl = {
|
|
name: 'ɵɵsanitizeResourceUrl',
|
|
moduleName: CORE,
|
|
};
|
|
static sanitizeScript = { name: 'ɵɵsanitizeScript', moduleName: CORE };
|
|
static sanitizeUrl = { name: 'ɵɵsanitizeUrl', moduleName: CORE };
|
|
static sanitizeUrlOrResourceUrl = {
|
|
name: 'ɵɵsanitizeUrlOrResourceUrl',
|
|
moduleName: CORE,
|
|
};
|
|
static trustConstantHtml = { name: 'ɵɵtrustConstantHtml', moduleName: CORE };
|
|
static trustConstantResourceUrl = {
|
|
name: 'ɵɵtrustConstantResourceUrl',
|
|
moduleName: CORE,
|
|
};
|
|
static validateIframeAttribute = {
|
|
name: 'ɵɵvalidateIframeAttribute',
|
|
moduleName: CORE,
|
|
};
|
|
// Decorators
|
|
static inputDecorator = { name: 'Input', moduleName: CORE };
|
|
static outputDecorator = { name: 'Output', moduleName: CORE };
|
|
static viewChildDecorator = { name: 'ViewChild', moduleName: CORE };
|
|
static viewChildrenDecorator = { name: 'ViewChildren', moduleName: CORE };
|
|
static contentChildDecorator = { name: 'ContentChild', moduleName: CORE };
|
|
static contentChildrenDecorator = {
|
|
name: 'ContentChildren',
|
|
moduleName: CORE,
|
|
};
|
|
// type-checking
|
|
static InputSignalBrandWriteType = { name: 'ɵINPUT_SIGNAL_BRAND_WRITE_TYPE', moduleName: CORE };
|
|
static UnwrapDirectiveSignalInputs = { name: 'ɵUnwrapDirectiveSignalInputs', moduleName: CORE };
|
|
static unwrapWritableSignal = { name: 'ɵunwrapWritableSignal', moduleName: CORE };
|
|
static assertType = { name: 'ɵassertType', moduleName: CORE };
|
|
}
|
|
|
|
const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
|
|
function dashCaseToCamelCase(input) {
|
|
return input.replace(DASH_CASE_REGEXP, (...m) => m[1].toUpperCase());
|
|
}
|
|
function splitAtColon(input, defaultValues) {
|
|
return _splitAt(input, ':', defaultValues);
|
|
}
|
|
function splitAtPeriod(input, defaultValues) {
|
|
return _splitAt(input, '.', defaultValues);
|
|
}
|
|
function _splitAt(input, character, defaultValues) {
|
|
const characterIndex = input.indexOf(character);
|
|
if (characterIndex == -1)
|
|
return defaultValues;
|
|
return [input.slice(0, characterIndex).trim(), input.slice(characterIndex + 1).trim()];
|
|
}
|
|
function noUndefined(val) {
|
|
return val === undefined ? null : val;
|
|
}
|
|
// Escape characters that have a special meaning in Regular Expressions
|
|
function escapeRegExp(s) {
|
|
return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
|
|
}
|
|
function utf8Encode(str) {
|
|
let encoded = [];
|
|
for (let index = 0; index < str.length; index++) {
|
|
let codePoint = str.charCodeAt(index);
|
|
// decode surrogate
|
|
// see https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
|
if (codePoint >= 0xd800 && codePoint <= 0xdbff && str.length > index + 1) {
|
|
const low = str.charCodeAt(index + 1);
|
|
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
index++;
|
|
codePoint = ((codePoint - 0xd800) << 10) + low - 0xdc00 + 0x10000;
|
|
}
|
|
}
|
|
if (codePoint <= 0x7f) {
|
|
encoded.push(codePoint);
|
|
}
|
|
else if (codePoint <= 0x7ff) {
|
|
encoded.push(((codePoint >> 6) & 0x1f) | 0xc0, (codePoint & 0x3f) | 0x80);
|
|
}
|
|
else if (codePoint <= 0xffff) {
|
|
encoded.push((codePoint >> 12) | 0xe0, ((codePoint >> 6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80);
|
|
}
|
|
else if (codePoint <= 0x1fffff) {
|
|
encoded.push(((codePoint >> 18) & 0x07) | 0xf0, ((codePoint >> 12) & 0x3f) | 0x80, ((codePoint >> 6) & 0x3f) | 0x80, (codePoint & 0x3f) | 0x80);
|
|
}
|
|
}
|
|
return encoded;
|
|
}
|
|
function stringify(token) {
|
|
if (typeof token === 'string') {
|
|
return token;
|
|
}
|
|
if (Array.isArray(token)) {
|
|
return `[${token.map(stringify).join(', ')}]`;
|
|
}
|
|
if (token == null) {
|
|
return '' + token;
|
|
}
|
|
const name = token.overriddenName || token.name;
|
|
if (name) {
|
|
return `${name}`;
|
|
}
|
|
if (!token.toString) {
|
|
return 'object';
|
|
}
|
|
// WARNING: do not try to `JSON.stringify(token)` here
|
|
// see https://github.com/angular/angular/issues/23440
|
|
const result = token.toString();
|
|
if (result == null) {
|
|
return '' + result;
|
|
}
|
|
const newLineIndex = result.indexOf('\n');
|
|
return newLineIndex >= 0 ? result.slice(0, newLineIndex) : result;
|
|
}
|
|
class Version {
|
|
full;
|
|
major;
|
|
minor;
|
|
patch;
|
|
constructor(full) {
|
|
this.full = full;
|
|
const splits = full.split('.');
|
|
this.major = splits[0];
|
|
this.minor = splits[1];
|
|
this.patch = splits.slice(2).join('.');
|
|
}
|
|
}
|
|
const _global = globalThis;
|
|
const V1_TO_18 = /^([1-9]|1[0-8])\./;
|
|
function getJitStandaloneDefaultForVersion(version) {
|
|
if (version.startsWith('0.')) {
|
|
// 0.0.0 is always "latest", default is true.
|
|
return true;
|
|
}
|
|
if (V1_TO_18.test(version)) {
|
|
// Angular v2 - v18 default is false.
|
|
return false;
|
|
}
|
|
// All other Angular versions (v19+) default to true.
|
|
return true;
|
|
}
|
|
|
|
// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
|
|
const VERSION$1 = 3;
|
|
const JS_B64_PREFIX = '# sourceMappingURL=data:application/json;base64,';
|
|
class SourceMapGenerator {
|
|
file;
|
|
sourcesContent = new Map();
|
|
lines = [];
|
|
lastCol0 = 0;
|
|
hasMappings = false;
|
|
constructor(file = null) {
|
|
this.file = file;
|
|
}
|
|
// The content is `null` when the content is expected to be loaded using the URL
|
|
addSource(url, content = null) {
|
|
if (!this.sourcesContent.has(url)) {
|
|
this.sourcesContent.set(url, content);
|
|
}
|
|
return this;
|
|
}
|
|
addLine() {
|
|
this.lines.push([]);
|
|
this.lastCol0 = 0;
|
|
return this;
|
|
}
|
|
addMapping(col0, sourceUrl, sourceLine0, sourceCol0) {
|
|
if (!this.currentLine) {
|
|
throw new Error(`A line must be added before mappings can be added`);
|
|
}
|
|
if (sourceUrl != null && !this.sourcesContent.has(sourceUrl)) {
|
|
throw new Error(`Unknown source file "${sourceUrl}"`);
|
|
}
|
|
if (col0 == null) {
|
|
throw new Error(`The column in the generated code must be provided`);
|
|
}
|
|
if (col0 < this.lastCol0) {
|
|
throw new Error(`Mapping should be added in output order`);
|
|
}
|
|
if (sourceUrl && (sourceLine0 == null || sourceCol0 == null)) {
|
|
throw new Error(`The source location must be provided when a source url is provided`);
|
|
}
|
|
this.hasMappings = true;
|
|
this.lastCol0 = col0;
|
|
this.currentLine.push({ col0, sourceUrl, sourceLine0, sourceCol0 });
|
|
return this;
|
|
}
|
|
/**
|
|
* @internal strip this from published d.ts files due to
|
|
* https://github.com/microsoft/TypeScript/issues/36216
|
|
*/
|
|
get currentLine() {
|
|
return this.lines.slice(-1)[0];
|
|
}
|
|
toJSON() {
|
|
if (!this.hasMappings) {
|
|
return null;
|
|
}
|
|
const sourcesIndex = new Map();
|
|
const sources = [];
|
|
const sourcesContent = [];
|
|
Array.from(this.sourcesContent.keys()).forEach((url, i) => {
|
|
sourcesIndex.set(url, i);
|
|
sources.push(url);
|
|
sourcesContent.push(this.sourcesContent.get(url) || null);
|
|
});
|
|
let mappings = '';
|
|
let lastCol0 = 0;
|
|
let lastSourceIndex = 0;
|
|
let lastSourceLine0 = 0;
|
|
let lastSourceCol0 = 0;
|
|
this.lines.forEach((segments) => {
|
|
lastCol0 = 0;
|
|
mappings += segments
|
|
.map((segment) => {
|
|
// zero-based starting column of the line in the generated code
|
|
let segAsStr = toBase64VLQ(segment.col0 - lastCol0);
|
|
lastCol0 = segment.col0;
|
|
if (segment.sourceUrl != null) {
|
|
// zero-based index into the “sources” list
|
|
segAsStr += toBase64VLQ(sourcesIndex.get(segment.sourceUrl) - lastSourceIndex);
|
|
lastSourceIndex = sourcesIndex.get(segment.sourceUrl);
|
|
// the zero-based starting line in the original source
|
|
segAsStr += toBase64VLQ(segment.sourceLine0 - lastSourceLine0);
|
|
lastSourceLine0 = segment.sourceLine0;
|
|
// the zero-based starting column in the original source
|
|
segAsStr += toBase64VLQ(segment.sourceCol0 - lastSourceCol0);
|
|
lastSourceCol0 = segment.sourceCol0;
|
|
}
|
|
return segAsStr;
|
|
})
|
|
.join(',');
|
|
mappings += ';';
|
|
});
|
|
mappings = mappings.slice(0, -1);
|
|
return {
|
|
'file': this.file || '',
|
|
'version': VERSION$1,
|
|
'sourceRoot': '',
|
|
'sources': sources,
|
|
'sourcesContent': sourcesContent,
|
|
'mappings': mappings,
|
|
};
|
|
}
|
|
toJsComment() {
|
|
return this.hasMappings
|
|
? '//' + JS_B64_PREFIX + toBase64String(JSON.stringify(this, null, 0))
|
|
: '';
|
|
}
|
|
}
|
|
function toBase64String(value) {
|
|
let b64 = '';
|
|
const encoded = utf8Encode(value);
|
|
for (let i = 0; i < encoded.length;) {
|
|
const i1 = encoded[i++];
|
|
const i2 = i < encoded.length ? encoded[i++] : null;
|
|
const i3 = i < encoded.length ? encoded[i++] : null;
|
|
b64 += toBase64Digit(i1 >> 2);
|
|
b64 += toBase64Digit(((i1 & 3) << 4) | (i2 === null ? 0 : i2 >> 4));
|
|
b64 += i2 === null ? '=' : toBase64Digit(((i2 & 15) << 2) | (i3 === null ? 0 : i3 >> 6));
|
|
b64 += i2 === null || i3 === null ? '=' : toBase64Digit(i3 & 63);
|
|
}
|
|
return b64;
|
|
}
|
|
function toBase64VLQ(value) {
|
|
value = value < 0 ? (-value << 1) + 1 : value << 1;
|
|
let out = '';
|
|
do {
|
|
let digit = value & 31;
|
|
value = value >> 5;
|
|
if (value > 0) {
|
|
digit = digit | 32;
|
|
}
|
|
out += toBase64Digit(digit);
|
|
} while (value > 0);
|
|
return out;
|
|
}
|
|
const B64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
function toBase64Digit(value) {
|
|
if (value < 0 || value >= 64) {
|
|
throw new Error(`Can only encode value in the range [0, 63]`);
|
|
}
|
|
return B64_DIGITS[value];
|
|
}
|
|
|
|
const _SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n|\r|\$/g;
|
|
const _LEGAL_IDENTIFIER_RE = /^[$A-Z_][0-9A-Z_$]*$/i;
|
|
const _INDENT_WITH = ' ';
|
|
class _EmittedLine {
|
|
indent;
|
|
partsLength = 0;
|
|
parts = [];
|
|
srcSpans = [];
|
|
constructor(indent) {
|
|
this.indent = indent;
|
|
}
|
|
}
|
|
const BINARY_OPERATORS$1 = new Map([
|
|
[BinaryOperator.And, '&&'],
|
|
[BinaryOperator.Bigger, '>'],
|
|
[BinaryOperator.BiggerEquals, '>='],
|
|
[BinaryOperator.BitwiseOr, '|'],
|
|
[BinaryOperator.BitwiseAnd, '&'],
|
|
[BinaryOperator.Divide, '/'],
|
|
[BinaryOperator.Assign, '='],
|
|
[BinaryOperator.Equals, '=='],
|
|
[BinaryOperator.Identical, '==='],
|
|
[BinaryOperator.Lower, '<'],
|
|
[BinaryOperator.LowerEquals, '<='],
|
|
[BinaryOperator.Minus, '-'],
|
|
[BinaryOperator.Modulo, '%'],
|
|
[BinaryOperator.Exponentiation, '**'],
|
|
[BinaryOperator.Multiply, '*'],
|
|
[BinaryOperator.NotEquals, '!='],
|
|
[BinaryOperator.NotIdentical, '!=='],
|
|
[BinaryOperator.NullishCoalesce, '??'],
|
|
[BinaryOperator.Or, '||'],
|
|
[BinaryOperator.Plus, '+'],
|
|
[BinaryOperator.In, 'in'],
|
|
[BinaryOperator.AdditionAssignment, '+='],
|
|
[BinaryOperator.SubtractionAssignment, '-='],
|
|
[BinaryOperator.MultiplicationAssignment, '*='],
|
|
[BinaryOperator.DivisionAssignment, '/='],
|
|
[BinaryOperator.RemainderAssignment, '%='],
|
|
[BinaryOperator.ExponentiationAssignment, '**='],
|
|
[BinaryOperator.AndAssignment, '&&='],
|
|
[BinaryOperator.OrAssignment, '||='],
|
|
[BinaryOperator.NullishCoalesceAssignment, '??='],
|
|
]);
|
|
class EmitterVisitorContext {
|
|
_indent;
|
|
static createRoot() {
|
|
return new EmitterVisitorContext(0);
|
|
}
|
|
_lines;
|
|
constructor(_indent) {
|
|
this._indent = _indent;
|
|
this._lines = [new _EmittedLine(_indent)];
|
|
}
|
|
/**
|
|
* @internal strip this from published d.ts files due to
|
|
* https://github.com/microsoft/TypeScript/issues/36216
|
|
*/
|
|
get _currentLine() {
|
|
return this._lines[this._lines.length - 1];
|
|
}
|
|
println(from, lastPart = '') {
|
|
this.print(from || null, lastPart, true);
|
|
}
|
|
lineIsEmpty() {
|
|
return this._currentLine.parts.length === 0;
|
|
}
|
|
lineLength() {
|
|
return this._currentLine.indent * _INDENT_WITH.length + this._currentLine.partsLength;
|
|
}
|
|
print(from, part, newLine = false) {
|
|
if (part.length > 0) {
|
|
this._currentLine.parts.push(part);
|
|
this._currentLine.partsLength += part.length;
|
|
this._currentLine.srcSpans.push((from && from.sourceSpan) || null);
|
|
}
|
|
if (newLine) {
|
|
this._lines.push(new _EmittedLine(this._indent));
|
|
}
|
|
}
|
|
removeEmptyLastLine() {
|
|
if (this.lineIsEmpty()) {
|
|
this._lines.pop();
|
|
}
|
|
}
|
|
incIndent() {
|
|
this._indent++;
|
|
if (this.lineIsEmpty()) {
|
|
this._currentLine.indent = this._indent;
|
|
}
|
|
}
|
|
decIndent() {
|
|
this._indent--;
|
|
if (this.lineIsEmpty()) {
|
|
this._currentLine.indent = this._indent;
|
|
}
|
|
}
|
|
toSource() {
|
|
return this.sourceLines
|
|
.map((l) => (l.parts.length > 0 ? _createIndent(l.indent) + l.parts.join('') : ''))
|
|
.join('\n');
|
|
}
|
|
toSourceMapGenerator(genFilePath, startsAtLine = 0) {
|
|
const map = new SourceMapGenerator(genFilePath);
|
|
let firstOffsetMapped = false;
|
|
const mapFirstOffsetIfNeeded = () => {
|
|
if (!firstOffsetMapped) {
|
|
// Add a single space so that tools won't try to load the file from disk.
|
|
// Note: We are using virtual urls like `ng:///`, so we have to
|
|
// provide a content here.
|
|
map.addSource(genFilePath, ' ').addMapping(0, genFilePath, 0, 0);
|
|
firstOffsetMapped = true;
|
|
}
|
|
};
|
|
for (let i = 0; i < startsAtLine; i++) {
|
|
map.addLine();
|
|
mapFirstOffsetIfNeeded();
|
|
}
|
|
this.sourceLines.forEach((line, lineIdx) => {
|
|
map.addLine();
|
|
const spans = line.srcSpans;
|
|
const parts = line.parts;
|
|
let col0 = line.indent * _INDENT_WITH.length;
|
|
let spanIdx = 0;
|
|
// skip leading parts without source spans
|
|
while (spanIdx < spans.length && !spans[spanIdx]) {
|
|
col0 += parts[spanIdx].length;
|
|
spanIdx++;
|
|
}
|
|
if (spanIdx < spans.length && lineIdx === 0 && col0 === 0) {
|
|
firstOffsetMapped = true;
|
|
}
|
|
else {
|
|
mapFirstOffsetIfNeeded();
|
|
}
|
|
while (spanIdx < spans.length) {
|
|
const span = spans[spanIdx];
|
|
const source = span.start.file;
|
|
const sourceLine = span.start.line;
|
|
const sourceCol = span.start.col;
|
|
map
|
|
.addSource(source.url, source.content)
|
|
.addMapping(col0, source.url, sourceLine, sourceCol);
|
|
col0 += parts[spanIdx].length;
|
|
spanIdx++;
|
|
// assign parts without span or the same span to the previous segment
|
|
while (spanIdx < spans.length && (span === spans[spanIdx] || !spans[spanIdx])) {
|
|
col0 += parts[spanIdx].length;
|
|
spanIdx++;
|
|
}
|
|
}
|
|
});
|
|
return map;
|
|
}
|
|
spanOf(line, column) {
|
|
const emittedLine = this._lines[line];
|
|
if (emittedLine) {
|
|
let columnsLeft = column - _createIndent(emittedLine.indent).length;
|
|
for (let partIndex = 0; partIndex < emittedLine.parts.length; partIndex++) {
|
|
const part = emittedLine.parts[partIndex];
|
|
if (part.length > columnsLeft) {
|
|
return emittedLine.srcSpans[partIndex];
|
|
}
|
|
columnsLeft -= part.length;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* @internal strip this from published d.ts files due to
|
|
* https://github.com/microsoft/TypeScript/issues/36216
|
|
*/
|
|
get sourceLines() {
|
|
if (this._lines.length && this._lines[this._lines.length - 1].parts.length === 0) {
|
|
return this._lines.slice(0, -1);
|
|
}
|
|
return this._lines;
|
|
}
|
|
}
|
|
class AbstractEmitterVisitor {
|
|
_escapeDollarInStrings;
|
|
lastIfCondition = null;
|
|
constructor(_escapeDollarInStrings) {
|
|
this._escapeDollarInStrings = _escapeDollarInStrings;
|
|
}
|
|
printLeadingComments(stmt, ctx) {
|
|
if (stmt.leadingComments === undefined) {
|
|
return;
|
|
}
|
|
for (const comment of stmt.leadingComments) {
|
|
if (comment instanceof JSDocComment) {
|
|
ctx.print(stmt, `/*${comment.toString()}*/`, comment.trailingNewline);
|
|
}
|
|
else {
|
|
if (comment.multiline) {
|
|
ctx.print(stmt, `/* ${comment.text} */`, comment.trailingNewline);
|
|
}
|
|
else {
|
|
comment.text.split('\n').forEach((line) => {
|
|
ctx.println(stmt, `// ${line}`);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
visitExpressionStmt(stmt, ctx) {
|
|
this.printLeadingComments(stmt, ctx);
|
|
stmt.expr.visitExpression(this, ctx);
|
|
ctx.println(stmt, ';');
|
|
return null;
|
|
}
|
|
visitReturnStmt(stmt, ctx) {
|
|
this.printLeadingComments(stmt, ctx);
|
|
ctx.print(stmt, `return `);
|
|
stmt.value.visitExpression(this, ctx);
|
|
ctx.println(stmt, ';');
|
|
return null;
|
|
}
|
|
visitIfStmt(stmt, ctx) {
|
|
this.printLeadingComments(stmt, ctx);
|
|
ctx.print(stmt, `if (`);
|
|
this.lastIfCondition = stmt.condition; // We can skip redundant parentheses for the condition.
|
|
stmt.condition.visitExpression(this, ctx);
|
|
this.lastIfCondition = null;
|
|
ctx.print(stmt, `) {`);
|
|
const hasElseCase = stmt.falseCase != null && stmt.falseCase.length > 0;
|
|
if (stmt.trueCase.length <= 1 && !hasElseCase) {
|
|
ctx.print(stmt, ` `);
|
|
this.visitAllStatements(stmt.trueCase, ctx);
|
|
ctx.removeEmptyLastLine();
|
|
ctx.print(stmt, ` `);
|
|
}
|
|
else {
|
|
ctx.println();
|
|
ctx.incIndent();
|
|
this.visitAllStatements(stmt.trueCase, ctx);
|
|
ctx.decIndent();
|
|
if (hasElseCase) {
|
|
ctx.println(stmt, `} else {`);
|
|
ctx.incIndent();
|
|
this.visitAllStatements(stmt.falseCase, ctx);
|
|
ctx.decIndent();
|
|
}
|
|
}
|
|
ctx.println(stmt, `}`);
|
|
return null;
|
|
}
|
|
visitInvokeFunctionExpr(expr, ctx) {
|
|
const shouldParenthesize = expr.fn instanceof ArrowFunctionExpr;
|
|
if (shouldParenthesize) {
|
|
ctx.print(expr.fn, '(');
|
|
}
|
|
expr.fn.visitExpression(this, ctx);
|
|
if (shouldParenthesize) {
|
|
ctx.print(expr.fn, ')');
|
|
}
|
|
ctx.print(expr, `(`);
|
|
this.visitAllExpressions(expr.args, ctx, ',');
|
|
ctx.print(expr, `)`);
|
|
return null;
|
|
}
|
|
visitTaggedTemplateLiteralExpr(expr, ctx) {
|
|
expr.tag.visitExpression(this, ctx);
|
|
expr.template.visitExpression(this, ctx);
|
|
return null;
|
|
}
|
|
visitTemplateLiteralExpr(expr, ctx) {
|
|
ctx.print(expr, '`');
|
|
for (let i = 0; i < expr.elements.length; i++) {
|
|
expr.elements[i].visitExpression(this, ctx);
|
|
const expression = i < expr.expressions.length ? expr.expressions[i] : null;
|
|
if (expression !== null) {
|
|
ctx.print(expression, '${');
|
|
expression.visitExpression(this, ctx);
|
|
ctx.print(expression, '}');
|
|
}
|
|
}
|
|
ctx.print(expr, '`');
|
|
}
|
|
visitTemplateLiteralElementExpr(expr, ctx) {
|
|
ctx.print(expr, expr.rawText);
|
|
}
|
|
visitWrappedNodeExpr(ast, ctx) {
|
|
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
|
|
}
|
|
visitTypeofExpr(expr, ctx) {
|
|
ctx.print(expr, 'typeof ');
|
|
expr.expr.visitExpression(this, ctx);
|
|
}
|
|
visitVoidExpr(expr, ctx) {
|
|
ctx.print(expr, 'void ');
|
|
expr.expr.visitExpression(this, ctx);
|
|
}
|
|
visitReadVarExpr(ast, ctx) {
|
|
ctx.print(ast, ast.name);
|
|
return null;
|
|
}
|
|
visitInstantiateExpr(ast, ctx) {
|
|
ctx.print(ast, `new `);
|
|
ast.classExpr.visitExpression(this, ctx);
|
|
ctx.print(ast, `(`);
|
|
this.visitAllExpressions(ast.args, ctx, ',');
|
|
ctx.print(ast, `)`);
|
|
return null;
|
|
}
|
|
visitLiteralExpr(ast, ctx) {
|
|
const value = ast.value;
|
|
if (typeof value === 'string') {
|
|
ctx.print(ast, escapeIdentifier(value, this._escapeDollarInStrings));
|
|
}
|
|
else {
|
|
ctx.print(ast, `${value}`);
|
|
}
|
|
return null;
|
|
}
|
|
visitLocalizedString(ast, ctx) {
|
|
const head = ast.serializeI18nHead();
|
|
ctx.print(ast, '$localize `' + head.raw);
|
|
for (let i = 1; i < ast.messageParts.length; i++) {
|
|
ctx.print(ast, '${');
|
|
ast.expressions[i - 1].visitExpression(this, ctx);
|
|
ctx.print(ast, `}${ast.serializeI18nTemplatePart(i).raw}`);
|
|
}
|
|
ctx.print(ast, '`');
|
|
return null;
|
|
}
|
|
visitConditionalExpr(ast, ctx) {
|
|
ctx.print(ast, `(`);
|
|
ast.condition.visitExpression(this, ctx);
|
|
ctx.print(ast, '? ');
|
|
ast.trueCase.visitExpression(this, ctx);
|
|
ctx.print(ast, ': ');
|
|
ast.falseCase.visitExpression(this, ctx);
|
|
ctx.print(ast, `)`);
|
|
return null;
|
|
}
|
|
visitDynamicImportExpr(ast, ctx) {
|
|
ctx.print(ast, `import(${ast.url})`);
|
|
}
|
|
visitNotExpr(ast, ctx) {
|
|
ctx.print(ast, '!');
|
|
ast.condition.visitExpression(this, ctx);
|
|
return null;
|
|
}
|
|
visitUnaryOperatorExpr(ast, ctx) {
|
|
let opStr;
|
|
switch (ast.operator) {
|
|
case UnaryOperator.Plus:
|
|
opStr = '+';
|
|
break;
|
|
case UnaryOperator.Minus:
|
|
opStr = '-';
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown operator ${ast.operator}`);
|
|
}
|
|
const parens = ast !== this.lastIfCondition;
|
|
if (parens)
|
|
ctx.print(ast, `(`);
|
|
ctx.print(ast, opStr);
|
|
ast.expr.visitExpression(this, ctx);
|
|
if (parens)
|
|
ctx.print(ast, `)`);
|
|
return null;
|
|
}
|
|
visitBinaryOperatorExpr(ast, ctx) {
|
|
const operator = BINARY_OPERATORS$1.get(ast.operator);
|
|
if (!operator) {
|
|
throw new Error(`Unknown operator ${ast.operator}`);
|
|
}
|
|
const parens = ast !== this.lastIfCondition;
|
|
if (parens)
|
|
ctx.print(ast, `(`);
|
|
ast.lhs.visitExpression(this, ctx);
|
|
ctx.print(ast, ` ${operator} `);
|
|
ast.rhs.visitExpression(this, ctx);
|
|
if (parens)
|
|
ctx.print(ast, `)`);
|
|
return null;
|
|
}
|
|
visitReadPropExpr(ast, ctx) {
|
|
ast.receiver.visitExpression(this, ctx);
|
|
ctx.print(ast, `.`);
|
|
ctx.print(ast, ast.name);
|
|
return null;
|
|
}
|
|
visitReadKeyExpr(ast, ctx) {
|
|
ast.receiver.visitExpression(this, ctx);
|
|
ctx.print(ast, `[`);
|
|
ast.index.visitExpression(this, ctx);
|
|
ctx.print(ast, `]`);
|
|
return null;
|
|
}
|
|
visitLiteralArrayExpr(ast, ctx) {
|
|
ctx.print(ast, `[`);
|
|
this.visitAllExpressions(ast.entries, ctx, ',');
|
|
ctx.print(ast, `]`);
|
|
return null;
|
|
}
|
|
visitLiteralMapExpr(ast, ctx) {
|
|
ctx.print(ast, `{`);
|
|
this.visitAllObjects((entry) => {
|
|
ctx.print(ast, `${escapeIdentifier(entry.key, this._escapeDollarInStrings, entry.quoted)}:`);
|
|
entry.value.visitExpression(this, ctx);
|
|
}, ast.entries, ctx, ',');
|
|
ctx.print(ast, `}`);
|
|
return null;
|
|
}
|
|
visitCommaExpr(ast, ctx) {
|
|
ctx.print(ast, '(');
|
|
this.visitAllExpressions(ast.parts, ctx, ',');
|
|
ctx.print(ast, ')');
|
|
return null;
|
|
}
|
|
visitParenthesizedExpr(ast, ctx) {
|
|
// We parenthesize everything regardless of an explicit ParenthesizedExpr, so we can just visit
|
|
// the inner expression.
|
|
// TODO: Do we *need* to parenthesize everything?
|
|
ast.expr.visitExpression(this, ctx);
|
|
}
|
|
visitAllExpressions(expressions, ctx, separator) {
|
|
this.visitAllObjects((expr) => expr.visitExpression(this, ctx), expressions, ctx, separator);
|
|
}
|
|
visitAllObjects(handler, expressions, ctx, separator) {
|
|
let incrementedIndent = false;
|
|
for (let i = 0; i < expressions.length; i++) {
|
|
if (i > 0) {
|
|
if (ctx.lineLength() > 80) {
|
|
ctx.print(null, separator, true);
|
|
if (!incrementedIndent) {
|
|
// continuation are marked with double indent.
|
|
ctx.incIndent();
|
|
ctx.incIndent();
|
|
incrementedIndent = true;
|
|
}
|
|
}
|
|
else {
|
|
ctx.print(null, separator, false);
|
|
}
|
|
}
|
|
handler(expressions[i]);
|
|
}
|
|
if (incrementedIndent) {
|
|
// continuation are marked with double indent.
|
|
ctx.decIndent();
|
|
ctx.decIndent();
|
|
}
|
|
}
|
|
visitAllStatements(statements, ctx) {
|
|
statements.forEach((stmt) => stmt.visitStatement(this, ctx));
|
|
}
|
|
}
|
|
function escapeIdentifier(input, escapeDollar, alwaysQuote = true) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
const body = input.replace(_SINGLE_QUOTE_ESCAPE_STRING_RE, (...match) => {
|
|
if (match[0] == '$') {
|
|
return escapeDollar ? '\\$' : '$';
|
|
}
|
|
else if (match[0] == '\n') {
|
|
return '\\n';
|
|
}
|
|
else if (match[0] == '\r') {
|
|
return '\\r';
|
|
}
|
|
else {
|
|
return `\\${match[0]}`;
|
|
}
|
|
});
|
|
const requiresQuotes = alwaysQuote || !_LEGAL_IDENTIFIER_RE.test(body);
|
|
return requiresQuotes ? `'${body}'` : body;
|
|
}
|
|
function _createIndent(count) {
|
|
let res = '';
|
|
for (let i = 0; i < count; i++) {
|
|
res += _INDENT_WITH;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function typeWithParameters(type, numParams) {
|
|
if (numParams === 0) {
|
|
return expressionType(type);
|
|
}
|
|
const params = [];
|
|
for (let i = 0; i < numParams; i++) {
|
|
params.push(DYNAMIC_TYPE);
|
|
}
|
|
return expressionType(type, undefined, params);
|
|
}
|
|
function getSafePropertyAccessString(accessor, name) {
|
|
const escapedName = escapeIdentifier(name, false, false);
|
|
return escapedName !== name ? `${accessor}[${escapedName}]` : `${accessor}.${name}`;
|
|
}
|
|
function jitOnlyGuardedExpression(expr) {
|
|
return guardedExpression('ngJitMode', expr);
|
|
}
|
|
function devOnlyGuardedExpression(expr) {
|
|
return guardedExpression('ngDevMode', expr);
|
|
}
|
|
function guardedExpression(guard, expr) {
|
|
const guardExpr = new ExternalExpr({ name: guard, moduleName: null });
|
|
const guardNotDefined = new BinaryOperatorExpr(BinaryOperator.Identical, new TypeofExpr(guardExpr), literal('undefined'));
|
|
const guardUndefinedOrTrue = new BinaryOperatorExpr(BinaryOperator.Or, guardNotDefined, guardExpr,
|
|
/* type */ undefined,
|
|
/* sourceSpan */ undefined);
|
|
return new BinaryOperatorExpr(BinaryOperator.And, guardUndefinedOrTrue, expr);
|
|
}
|
|
function wrapReference(value) {
|
|
const wrapped = new WrappedNodeExpr(value);
|
|
return { value: wrapped, type: wrapped };
|
|
}
|
|
function refsToArray(refs, shouldForwardDeclare) {
|
|
const values = literalArr(refs.map((ref) => ref.value));
|
|
return shouldForwardDeclare ? arrowFn([], values) : values;
|
|
}
|
|
function createMayBeForwardRefExpression(expression, forwardRef) {
|
|
return { expression, forwardRef };
|
|
}
|
|
/**
|
|
* Convert a `MaybeForwardRefExpression` to an `Expression`, possibly wrapping its expression in a
|
|
* `forwardRef()` call.
|
|
*
|
|
* If `MaybeForwardRefExpression.forwardRef` is `ForwardRefHandling.Unwrapped` then the expression
|
|
* was originally wrapped in a `forwardRef()` call to prevent the value from being eagerly evaluated
|
|
* in the code.
|
|
*
|
|
* See `packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts` and
|
|
* `packages/compiler/src/jit_compiler_facade.ts` for more information.
|
|
*/
|
|
function convertFromMaybeForwardRefExpression({ expression, forwardRef, }) {
|
|
switch (forwardRef) {
|
|
case 0 /* ForwardRefHandling.None */:
|
|
case 1 /* ForwardRefHandling.Wrapped */:
|
|
return expression;
|
|
case 2 /* ForwardRefHandling.Unwrapped */:
|
|
return generateForwardRef(expression);
|
|
}
|
|
}
|
|
/**
|
|
* Generate an expression that has the given `expr` wrapped in the following form:
|
|
*
|
|
* ```ts
|
|
* forwardRef(() => expr)
|
|
* ```
|
|
*/
|
|
function generateForwardRef(expr) {
|
|
return importExpr(Identifiers.forwardRef).callFn([arrowFn([], expr)]);
|
|
}
|
|
|
|
var R3FactoryDelegateType;
|
|
(function (R3FactoryDelegateType) {
|
|
R3FactoryDelegateType[R3FactoryDelegateType["Class"] = 0] = "Class";
|
|
R3FactoryDelegateType[R3FactoryDelegateType["Function"] = 1] = "Function";
|
|
})(R3FactoryDelegateType || (R3FactoryDelegateType = {}));
|
|
/**
|
|
* Construct a factory function expression for the given `R3FactoryMetadata`.
|
|
*/
|
|
function compileFactoryFunction(meta) {
|
|
const t = variable('__ngFactoryType__');
|
|
let baseFactoryVar = null;
|
|
// The type to instantiate via constructor invocation. If there is no delegated factory, meaning
|
|
// this type is always created by constructor invocation, then this is the type-to-create
|
|
// parameter provided by the user (t) if specified, or the current type if not. If there is a
|
|
// delegated factory (which is used to create the current type) then this is only the type-to-
|
|
// create parameter (t).
|
|
const typeForCtor = !isDelegatedFactoryMetadata(meta)
|
|
? new BinaryOperatorExpr(BinaryOperator.Or, t, meta.type.value)
|
|
: t;
|
|
let ctorExpr = null;
|
|
if (meta.deps !== null) {
|
|
// There is a constructor (either explicitly or implicitly defined).
|
|
if (meta.deps !== 'invalid') {
|
|
ctorExpr = new InstantiateExpr(typeForCtor, injectDependencies(meta.deps, meta.target));
|
|
}
|
|
}
|
|
else {
|
|
// There is no constructor, use the base class' factory to construct typeForCtor.
|
|
baseFactoryVar = variable(`ɵ${meta.name}_BaseFactory`);
|
|
ctorExpr = baseFactoryVar.callFn([typeForCtor]);
|
|
}
|
|
const body = [];
|
|
let retExpr = null;
|
|
function makeConditionalFactory(nonCtorExpr) {
|
|
const r = variable('__ngConditionalFactory__');
|
|
body.push(new DeclareVarStmt(r.name, NULL_EXPR, INFERRED_TYPE));
|
|
const ctorStmt = ctorExpr !== null
|
|
? r.set(ctorExpr).toStmt()
|
|
: importExpr(Identifiers.invalidFactory).callFn([]).toStmt();
|
|
body.push(ifStmt(t, [ctorStmt], [r.set(nonCtorExpr).toStmt()]));
|
|
return r;
|
|
}
|
|
if (isDelegatedFactoryMetadata(meta)) {
|
|
// This type is created with a delegated factory. If a type parameter is not specified, call
|
|
// the factory instead.
|
|
const delegateArgs = injectDependencies(meta.delegateDeps, meta.target);
|
|
// Either call `new delegate(...)` or `delegate(...)` depending on meta.delegateType.
|
|
const factoryExpr = new (meta.delegateType === R3FactoryDelegateType.Class ? InstantiateExpr : InvokeFunctionExpr)(meta.delegate, delegateArgs);
|
|
retExpr = makeConditionalFactory(factoryExpr);
|
|
}
|
|
else if (isExpressionFactoryMetadata(meta)) {
|
|
// TODO(alxhub): decide whether to lower the value here or in the caller
|
|
retExpr = makeConditionalFactory(meta.expression);
|
|
}
|
|
else {
|
|
retExpr = ctorExpr;
|
|
}
|
|
if (retExpr === null) {
|
|
// The expression cannot be formed so render an `ɵɵinvalidFactory()` call.
|
|
body.push(importExpr(Identifiers.invalidFactory).callFn([]).toStmt());
|
|
}
|
|
else if (baseFactoryVar !== null) {
|
|
// This factory uses a base factory, so call `ɵɵgetInheritedFactory()` to compute it.
|
|
const getInheritedFactoryCall = importExpr(Identifiers.getInheritedFactory).callFn([meta.type.value]);
|
|
// Memoize the base factoryFn: `baseFactory || (baseFactory = ɵɵgetInheritedFactory(...))`
|
|
const baseFactory = new BinaryOperatorExpr(BinaryOperator.Or, baseFactoryVar, baseFactoryVar.set(getInheritedFactoryCall));
|
|
body.push(new ReturnStatement(baseFactory.callFn([typeForCtor])));
|
|
}
|
|
else {
|
|
// This is straightforward factory, just return it.
|
|
body.push(new ReturnStatement(retExpr));
|
|
}
|
|
let factoryFn = fn([new FnParam(t.name, DYNAMIC_TYPE)], body, INFERRED_TYPE, undefined, `${meta.name}_Factory`);
|
|
if (baseFactoryVar !== null) {
|
|
// There is a base factory variable so wrap its declaration along with the factory function into
|
|
// an IIFE.
|
|
factoryFn = arrowFn([], [new DeclareVarStmt(baseFactoryVar.name), new ReturnStatement(factoryFn)])
|
|
.callFn([], /* sourceSpan */ undefined, /* pure */ true);
|
|
}
|
|
return {
|
|
expression: factoryFn,
|
|
statements: [],
|
|
type: createFactoryType(meta),
|
|
};
|
|
}
|
|
function createFactoryType(meta) {
|
|
const ctorDepsType = meta.deps !== null && meta.deps !== 'invalid' ? createCtorDepsType(meta.deps) : NONE_TYPE;
|
|
return expressionType(importExpr(Identifiers.FactoryDeclaration, [
|
|
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
|
ctorDepsType,
|
|
]));
|
|
}
|
|
function injectDependencies(deps, target) {
|
|
return deps.map((dep, index) => compileInjectDependency(dep, target, index));
|
|
}
|
|
function compileInjectDependency(dep, target, index) {
|
|
// Interpret the dependency according to its resolved type.
|
|
if (dep.token === null) {
|
|
return importExpr(Identifiers.invalidFactoryDep).callFn([literal(index)]);
|
|
}
|
|
else if (dep.attributeNameType === null) {
|
|
// Build up the injection flags according to the metadata.
|
|
const flags = 0 /* InjectFlags.Default */ |
|
|
(dep.self ? 2 /* InjectFlags.Self */ : 0) |
|
|
(dep.skipSelf ? 4 /* InjectFlags.SkipSelf */ : 0) |
|
|
(dep.host ? 1 /* InjectFlags.Host */ : 0) |
|
|
(dep.optional ? 8 /* InjectFlags.Optional */ : 0) |
|
|
(target === FactoryTarget.Pipe ? 16 /* InjectFlags.ForPipe */ : 0);
|
|
// If this dependency is optional or otherwise has non-default flags, then additional
|
|
// parameters describing how to inject the dependency must be passed to the inject function
|
|
// that's being used.
|
|
let flagsParam = flags !== 0 /* InjectFlags.Default */ || dep.optional ? literal(flags) : null;
|
|
// Build up the arguments to the injectFn call.
|
|
const injectArgs = [dep.token];
|
|
if (flagsParam) {
|
|
injectArgs.push(flagsParam);
|
|
}
|
|
const injectFn = getInjectFn(target);
|
|
return importExpr(injectFn).callFn(injectArgs);
|
|
}
|
|
else {
|
|
// The `dep.attributeTypeName` value is defined, which indicates that this is an `@Attribute()`
|
|
// type dependency. For the generated JS we still want to use the `dep.token` value in case the
|
|
// name given for the attribute is not a string literal. For example given `@Attribute(foo())`,
|
|
// we want to generate `ɵɵinjectAttribute(foo())`.
|
|
//
|
|
// The `dep.attributeTypeName` is only actually used (in `createCtorDepType()`) to generate
|
|
// typings.
|
|
return importExpr(Identifiers.injectAttribute).callFn([dep.token]);
|
|
}
|
|
}
|
|
function createCtorDepsType(deps) {
|
|
let hasTypes = false;
|
|
const attributeTypes = deps.map((dep) => {
|
|
const type = createCtorDepType(dep);
|
|
if (type !== null) {
|
|
hasTypes = true;
|
|
return type;
|
|
}
|
|
else {
|
|
return literal(null);
|
|
}
|
|
});
|
|
if (hasTypes) {
|
|
return expressionType(literalArr(attributeTypes));
|
|
}
|
|
else {
|
|
return NONE_TYPE;
|
|
}
|
|
}
|
|
function createCtorDepType(dep) {
|
|
const entries = [];
|
|
if (dep.attributeNameType !== null) {
|
|
entries.push({ key: 'attribute', value: dep.attributeNameType, quoted: false });
|
|
}
|
|
if (dep.optional) {
|
|
entries.push({ key: 'optional', value: literal(true), quoted: false });
|
|
}
|
|
if (dep.host) {
|
|
entries.push({ key: 'host', value: literal(true), quoted: false });
|
|
}
|
|
if (dep.self) {
|
|
entries.push({ key: 'self', value: literal(true), quoted: false });
|
|
}
|
|
if (dep.skipSelf) {
|
|
entries.push({ key: 'skipSelf', value: literal(true), quoted: false });
|
|
}
|
|
return entries.length > 0 ? literalMap(entries) : null;
|
|
}
|
|
function isDelegatedFactoryMetadata(meta) {
|
|
return meta.delegateType !== undefined;
|
|
}
|
|
function isExpressionFactoryMetadata(meta) {
|
|
return meta.expression !== undefined;
|
|
}
|
|
function getInjectFn(target) {
|
|
switch (target) {
|
|
case FactoryTarget.Component:
|
|
case FactoryTarget.Directive:
|
|
case FactoryTarget.Pipe:
|
|
return Identifiers.directiveInject;
|
|
case FactoryTarget.NgModule:
|
|
case FactoryTarget.Injectable:
|
|
default:
|
|
return Identifiers.inject;
|
|
}
|
|
}
|
|
|
|
class ParseSpan {
|
|
start;
|
|
end;
|
|
constructor(start, end) {
|
|
this.start = start;
|
|
this.end = end;
|
|
}
|
|
toAbsolute(absoluteOffset) {
|
|
return new AbsoluteSourceSpan(absoluteOffset + this.start, absoluteOffset + this.end);
|
|
}
|
|
}
|
|
class AST {
|
|
span;
|
|
sourceSpan;
|
|
constructor(span,
|
|
/**
|
|
* Absolute location of the expression AST in a source code file.
|
|
*/
|
|
sourceSpan) {
|
|
this.span = span;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
toString() {
|
|
return 'AST';
|
|
}
|
|
}
|
|
class ASTWithName extends AST {
|
|
nameSpan;
|
|
constructor(span, sourceSpan, nameSpan) {
|
|
super(span, sourceSpan);
|
|
this.nameSpan = nameSpan;
|
|
}
|
|
}
|
|
let EmptyExpr$1 = class EmptyExpr extends AST {
|
|
visit(visitor, context = null) {
|
|
// do nothing
|
|
}
|
|
};
|
|
class ImplicitReceiver extends AST {
|
|
visit(visitor, context = null) {
|
|
return visitor.visitImplicitReceiver(this, context);
|
|
}
|
|
}
|
|
/**
|
|
* Receiver when something is accessed through `this` (e.g. `this.foo`). Note that this class
|
|
* inherits from `ImplicitReceiver`, because accessing something through `this` is treated the
|
|
* same as accessing it implicitly inside of an Angular template (e.g. `[attr.title]="this.title"`
|
|
* is the same as `[attr.title]="title"`.). Inheriting allows for the `this` accesses to be treated
|
|
* the same as implicit ones, except for a couple of exceptions like `$event` and `$any`.
|
|
* TODO: we should find a way for this class not to extend from `ImplicitReceiver` in the future.
|
|
*/
|
|
class ThisReceiver extends ImplicitReceiver {
|
|
visit(visitor, context = null) {
|
|
return visitor.visitThisReceiver?.(this, context);
|
|
}
|
|
}
|
|
/**
|
|
* Multiple expressions separated by a semicolon.
|
|
*/
|
|
class Chain extends AST {
|
|
expressions;
|
|
constructor(span, sourceSpan, expressions) {
|
|
super(span, sourceSpan);
|
|
this.expressions = expressions;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitChain(this, context);
|
|
}
|
|
}
|
|
class Conditional extends AST {
|
|
condition;
|
|
trueExp;
|
|
falseExp;
|
|
constructor(span, sourceSpan, condition, trueExp, falseExp) {
|
|
super(span, sourceSpan);
|
|
this.condition = condition;
|
|
this.trueExp = trueExp;
|
|
this.falseExp = falseExp;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitConditional(this, context);
|
|
}
|
|
}
|
|
class PropertyRead extends ASTWithName {
|
|
receiver;
|
|
name;
|
|
constructor(span, sourceSpan, nameSpan, receiver, name) {
|
|
super(span, sourceSpan, nameSpan);
|
|
this.receiver = receiver;
|
|
this.name = name;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitPropertyRead(this, context);
|
|
}
|
|
}
|
|
class SafePropertyRead extends ASTWithName {
|
|
receiver;
|
|
name;
|
|
constructor(span, sourceSpan, nameSpan, receiver, name) {
|
|
super(span, sourceSpan, nameSpan);
|
|
this.receiver = receiver;
|
|
this.name = name;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitSafePropertyRead(this, context);
|
|
}
|
|
}
|
|
class KeyedRead extends AST {
|
|
receiver;
|
|
key;
|
|
constructor(span, sourceSpan, receiver, key) {
|
|
super(span, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.key = key;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitKeyedRead(this, context);
|
|
}
|
|
}
|
|
class SafeKeyedRead extends AST {
|
|
receiver;
|
|
key;
|
|
constructor(span, sourceSpan, receiver, key) {
|
|
super(span, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.key = key;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitSafeKeyedRead(this, context);
|
|
}
|
|
}
|
|
/** Possible types for a pipe. */
|
|
var BindingPipeType;
|
|
(function (BindingPipeType) {
|
|
/**
|
|
* Pipe is being referenced by its name, for example:
|
|
* `@Pipe({name: 'foo'}) class FooPipe` and `{{123 | foo}}`.
|
|
*/
|
|
BindingPipeType[BindingPipeType["ReferencedByName"] = 0] = "ReferencedByName";
|
|
/**
|
|
* Pipe is being referenced by its class name, for example:
|
|
* `@Pipe() class FooPipe` and `{{123 | FooPipe}}`.
|
|
*/
|
|
BindingPipeType[BindingPipeType["ReferencedDirectly"] = 1] = "ReferencedDirectly";
|
|
})(BindingPipeType || (BindingPipeType = {}));
|
|
class BindingPipe extends ASTWithName {
|
|
exp;
|
|
name;
|
|
args;
|
|
type;
|
|
constructor(span, sourceSpan, exp, name, args, type, nameSpan) {
|
|
super(span, sourceSpan, nameSpan);
|
|
this.exp = exp;
|
|
this.name = name;
|
|
this.args = args;
|
|
this.type = type;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitPipe(this, context);
|
|
}
|
|
}
|
|
class LiteralPrimitive extends AST {
|
|
value;
|
|
constructor(span, sourceSpan, value) {
|
|
super(span, sourceSpan);
|
|
this.value = value;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitLiteralPrimitive(this, context);
|
|
}
|
|
}
|
|
class LiteralArray extends AST {
|
|
expressions;
|
|
constructor(span, sourceSpan, expressions) {
|
|
super(span, sourceSpan);
|
|
this.expressions = expressions;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitLiteralArray(this, context);
|
|
}
|
|
}
|
|
class LiteralMap extends AST {
|
|
keys;
|
|
values;
|
|
constructor(span, sourceSpan, keys, values) {
|
|
super(span, sourceSpan);
|
|
this.keys = keys;
|
|
this.values = values;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitLiteralMap(this, context);
|
|
}
|
|
}
|
|
let Interpolation$1 = class Interpolation extends AST {
|
|
strings;
|
|
expressions;
|
|
constructor(span, sourceSpan, strings, expressions) {
|
|
super(span, sourceSpan);
|
|
this.strings = strings;
|
|
this.expressions = expressions;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitInterpolation(this, context);
|
|
}
|
|
};
|
|
class Binary extends AST {
|
|
operation;
|
|
left;
|
|
right;
|
|
constructor(span, sourceSpan, operation, left, right) {
|
|
super(span, sourceSpan);
|
|
this.operation = operation;
|
|
this.left = left;
|
|
this.right = right;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitBinary(this, context);
|
|
}
|
|
static isAssignmentOperation(op) {
|
|
return (op === '=' ||
|
|
op === '+=' ||
|
|
op === '-=' ||
|
|
op === '*=' ||
|
|
op === '/=' ||
|
|
op === '%=' ||
|
|
op === '**=' ||
|
|
op === '&&=' ||
|
|
op === '||=' ||
|
|
op === '??=');
|
|
}
|
|
}
|
|
/**
|
|
* For backwards compatibility reasons, `Unary` inherits from `Binary` and mimics the binary AST
|
|
* node that was originally used. This inheritance relation can be deleted in some future major,
|
|
* after consumers have been given a chance to fully support Unary.
|
|
*/
|
|
class Unary extends Binary {
|
|
operator;
|
|
expr;
|
|
// Redeclare the properties that are inherited from `Binary` as `never`, as consumers should not
|
|
// depend on these fields when operating on `Unary`.
|
|
left = null;
|
|
right = null;
|
|
operation = null;
|
|
/**
|
|
* Creates a unary minus expression "-x", represented as `Binary` using "0 - x".
|
|
*/
|
|
static createMinus(span, sourceSpan, expr) {
|
|
return new Unary(span, sourceSpan, '-', expr, '-', new LiteralPrimitive(span, sourceSpan, 0), expr);
|
|
}
|
|
/**
|
|
* Creates a unary plus expression "+x", represented as `Binary` using "x - 0".
|
|
*/
|
|
static createPlus(span, sourceSpan, expr) {
|
|
return new Unary(span, sourceSpan, '+', expr, '-', expr, new LiteralPrimitive(span, sourceSpan, 0));
|
|
}
|
|
/**
|
|
* During the deprecation period this constructor is private, to avoid consumers from creating
|
|
* a `Unary` with the fallback properties for `Binary`.
|
|
*/
|
|
constructor(span, sourceSpan, operator, expr, binaryOp, binaryLeft, binaryRight) {
|
|
super(span, sourceSpan, binaryOp, binaryLeft, binaryRight);
|
|
this.operator = operator;
|
|
this.expr = expr;
|
|
}
|
|
visit(visitor, context = null) {
|
|
if (visitor.visitUnary !== undefined) {
|
|
return visitor.visitUnary(this, context);
|
|
}
|
|
return visitor.visitBinary(this, context);
|
|
}
|
|
}
|
|
class PrefixNot extends AST {
|
|
expression;
|
|
constructor(span, sourceSpan, expression) {
|
|
super(span, sourceSpan);
|
|
this.expression = expression;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitPrefixNot(this, context);
|
|
}
|
|
}
|
|
class TypeofExpression extends AST {
|
|
expression;
|
|
constructor(span, sourceSpan, expression) {
|
|
super(span, sourceSpan);
|
|
this.expression = expression;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitTypeofExpression(this, context);
|
|
}
|
|
}
|
|
class VoidExpression extends AST {
|
|
expression;
|
|
constructor(span, sourceSpan, expression) {
|
|
super(span, sourceSpan);
|
|
this.expression = expression;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitVoidExpression(this, context);
|
|
}
|
|
}
|
|
class NonNullAssert extends AST {
|
|
expression;
|
|
constructor(span, sourceSpan, expression) {
|
|
super(span, sourceSpan);
|
|
this.expression = expression;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitNonNullAssert(this, context);
|
|
}
|
|
}
|
|
class Call extends AST {
|
|
receiver;
|
|
args;
|
|
argumentSpan;
|
|
constructor(span, sourceSpan, receiver, args, argumentSpan) {
|
|
super(span, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.args = args;
|
|
this.argumentSpan = argumentSpan;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitCall(this, context);
|
|
}
|
|
}
|
|
class SafeCall extends AST {
|
|
receiver;
|
|
args;
|
|
argumentSpan;
|
|
constructor(span, sourceSpan, receiver, args, argumentSpan) {
|
|
super(span, sourceSpan);
|
|
this.receiver = receiver;
|
|
this.args = args;
|
|
this.argumentSpan = argumentSpan;
|
|
}
|
|
visit(visitor, context = null) {
|
|
return visitor.visitSafeCall(this, context);
|
|
}
|
|
}
|
|
class TaggedTemplateLiteral extends AST {
|
|
tag;
|
|
template;
|
|
constructor(span, sourceSpan, tag, template) {
|
|
super(span, sourceSpan);
|
|
this.tag = tag;
|
|
this.template = template;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitTaggedTemplateLiteral(this, context);
|
|
}
|
|
}
|
|
class TemplateLiteral extends AST {
|
|
elements;
|
|
expressions;
|
|
constructor(span, sourceSpan, elements, expressions) {
|
|
super(span, sourceSpan);
|
|
this.elements = elements;
|
|
this.expressions = expressions;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitTemplateLiteral(this, context);
|
|
}
|
|
}
|
|
class TemplateLiteralElement extends AST {
|
|
text;
|
|
constructor(span, sourceSpan, text) {
|
|
super(span, sourceSpan);
|
|
this.text = text;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitTemplateLiteralElement(this, context);
|
|
}
|
|
}
|
|
class ParenthesizedExpression extends AST {
|
|
expression;
|
|
constructor(span, sourceSpan, expression) {
|
|
super(span, sourceSpan);
|
|
this.expression = expression;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitParenthesizedExpression(this, context);
|
|
}
|
|
}
|
|
/**
|
|
* Records the absolute position of a text span in a source file, where `start` and `end` are the
|
|
* starting and ending byte offsets, respectively, of the text span in a source file.
|
|
*/
|
|
class AbsoluteSourceSpan {
|
|
start;
|
|
end;
|
|
constructor(start, end) {
|
|
this.start = start;
|
|
this.end = end;
|
|
}
|
|
}
|
|
class ASTWithSource extends AST {
|
|
ast;
|
|
source;
|
|
location;
|
|
errors;
|
|
constructor(ast, source, location, absoluteOffset, errors) {
|
|
super(new ParseSpan(0, source === null ? 0 : source.length), new AbsoluteSourceSpan(absoluteOffset, source === null ? absoluteOffset : absoluteOffset + source.length));
|
|
this.ast = ast;
|
|
this.source = source;
|
|
this.location = location;
|
|
this.errors = errors;
|
|
}
|
|
visit(visitor, context = null) {
|
|
if (visitor.visitASTWithSource) {
|
|
return visitor.visitASTWithSource(this, context);
|
|
}
|
|
return this.ast.visit(visitor, context);
|
|
}
|
|
toString() {
|
|
return `${this.source} in ${this.location}`;
|
|
}
|
|
}
|
|
class VariableBinding {
|
|
sourceSpan;
|
|
key;
|
|
value;
|
|
/**
|
|
* @param sourceSpan entire span of the binding.
|
|
* @param key name of the LHS along with its span.
|
|
* @param value optional value for the RHS along with its span.
|
|
*/
|
|
constructor(sourceSpan, key, value) {
|
|
this.sourceSpan = sourceSpan;
|
|
this.key = key;
|
|
this.value = value;
|
|
}
|
|
}
|
|
class ExpressionBinding {
|
|
sourceSpan;
|
|
key;
|
|
value;
|
|
/**
|
|
* @param sourceSpan entire span of the binding.
|
|
* @param key binding name, like ngForOf, ngForTrackBy, ngIf, along with its
|
|
* span. Note that the length of the span may not be the same as
|
|
* `key.source.length`. For example,
|
|
* 1. key.source = ngFor, key.span is for "ngFor"
|
|
* 2. key.source = ngForOf, key.span is for "of"
|
|
* 3. key.source = ngForTrackBy, key.span is for "trackBy"
|
|
* @param value optional expression for the RHS.
|
|
*/
|
|
constructor(sourceSpan, key, value) {
|
|
this.sourceSpan = sourceSpan;
|
|
this.key = key;
|
|
this.value = value;
|
|
}
|
|
}
|
|
class RecursiveAstVisitor {
|
|
visit(ast, context) {
|
|
// The default implementation just visits every node.
|
|
// Classes that extend RecursiveAstVisitor should override this function
|
|
// to selectively visit the specified node.
|
|
ast.visit(this, context);
|
|
}
|
|
visitUnary(ast, context) {
|
|
this.visit(ast.expr, context);
|
|
}
|
|
visitBinary(ast, context) {
|
|
this.visit(ast.left, context);
|
|
this.visit(ast.right, context);
|
|
}
|
|
visitChain(ast, context) {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitConditional(ast, context) {
|
|
this.visit(ast.condition, context);
|
|
this.visit(ast.trueExp, context);
|
|
this.visit(ast.falseExp, context);
|
|
}
|
|
visitPipe(ast, context) {
|
|
this.visit(ast.exp, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitImplicitReceiver(ast, context) { }
|
|
visitThisReceiver(ast, context) { }
|
|
visitInterpolation(ast, context) {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitKeyedRead(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
this.visit(ast.key, context);
|
|
}
|
|
visitLiteralArray(ast, context) {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitLiteralMap(ast, context) {
|
|
this.visitAll(ast.values, context);
|
|
}
|
|
visitLiteralPrimitive(ast, context) { }
|
|
visitPrefixNot(ast, context) {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitTypeofExpression(ast, context) {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitVoidExpression(ast, context) {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitNonNullAssert(ast, context) {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitPropertyRead(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
}
|
|
visitSafePropertyRead(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
}
|
|
visitSafeKeyedRead(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
this.visit(ast.key, context);
|
|
}
|
|
visitCall(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitSafeCall(ast, context) {
|
|
this.visit(ast.receiver, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitTemplateLiteral(ast, context) {
|
|
// Iterate in the declaration order. Note that there will
|
|
// always be one expression less than the number of elements.
|
|
for (let i = 0; i < ast.elements.length; i++) {
|
|
this.visit(ast.elements[i], context);
|
|
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
|
|
if (expression !== null) {
|
|
this.visit(expression, context);
|
|
}
|
|
}
|
|
}
|
|
visitTemplateLiteralElement(ast, context) { }
|
|
visitTaggedTemplateLiteral(ast, context) {
|
|
this.visit(ast.tag, context);
|
|
this.visit(ast.template, context);
|
|
}
|
|
visitParenthesizedExpression(ast, context) {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
// This is not part of the AstVisitor interface, just a helper method
|
|
visitAll(asts, context) {
|
|
for (const ast of asts) {
|
|
this.visit(ast, context);
|
|
}
|
|
}
|
|
}
|
|
// Bindings
|
|
class ParsedProperty {
|
|
name;
|
|
expression;
|
|
type;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
isLiteral;
|
|
isLegacyAnimation;
|
|
isAnimation;
|
|
constructor(name, expression, type, sourceSpan, keySpan, valueSpan) {
|
|
this.name = name;
|
|
this.expression = expression;
|
|
this.type = type;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR;
|
|
this.isLegacyAnimation = this.type === ParsedPropertyType.LEGACY_ANIMATION;
|
|
this.isAnimation = this.type === ParsedPropertyType.ANIMATION;
|
|
}
|
|
}
|
|
var ParsedPropertyType;
|
|
(function (ParsedPropertyType) {
|
|
ParsedPropertyType[ParsedPropertyType["DEFAULT"] = 0] = "DEFAULT";
|
|
ParsedPropertyType[ParsedPropertyType["LITERAL_ATTR"] = 1] = "LITERAL_ATTR";
|
|
ParsedPropertyType[ParsedPropertyType["LEGACY_ANIMATION"] = 2] = "LEGACY_ANIMATION";
|
|
ParsedPropertyType[ParsedPropertyType["TWO_WAY"] = 3] = "TWO_WAY";
|
|
ParsedPropertyType[ParsedPropertyType["ANIMATION"] = 4] = "ANIMATION";
|
|
})(ParsedPropertyType || (ParsedPropertyType = {}));
|
|
var ParsedEventType;
|
|
(function (ParsedEventType) {
|
|
// DOM or Directive event
|
|
ParsedEventType[ParsedEventType["Regular"] = 0] = "Regular";
|
|
// Legacy animation specific event
|
|
ParsedEventType[ParsedEventType["LegacyAnimation"] = 1] = "LegacyAnimation";
|
|
// Event side of a two-way binding (e.g. `[(property)]="expression"`).
|
|
ParsedEventType[ParsedEventType["TwoWay"] = 2] = "TwoWay";
|
|
// Animation specific event
|
|
ParsedEventType[ParsedEventType["Animation"] = 3] = "Animation";
|
|
})(ParsedEventType || (ParsedEventType = {}));
|
|
class ParsedEvent {
|
|
name;
|
|
targetOrPhase;
|
|
type;
|
|
handler;
|
|
sourceSpan;
|
|
handlerSpan;
|
|
keySpan;
|
|
constructor(name, targetOrPhase, type, handler, sourceSpan, handlerSpan, keySpan) {
|
|
this.name = name;
|
|
this.targetOrPhase = targetOrPhase;
|
|
this.type = type;
|
|
this.handler = handler;
|
|
this.sourceSpan = sourceSpan;
|
|
this.handlerSpan = handlerSpan;
|
|
this.keySpan = keySpan;
|
|
}
|
|
}
|
|
/**
|
|
* ParsedVariable represents a variable declaration in a microsyntax expression.
|
|
*/
|
|
class ParsedVariable {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
constructor(name, value, sourceSpan, keySpan, valueSpan) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
}
|
|
var BindingType;
|
|
(function (BindingType) {
|
|
// A regular binding to a property (e.g. `[property]="expression"`).
|
|
BindingType[BindingType["Property"] = 0] = "Property";
|
|
// A binding to an element attribute (e.g. `[attr.name]="expression"`).
|
|
BindingType[BindingType["Attribute"] = 1] = "Attribute";
|
|
// A binding to a CSS class (e.g. `[class.name]="condition"`).
|
|
BindingType[BindingType["Class"] = 2] = "Class";
|
|
// A binding to a style rule (e.g. `[style.rule]="expression"`).
|
|
BindingType[BindingType["Style"] = 3] = "Style";
|
|
// A binding to a legacy animation reference (e.g. `[animate.key]="expression"`).
|
|
BindingType[BindingType["LegacyAnimation"] = 4] = "LegacyAnimation";
|
|
// Property side of a two-way binding (e.g. `[(property)]="expression"`).
|
|
BindingType[BindingType["TwoWay"] = 5] = "TwoWay";
|
|
// A binding to an animation CSS class or function (e.g. `[animate.leave]="expression"`).
|
|
BindingType[BindingType["Animation"] = 6] = "Animation";
|
|
})(BindingType || (BindingType = {}));
|
|
class BoundElementProperty {
|
|
name;
|
|
type;
|
|
securityContext;
|
|
value;
|
|
unit;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
constructor(name, type, securityContext, value, unit, sourceSpan, keySpan, valueSpan) {
|
|
this.name = name;
|
|
this.type = type;
|
|
this.securityContext = securityContext;
|
|
this.value = value;
|
|
this.unit = unit;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
}
|
|
|
|
var TagContentType;
|
|
(function (TagContentType) {
|
|
TagContentType[TagContentType["RAW_TEXT"] = 0] = "RAW_TEXT";
|
|
TagContentType[TagContentType["ESCAPABLE_RAW_TEXT"] = 1] = "ESCAPABLE_RAW_TEXT";
|
|
TagContentType[TagContentType["PARSABLE_DATA"] = 2] = "PARSABLE_DATA";
|
|
})(TagContentType || (TagContentType = {}));
|
|
function splitNsName(elementName, fatal = true) {
|
|
if (elementName[0] != ':') {
|
|
return [null, elementName];
|
|
}
|
|
const colonIndex = elementName.indexOf(':', 1);
|
|
if (colonIndex === -1) {
|
|
if (fatal) {
|
|
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
|
|
}
|
|
else {
|
|
return [null, elementName];
|
|
}
|
|
}
|
|
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
|
|
}
|
|
// `<ng-container>` tags work the same regardless the namespace
|
|
function isNgContainer(tagName) {
|
|
return splitNsName(tagName)[1] === 'ng-container';
|
|
}
|
|
// `<ng-content>` tags work the same regardless the namespace
|
|
function isNgContent(tagName) {
|
|
return splitNsName(tagName)[1] === 'ng-content';
|
|
}
|
|
// `<ng-template>` tags work the same regardless the namespace
|
|
function isNgTemplate(tagName) {
|
|
return splitNsName(tagName)[1] === 'ng-template';
|
|
}
|
|
function getNsPrefix(fullName) {
|
|
return fullName === null ? null : splitNsName(fullName)[0];
|
|
}
|
|
function mergeNsAndName(prefix, localName) {
|
|
return prefix ? `:${prefix}:${localName}` : localName;
|
|
}
|
|
|
|
/**
|
|
* This is an R3 `Node`-like wrapper for a raw `html.Comment` node. We do not currently
|
|
* require the implementation of a visitor for Comments as they are only collected at
|
|
* the top-level of the R3 AST, and only if `Render3ParseOptions['collectCommentNodes']`
|
|
* is true.
|
|
*/
|
|
let Comment$1 = class Comment {
|
|
value;
|
|
sourceSpan;
|
|
constructor(value, sourceSpan) {
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(_visitor) {
|
|
throw new Error('visit() not implemented for Comment');
|
|
}
|
|
};
|
|
let Text$3 = class Text {
|
|
value;
|
|
sourceSpan;
|
|
constructor(value, sourceSpan) {
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitText(this);
|
|
}
|
|
};
|
|
class BoundText {
|
|
value;
|
|
sourceSpan;
|
|
i18n;
|
|
constructor(value, sourceSpan, i18n) {
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitBoundText(this);
|
|
}
|
|
}
|
|
/**
|
|
* Represents a text attribute in the template.
|
|
*
|
|
* `valueSpan` may not be present in cases where there is no value `<div a></div>`.
|
|
* `keySpan` may also not be present for synthetic attributes from ICU expansions.
|
|
*/
|
|
class TextAttribute {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
i18n;
|
|
constructor(name, value, sourceSpan, keySpan, valueSpan, i18n) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitTextAttribute(this);
|
|
}
|
|
}
|
|
class BoundAttribute {
|
|
name;
|
|
type;
|
|
securityContext;
|
|
value;
|
|
unit;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
i18n;
|
|
constructor(name, type, securityContext, value, unit, sourceSpan, keySpan, valueSpan, i18n) {
|
|
this.name = name;
|
|
this.type = type;
|
|
this.securityContext = securityContext;
|
|
this.value = value;
|
|
this.unit = unit;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
static fromBoundElementProperty(prop, i18n) {
|
|
if (prop.keySpan === undefined) {
|
|
throw new Error(`Unexpected state: keySpan must be defined for bound attributes but was not for ${prop.name}: ${prop.sourceSpan}`);
|
|
}
|
|
return new BoundAttribute(prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan, prop.keySpan, prop.valueSpan, i18n);
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitBoundAttribute(this);
|
|
}
|
|
}
|
|
class BoundEvent {
|
|
name;
|
|
type;
|
|
handler;
|
|
target;
|
|
phase;
|
|
sourceSpan;
|
|
handlerSpan;
|
|
keySpan;
|
|
constructor(name, type, handler, target, phase, sourceSpan, handlerSpan, keySpan) {
|
|
this.name = name;
|
|
this.type = type;
|
|
this.handler = handler;
|
|
this.target = target;
|
|
this.phase = phase;
|
|
this.sourceSpan = sourceSpan;
|
|
this.handlerSpan = handlerSpan;
|
|
this.keySpan = keySpan;
|
|
}
|
|
static fromParsedEvent(event) {
|
|
const target = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
|
const phase = event.type === ParsedEventType.LegacyAnimation ? event.targetOrPhase : null;
|
|
if (event.keySpan === undefined) {
|
|
throw new Error(`Unexpected state: keySpan must be defined for bound event but was not for ${event.name}: ${event.sourceSpan}`);
|
|
}
|
|
return new BoundEvent(event.name, event.type, event.handler, target, phase, event.sourceSpan, event.handlerSpan, event.keySpan);
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitBoundEvent(this);
|
|
}
|
|
}
|
|
let Element$1 = class Element {
|
|
name;
|
|
attributes;
|
|
inputs;
|
|
outputs;
|
|
directives;
|
|
children;
|
|
references;
|
|
isSelfClosing;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
isVoid;
|
|
i18n;
|
|
constructor(name, attributes, inputs, outputs, directives, children, references, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan, isVoid, i18n) {
|
|
this.name = name;
|
|
this.attributes = attributes;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
this.directives = directives;
|
|
this.children = children;
|
|
this.references = references;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.isVoid = isVoid;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitElement(this);
|
|
}
|
|
};
|
|
class DeferredTrigger {
|
|
nameSpan;
|
|
sourceSpan;
|
|
prefetchSpan;
|
|
whenOrOnSourceSpan;
|
|
hydrateSpan;
|
|
constructor(nameSpan, sourceSpan, prefetchSpan, whenOrOnSourceSpan, hydrateSpan) {
|
|
this.nameSpan = nameSpan;
|
|
this.sourceSpan = sourceSpan;
|
|
this.prefetchSpan = prefetchSpan;
|
|
this.whenOrOnSourceSpan = whenOrOnSourceSpan;
|
|
this.hydrateSpan = hydrateSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeferredTrigger(this);
|
|
}
|
|
}
|
|
class BoundDeferredTrigger extends DeferredTrigger {
|
|
value;
|
|
constructor(value, sourceSpan, prefetchSpan, whenSourceSpan, hydrateSpan) {
|
|
// BoundDeferredTrigger is for 'when' triggers. These aren't really "triggers" and don't have a
|
|
// nameSpan. Trigger names are the built in event triggers like hover, interaction, etc.
|
|
super(/** nameSpan */ null, sourceSpan, prefetchSpan, whenSourceSpan, hydrateSpan);
|
|
this.value = value;
|
|
}
|
|
}
|
|
class NeverDeferredTrigger extends DeferredTrigger {
|
|
}
|
|
class IdleDeferredTrigger extends DeferredTrigger {
|
|
}
|
|
class ImmediateDeferredTrigger extends DeferredTrigger {
|
|
}
|
|
class HoverDeferredTrigger extends DeferredTrigger {
|
|
reference;
|
|
constructor(reference, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
this.reference = reference;
|
|
}
|
|
}
|
|
class TimerDeferredTrigger extends DeferredTrigger {
|
|
delay;
|
|
constructor(delay, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
this.delay = delay;
|
|
}
|
|
}
|
|
class InteractionDeferredTrigger extends DeferredTrigger {
|
|
reference;
|
|
constructor(reference, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
this.reference = reference;
|
|
}
|
|
}
|
|
class ViewportDeferredTrigger extends DeferredTrigger {
|
|
reference;
|
|
constructor(reference, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
this.reference = reference;
|
|
}
|
|
}
|
|
class BlockNode {
|
|
nameSpan;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
constructor(nameSpan, sourceSpan, startSourceSpan, endSourceSpan) {
|
|
this.nameSpan = nameSpan;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
}
|
|
}
|
|
class DeferredBlockPlaceholder extends BlockNode {
|
|
children;
|
|
minimumTime;
|
|
i18n;
|
|
constructor(children, minimumTime, nameSpan, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.children = children;
|
|
this.minimumTime = minimumTime;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeferredBlockPlaceholder(this);
|
|
}
|
|
}
|
|
class DeferredBlockLoading extends BlockNode {
|
|
children;
|
|
afterTime;
|
|
minimumTime;
|
|
i18n;
|
|
constructor(children, afterTime, minimumTime, nameSpan, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.children = children;
|
|
this.afterTime = afterTime;
|
|
this.minimumTime = minimumTime;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeferredBlockLoading(this);
|
|
}
|
|
}
|
|
class DeferredBlockError extends BlockNode {
|
|
children;
|
|
i18n;
|
|
constructor(children, nameSpan, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.children = children;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeferredBlockError(this);
|
|
}
|
|
}
|
|
class DeferredBlock extends BlockNode {
|
|
children;
|
|
placeholder;
|
|
loading;
|
|
error;
|
|
mainBlockSpan;
|
|
i18n;
|
|
triggers;
|
|
prefetchTriggers;
|
|
hydrateTriggers;
|
|
definedTriggers;
|
|
definedPrefetchTriggers;
|
|
definedHydrateTriggers;
|
|
constructor(children, triggers, prefetchTriggers, hydrateTriggers, placeholder, loading, error, nameSpan, sourceSpan, mainBlockSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.children = children;
|
|
this.placeholder = placeholder;
|
|
this.loading = loading;
|
|
this.error = error;
|
|
this.mainBlockSpan = mainBlockSpan;
|
|
this.i18n = i18n;
|
|
this.triggers = triggers;
|
|
this.prefetchTriggers = prefetchTriggers;
|
|
this.hydrateTriggers = hydrateTriggers;
|
|
// We cache the keys since we know that they won't change and we
|
|
// don't want to enumarate them every time we're traversing the AST.
|
|
this.definedTriggers = Object.keys(triggers);
|
|
this.definedPrefetchTriggers = Object.keys(prefetchTriggers);
|
|
this.definedHydrateTriggers = Object.keys(hydrateTriggers);
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeferredBlock(this);
|
|
}
|
|
visitAll(visitor) {
|
|
// Visit the hydrate triggers first to match their insertion order.
|
|
this.visitTriggers(this.definedHydrateTriggers, this.hydrateTriggers, visitor);
|
|
this.visitTriggers(this.definedTriggers, this.triggers, visitor);
|
|
this.visitTriggers(this.definedPrefetchTriggers, this.prefetchTriggers, visitor);
|
|
visitAll$1(visitor, this.children);
|
|
const remainingBlocks = [this.placeholder, this.loading, this.error].filter((x) => x !== null);
|
|
visitAll$1(visitor, remainingBlocks);
|
|
}
|
|
visitTriggers(keys, triggers, visitor) {
|
|
visitAll$1(visitor, keys.map((k) => triggers[k]));
|
|
}
|
|
}
|
|
class SwitchBlock extends BlockNode {
|
|
expression;
|
|
cases;
|
|
unknownBlocks;
|
|
constructor(expression, cases,
|
|
/**
|
|
* These blocks are only captured to allow for autocompletion in the language service. They
|
|
* aren't meant to be processed in any other way.
|
|
*/
|
|
unknownBlocks, sourceSpan, startSourceSpan, endSourceSpan, nameSpan) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.expression = expression;
|
|
this.cases = cases;
|
|
this.unknownBlocks = unknownBlocks;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitSwitchBlock(this);
|
|
}
|
|
}
|
|
class SwitchBlockCase extends BlockNode {
|
|
expression;
|
|
children;
|
|
i18n;
|
|
constructor(expression, children, sourceSpan, startSourceSpan, endSourceSpan, nameSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.expression = expression;
|
|
this.children = children;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitSwitchBlockCase(this);
|
|
}
|
|
}
|
|
class ForLoopBlock extends BlockNode {
|
|
item;
|
|
expression;
|
|
trackBy;
|
|
trackKeywordSpan;
|
|
contextVariables;
|
|
children;
|
|
empty;
|
|
mainBlockSpan;
|
|
i18n;
|
|
constructor(item, expression, trackBy, trackKeywordSpan, contextVariables, children, empty, sourceSpan, mainBlockSpan, startSourceSpan, endSourceSpan, nameSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.item = item;
|
|
this.expression = expression;
|
|
this.trackBy = trackBy;
|
|
this.trackKeywordSpan = trackKeywordSpan;
|
|
this.contextVariables = contextVariables;
|
|
this.children = children;
|
|
this.empty = empty;
|
|
this.mainBlockSpan = mainBlockSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitForLoopBlock(this);
|
|
}
|
|
}
|
|
class ForLoopBlockEmpty extends BlockNode {
|
|
children;
|
|
i18n;
|
|
constructor(children, sourceSpan, startSourceSpan, endSourceSpan, nameSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.children = children;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitForLoopBlockEmpty(this);
|
|
}
|
|
}
|
|
class IfBlock extends BlockNode {
|
|
branches;
|
|
constructor(branches, sourceSpan, startSourceSpan, endSourceSpan, nameSpan) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.branches = branches;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitIfBlock(this);
|
|
}
|
|
}
|
|
class IfBlockBranch extends BlockNode {
|
|
expression;
|
|
children;
|
|
expressionAlias;
|
|
i18n;
|
|
constructor(expression, children, expressionAlias, sourceSpan, startSourceSpan, endSourceSpan, nameSpan, i18n) {
|
|
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
|
|
this.expression = expression;
|
|
this.children = children;
|
|
this.expressionAlias = expressionAlias;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitIfBlockBranch(this);
|
|
}
|
|
}
|
|
class UnknownBlock {
|
|
name;
|
|
sourceSpan;
|
|
nameSpan;
|
|
constructor(name, sourceSpan, nameSpan) {
|
|
this.name = name;
|
|
this.sourceSpan = sourceSpan;
|
|
this.nameSpan = nameSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitUnknownBlock(this);
|
|
}
|
|
}
|
|
let LetDeclaration$1 = class LetDeclaration {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
nameSpan;
|
|
valueSpan;
|
|
constructor(name, value, sourceSpan, nameSpan, valueSpan) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.nameSpan = nameSpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitLetDeclaration(this);
|
|
}
|
|
};
|
|
let Component$1 = class Component {
|
|
componentName;
|
|
tagName;
|
|
fullName;
|
|
attributes;
|
|
inputs;
|
|
outputs;
|
|
directives;
|
|
children;
|
|
references;
|
|
isSelfClosing;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
i18n;
|
|
constructor(componentName, tagName, fullName, attributes, inputs, outputs, directives, children, references, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
this.componentName = componentName;
|
|
this.tagName = tagName;
|
|
this.fullName = fullName;
|
|
this.attributes = attributes;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
this.directives = directives;
|
|
this.children = children;
|
|
this.references = references;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitComponent(this);
|
|
}
|
|
};
|
|
let Directive$1 = class Directive {
|
|
name;
|
|
attributes;
|
|
inputs;
|
|
outputs;
|
|
references;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
i18n;
|
|
constructor(name, attributes, inputs, outputs, references, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
this.name = name;
|
|
this.attributes = attributes;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
this.references = references;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDirective(this);
|
|
}
|
|
};
|
|
class Template {
|
|
tagName;
|
|
attributes;
|
|
inputs;
|
|
outputs;
|
|
directives;
|
|
templateAttrs;
|
|
children;
|
|
references;
|
|
variables;
|
|
isSelfClosing;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
i18n;
|
|
constructor(
|
|
// tagName is the name of the container element, if applicable.
|
|
// `null` is a special case for when there is a structural directive on an `ng-template` so
|
|
// the renderer can differentiate between the synthetic template and the one written in the
|
|
// file.
|
|
tagName, attributes, inputs, outputs, directives, templateAttrs, children, references, variables, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
this.tagName = tagName;
|
|
this.attributes = attributes;
|
|
this.inputs = inputs;
|
|
this.outputs = outputs;
|
|
this.directives = directives;
|
|
this.templateAttrs = templateAttrs;
|
|
this.children = children;
|
|
this.references = references;
|
|
this.variables = variables;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitTemplate(this);
|
|
}
|
|
}
|
|
class Content {
|
|
selector;
|
|
attributes;
|
|
children;
|
|
isSelfClosing;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
i18n;
|
|
name = 'ng-content';
|
|
constructor(selector, attributes, children, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan, i18n) {
|
|
this.selector = selector;
|
|
this.attributes = attributes;
|
|
this.children = children;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitContent(this);
|
|
}
|
|
}
|
|
class Variable {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
constructor(name, value, sourceSpan, keySpan, valueSpan) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitVariable(this);
|
|
}
|
|
}
|
|
class Reference {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
keySpan;
|
|
valueSpan;
|
|
constructor(name, value, sourceSpan, keySpan, valueSpan) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.keySpan = keySpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitReference(this);
|
|
}
|
|
}
|
|
let Icu$1 = class Icu {
|
|
vars;
|
|
placeholders;
|
|
sourceSpan;
|
|
i18n;
|
|
constructor(vars, placeholders, sourceSpan, i18n) {
|
|
this.vars = vars;
|
|
this.placeholders = placeholders;
|
|
this.sourceSpan = sourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitIcu(this);
|
|
}
|
|
};
|
|
/**
|
|
* AST node that represents the host element of a directive.
|
|
* This node is used only for type checking purposes and cannot be produced from a user's template.
|
|
*/
|
|
class HostElement {
|
|
tagNames;
|
|
bindings;
|
|
listeners;
|
|
sourceSpan;
|
|
constructor(tagNames, bindings, listeners, sourceSpan) {
|
|
this.tagNames = tagNames;
|
|
this.bindings = bindings;
|
|
this.listeners = listeners;
|
|
this.sourceSpan = sourceSpan;
|
|
if (tagNames.length === 0) {
|
|
throw new Error('HostElement must have at least one tag name.');
|
|
}
|
|
}
|
|
visit() {
|
|
throw new Error(`HostElement cannot be visited`);
|
|
}
|
|
}
|
|
let RecursiveVisitor$1 = class RecursiveVisitor {
|
|
visitElement(element) {
|
|
visitAll$1(this, element.attributes);
|
|
visitAll$1(this, element.inputs);
|
|
visitAll$1(this, element.outputs);
|
|
visitAll$1(this, element.directives);
|
|
visitAll$1(this, element.children);
|
|
visitAll$1(this, element.references);
|
|
}
|
|
visitTemplate(template) {
|
|
visitAll$1(this, template.attributes);
|
|
visitAll$1(this, template.inputs);
|
|
visitAll$1(this, template.outputs);
|
|
visitAll$1(this, template.directives);
|
|
visitAll$1(this, template.children);
|
|
visitAll$1(this, template.references);
|
|
visitAll$1(this, template.variables);
|
|
}
|
|
visitDeferredBlock(deferred) {
|
|
deferred.visitAll(this);
|
|
}
|
|
visitDeferredBlockPlaceholder(block) {
|
|
visitAll$1(this, block.children);
|
|
}
|
|
visitDeferredBlockError(block) {
|
|
visitAll$1(this, block.children);
|
|
}
|
|
visitDeferredBlockLoading(block) {
|
|
visitAll$1(this, block.children);
|
|
}
|
|
visitSwitchBlock(block) {
|
|
visitAll$1(this, block.cases);
|
|
}
|
|
visitSwitchBlockCase(block) {
|
|
visitAll$1(this, block.children);
|
|
}
|
|
visitForLoopBlock(block) {
|
|
const blockItems = [block.item, ...block.contextVariables, ...block.children];
|
|
block.empty && blockItems.push(block.empty);
|
|
visitAll$1(this, blockItems);
|
|
}
|
|
visitForLoopBlockEmpty(block) {
|
|
visitAll$1(this, block.children);
|
|
}
|
|
visitIfBlock(block) {
|
|
visitAll$1(this, block.branches);
|
|
}
|
|
visitIfBlockBranch(block) {
|
|
const blockItems = block.children;
|
|
block.expressionAlias && blockItems.push(block.expressionAlias);
|
|
visitAll$1(this, blockItems);
|
|
}
|
|
visitContent(content) {
|
|
visitAll$1(this, content.children);
|
|
}
|
|
visitComponent(component) {
|
|
visitAll$1(this, component.attributes);
|
|
visitAll$1(this, component.inputs);
|
|
visitAll$1(this, component.outputs);
|
|
visitAll$1(this, component.directives);
|
|
visitAll$1(this, component.children);
|
|
visitAll$1(this, component.references);
|
|
}
|
|
visitDirective(directive) {
|
|
visitAll$1(this, directive.attributes);
|
|
visitAll$1(this, directive.inputs);
|
|
visitAll$1(this, directive.outputs);
|
|
visitAll$1(this, directive.references);
|
|
}
|
|
visitVariable(variable) { }
|
|
visitReference(reference) { }
|
|
visitTextAttribute(attribute) { }
|
|
visitBoundAttribute(attribute) { }
|
|
visitBoundEvent(attribute) { }
|
|
visitText(text) { }
|
|
visitBoundText(text) { }
|
|
visitIcu(icu) { }
|
|
visitDeferredTrigger(trigger) { }
|
|
visitUnknownBlock(block) { }
|
|
visitLetDeclaration(decl) { }
|
|
};
|
|
function visitAll$1(visitor, nodes) {
|
|
const result = [];
|
|
if (visitor.visit) {
|
|
for (const node of nodes) {
|
|
visitor.visit(node);
|
|
}
|
|
}
|
|
else {
|
|
for (const node of nodes) {
|
|
const newNode = node.visit(visitor);
|
|
if (newNode) {
|
|
result.push(newNode);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
class Message {
|
|
nodes;
|
|
placeholders;
|
|
placeholderToMessage;
|
|
meaning;
|
|
description;
|
|
customId;
|
|
sources;
|
|
id;
|
|
/** The ids to use if there are no custom id and if `i18nLegacyMessageIdFormat` is not empty */
|
|
legacyIds = [];
|
|
messageString;
|
|
/**
|
|
* @param nodes message AST
|
|
* @param placeholders maps placeholder names to static content and their source spans
|
|
* @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
|
|
* @param meaning
|
|
* @param description
|
|
* @param customId
|
|
*/
|
|
constructor(nodes, placeholders, placeholderToMessage, meaning, description, customId) {
|
|
this.nodes = nodes;
|
|
this.placeholders = placeholders;
|
|
this.placeholderToMessage = placeholderToMessage;
|
|
this.meaning = meaning;
|
|
this.description = description;
|
|
this.customId = customId;
|
|
this.id = this.customId;
|
|
this.messageString = serializeMessage(this.nodes);
|
|
if (nodes.length) {
|
|
this.sources = [
|
|
{
|
|
filePath: nodes[0].sourceSpan.start.file.url,
|
|
startLine: nodes[0].sourceSpan.start.line + 1,
|
|
startCol: nodes[0].sourceSpan.start.col + 1,
|
|
endLine: nodes[nodes.length - 1].sourceSpan.end.line + 1,
|
|
endCol: nodes[0].sourceSpan.start.col + 1,
|
|
},
|
|
];
|
|
}
|
|
else {
|
|
this.sources = [];
|
|
}
|
|
}
|
|
}
|
|
let Text$2 = class Text {
|
|
value;
|
|
sourceSpan;
|
|
constructor(value, sourceSpan) {
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitText(this, context);
|
|
}
|
|
};
|
|
// TODO(vicb): do we really need this node (vs an array) ?
|
|
class Container {
|
|
children;
|
|
sourceSpan;
|
|
constructor(children, sourceSpan) {
|
|
this.children = children;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitContainer(this, context);
|
|
}
|
|
}
|
|
class Icu {
|
|
expression;
|
|
type;
|
|
cases;
|
|
sourceSpan;
|
|
expressionPlaceholder;
|
|
constructor(expression, type, cases, sourceSpan, expressionPlaceholder) {
|
|
this.expression = expression;
|
|
this.type = type;
|
|
this.cases = cases;
|
|
this.sourceSpan = sourceSpan;
|
|
this.expressionPlaceholder = expressionPlaceholder;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitIcu(this, context);
|
|
}
|
|
}
|
|
class TagPlaceholder {
|
|
tag;
|
|
attrs;
|
|
startName;
|
|
closeName;
|
|
children;
|
|
isVoid;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
constructor(tag, attrs, startName, closeName, children, isVoid,
|
|
// TODO sourceSpan should cover all (we need a startSourceSpan and endSourceSpan)
|
|
sourceSpan, startSourceSpan, endSourceSpan) {
|
|
this.tag = tag;
|
|
this.attrs = attrs;
|
|
this.startName = startName;
|
|
this.closeName = closeName;
|
|
this.children = children;
|
|
this.isVoid = isVoid;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitTagPlaceholder(this, context);
|
|
}
|
|
}
|
|
class Placeholder {
|
|
value;
|
|
name;
|
|
sourceSpan;
|
|
constructor(value, name, sourceSpan) {
|
|
this.value = value;
|
|
this.name = name;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitPlaceholder(this, context);
|
|
}
|
|
}
|
|
class IcuPlaceholder {
|
|
value;
|
|
name;
|
|
sourceSpan;
|
|
/** Used to capture a message computed from a previous processing pass (see `setI18nRefs()`). */
|
|
previousMessage;
|
|
constructor(value, name, sourceSpan) {
|
|
this.value = value;
|
|
this.name = name;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitIcuPlaceholder(this, context);
|
|
}
|
|
}
|
|
class BlockPlaceholder {
|
|
name;
|
|
parameters;
|
|
startName;
|
|
closeName;
|
|
children;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
constructor(name, parameters, startName, closeName, children, sourceSpan, startSourceSpan, endSourceSpan) {
|
|
this.name = name;
|
|
this.parameters = parameters;
|
|
this.startName = startName;
|
|
this.closeName = closeName;
|
|
this.children = children;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitBlockPlaceholder(this, context);
|
|
}
|
|
}
|
|
// Clone the AST
|
|
class CloneVisitor {
|
|
visitText(text, context) {
|
|
return new Text$2(text.value, text.sourceSpan);
|
|
}
|
|
visitContainer(container, context) {
|
|
const children = container.children.map((n) => n.visit(this, context));
|
|
return new Container(children, container.sourceSpan);
|
|
}
|
|
visitIcu(icu, context) {
|
|
const cases = {};
|
|
Object.keys(icu.cases).forEach((key) => (cases[key] = icu.cases[key].visit(this, context)));
|
|
const msg = new Icu(icu.expression, icu.type, cases, icu.sourceSpan, icu.expressionPlaceholder);
|
|
return msg;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
const children = ph.children.map((n) => n.visit(this, context));
|
|
return new TagPlaceholder(ph.tag, ph.attrs, ph.startName, ph.closeName, children, ph.isVoid, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
return new Placeholder(ph.value, ph.name, ph.sourceSpan);
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
return new IcuPlaceholder(ph.value, ph.name, ph.sourceSpan);
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
const children = ph.children.map((n) => n.visit(this, context));
|
|
return new BlockPlaceholder(ph.name, ph.parameters, ph.startName, ph.closeName, children, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
|
|
}
|
|
}
|
|
// Visit all the nodes recursively
|
|
class RecurseVisitor {
|
|
visitText(text, context) { }
|
|
visitContainer(container, context) {
|
|
container.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitIcu(icu, context) {
|
|
Object.keys(icu.cases).forEach((k) => {
|
|
icu.cases[k].visit(this);
|
|
});
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
ph.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitPlaceholder(ph, context) { }
|
|
visitIcuPlaceholder(ph, context) { }
|
|
visitBlockPlaceholder(ph, context) {
|
|
ph.children.forEach((child) => child.visit(this));
|
|
}
|
|
}
|
|
/**
|
|
* Serialize the message to the Localize backtick string format that would appear in compiled code.
|
|
*/
|
|
function serializeMessage(messageNodes) {
|
|
const visitor = new LocalizeMessageStringVisitor();
|
|
const str = messageNodes.map((n) => n.visit(visitor)).join('');
|
|
return str;
|
|
}
|
|
class LocalizeMessageStringVisitor {
|
|
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)}}`);
|
|
return `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
|
}
|
|
visitTagPlaceholder(ph) {
|
|
const children = ph.children.map((child) => child.visit(this)).join('');
|
|
return `{$${ph.startName}}${children}{$${ph.closeName}}`;
|
|
}
|
|
visitPlaceholder(ph) {
|
|
return `{$${ph.name}}`;
|
|
}
|
|
visitIcuPlaceholder(ph) {
|
|
return `{$${ph.name}}`;
|
|
}
|
|
visitBlockPlaceholder(ph) {
|
|
const children = ph.children.map((child) => child.visit(this)).join('');
|
|
return `{$${ph.startName}}${children}{$${ph.closeName}}`;
|
|
}
|
|
}
|
|
|
|
class Serializer {
|
|
// Creates a name mapper, see `PlaceholderMapper`
|
|
// Returning `null` means that no name mapping is used.
|
|
createNameMapper(message) {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* A simple mapper that take a function to transform an internal name to a public name
|
|
*/
|
|
class SimplePlaceholderMapper extends RecurseVisitor {
|
|
mapName;
|
|
internalToPublic = {};
|
|
publicToNextId = {};
|
|
publicToInternal = {};
|
|
// create a mapping from the message
|
|
constructor(message, mapName) {
|
|
super();
|
|
this.mapName = mapName;
|
|
message.nodes.forEach((node) => node.visit(this));
|
|
}
|
|
toPublicName(internalName) {
|
|
return this.internalToPublic.hasOwnProperty(internalName)
|
|
? this.internalToPublic[internalName]
|
|
: null;
|
|
}
|
|
toInternalName(publicName) {
|
|
return this.publicToInternal.hasOwnProperty(publicName)
|
|
? this.publicToInternal[publicName]
|
|
: null;
|
|
}
|
|
visitText(text, context) {
|
|
return null;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
this.visitPlaceholderName(ph.startName);
|
|
super.visitTagPlaceholder(ph, context);
|
|
this.visitPlaceholderName(ph.closeName);
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
this.visitPlaceholderName(ph.name);
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
this.visitPlaceholderName(ph.startName);
|
|
super.visitBlockPlaceholder(ph, context);
|
|
this.visitPlaceholderName(ph.closeName);
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
this.visitPlaceholderName(ph.name);
|
|
}
|
|
// XMB placeholders could only contains A-Z, 0-9 and _
|
|
visitPlaceholderName(internalName) {
|
|
if (!internalName || this.internalToPublic.hasOwnProperty(internalName)) {
|
|
return;
|
|
}
|
|
let publicName = this.mapName(internalName);
|
|
if (this.publicToInternal.hasOwnProperty(publicName)) {
|
|
// Create a new XMB when it has already been used
|
|
const nextId = this.publicToNextId[publicName];
|
|
this.publicToNextId[publicName] = nextId + 1;
|
|
publicName = `${publicName}_${nextId}`;
|
|
}
|
|
else {
|
|
this.publicToNextId[publicName] = 1;
|
|
}
|
|
this.internalToPublic[internalName] = publicName;
|
|
this.publicToInternal[publicName] = internalName;
|
|
}
|
|
}
|
|
|
|
let _Visitor$2 = class _Visitor {
|
|
visitTag(tag) {
|
|
const strAttrs = this._serializeAttributes(tag.attrs);
|
|
if (tag.children.length == 0) {
|
|
return `<${tag.name}${strAttrs}/>`;
|
|
}
|
|
const strChildren = tag.children.map((node) => node.visit(this));
|
|
return `<${tag.name}${strAttrs}>${strChildren.join('')}</${tag.name}>`;
|
|
}
|
|
visitText(text) {
|
|
return text.value;
|
|
}
|
|
visitDeclaration(decl) {
|
|
return `<?xml${this._serializeAttributes(decl.attrs)} ?>`;
|
|
}
|
|
_serializeAttributes(attrs) {
|
|
const strAttrs = Object.keys(attrs)
|
|
.map((name) => `${name}="${attrs[name]}"`)
|
|
.join(' ');
|
|
return strAttrs.length > 0 ? ' ' + strAttrs : '';
|
|
}
|
|
visitDoctype(doctype) {
|
|
return `<!DOCTYPE ${doctype.rootTag} [\n${doctype.dtd}\n]>`;
|
|
}
|
|
};
|
|
const _visitor = new _Visitor$2();
|
|
function serialize$1(nodes) {
|
|
return nodes.map((node) => node.visit(_visitor)).join('');
|
|
}
|
|
class Declaration {
|
|
attrs = {};
|
|
constructor(unescapedAttrs) {
|
|
Object.keys(unescapedAttrs).forEach((k) => {
|
|
this.attrs[k] = escapeXml(unescapedAttrs[k]);
|
|
});
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDeclaration(this);
|
|
}
|
|
}
|
|
class Doctype {
|
|
rootTag;
|
|
dtd;
|
|
constructor(rootTag, dtd) {
|
|
this.rootTag = rootTag;
|
|
this.dtd = dtd;
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitDoctype(this);
|
|
}
|
|
}
|
|
class Tag {
|
|
name;
|
|
children;
|
|
attrs = {};
|
|
constructor(name, unescapedAttrs = {}, children = []) {
|
|
this.name = name;
|
|
this.children = children;
|
|
Object.keys(unescapedAttrs).forEach((k) => {
|
|
this.attrs[k] = escapeXml(unescapedAttrs[k]);
|
|
});
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitTag(this);
|
|
}
|
|
}
|
|
let Text$1 = class Text {
|
|
value;
|
|
constructor(unescapedValue) {
|
|
this.value = escapeXml(unescapedValue);
|
|
}
|
|
visit(visitor) {
|
|
return visitor.visitText(this);
|
|
}
|
|
};
|
|
class CR extends Text$1 {
|
|
constructor(ws = 0) {
|
|
super(`\n${new Array(ws + 1).join(' ')}`);
|
|
}
|
|
}
|
|
const _ESCAPED_CHARS = [
|
|
[/&/g, '&'],
|
|
[/"/g, '"'],
|
|
[/'/g, '''],
|
|
[/</g, '<'],
|
|
[/>/g, '>'],
|
|
];
|
|
// Escape `_ESCAPED_CHARS` characters in the given text with encoded entities
|
|
function escapeXml(text) {
|
|
return _ESCAPED_CHARS.reduce((text, entry) => text.replace(entry[0], entry[1]), text);
|
|
}
|
|
|
|
/**
|
|
* Defines the `handler` value on the serialized XMB, indicating that Angular
|
|
* generated the bundle. This is useful for analytics in Translation Console.
|
|
*
|
|
* NOTE: Keep in sync with
|
|
* packages/localize/tools/src/extract/translation_files/xmb_translation_serializer.ts.
|
|
*/
|
|
const _XMB_HANDLER = 'angular';
|
|
const _MESSAGES_TAG = 'messagebundle';
|
|
const _MESSAGE_TAG = 'msg';
|
|
const _PLACEHOLDER_TAG$3 = 'ph';
|
|
const _EXAMPLE_TAG = 'ex';
|
|
const _SOURCE_TAG$2 = 'source';
|
|
const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
|
|
<!ATTLIST messagebundle class CDATA #IMPLIED>
|
|
|
|
<!ELEMENT msg (#PCDATA|ph|source)*>
|
|
<!ATTLIST msg id CDATA #IMPLIED>
|
|
<!ATTLIST msg seq CDATA #IMPLIED>
|
|
<!ATTLIST msg name CDATA #IMPLIED>
|
|
<!ATTLIST msg desc CDATA #IMPLIED>
|
|
<!ATTLIST msg meaning CDATA #IMPLIED>
|
|
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
|
|
<!ATTLIST msg xml:space (default|preserve) "default">
|
|
<!ATTLIST msg is_hidden CDATA #IMPLIED>
|
|
|
|
<!ELEMENT source (#PCDATA)>
|
|
|
|
<!ELEMENT ph (#PCDATA|ex)*>
|
|
<!ATTLIST ph name CDATA #REQUIRED>
|
|
|
|
<!ELEMENT ex (#PCDATA)>`;
|
|
class Xmb extends Serializer {
|
|
write(messages, locale) {
|
|
const exampleVisitor = new ExampleVisitor();
|
|
const visitor = new _Visitor$1();
|
|
const rootNode = new Tag(_MESSAGES_TAG);
|
|
rootNode.attrs['handler'] = _XMB_HANDLER;
|
|
messages.forEach((message) => {
|
|
const attrs = { id: message.id };
|
|
if (message.description) {
|
|
attrs['desc'] = message.description;
|
|
}
|
|
if (message.meaning) {
|
|
attrs['meaning'] = message.meaning;
|
|
}
|
|
let sourceTags = [];
|
|
message.sources.forEach((source) => {
|
|
sourceTags.push(new Tag(_SOURCE_TAG$2, {}, [
|
|
new Text$1(`${source.filePath}:${source.startLine}${source.endLine !== source.startLine ? ',' + source.endLine : ''}`),
|
|
]));
|
|
});
|
|
rootNode.children.push(new CR(2), new Tag(_MESSAGE_TAG, attrs, [...sourceTags, ...visitor.serialize(message.nodes)]));
|
|
});
|
|
rootNode.children.push(new CR());
|
|
return serialize$1([
|
|
new Declaration({ version: '1.0', encoding: 'UTF-8' }),
|
|
new CR(),
|
|
new Doctype(_MESSAGES_TAG, _DOCTYPE),
|
|
new CR(),
|
|
exampleVisitor.addDefaultExamples(rootNode),
|
|
new CR(),
|
|
]);
|
|
}
|
|
load(content, url) {
|
|
throw new Error('Unsupported');
|
|
}
|
|
digest(message) {
|
|
return digest(message);
|
|
}
|
|
createNameMapper(message) {
|
|
return new SimplePlaceholderMapper(message, toPublicName);
|
|
}
|
|
}
|
|
let _Visitor$1 = class _Visitor {
|
|
visitText(text, context) {
|
|
return [new Text$1(text.value)];
|
|
}
|
|
visitContainer(container, context) {
|
|
const nodes = [];
|
|
container.children.forEach((node) => nodes.push(...node.visit(this)));
|
|
return nodes;
|
|
}
|
|
visitIcu(icu, context) {
|
|
const nodes = [new Text$1(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
|
|
Object.keys(icu.cases).forEach((c) => {
|
|
nodes.push(new Text$1(`${c} {`), ...icu.cases[c].visit(this), new Text$1(`} `));
|
|
});
|
|
nodes.push(new Text$1(`}`));
|
|
return nodes;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
const startTagAsText = new Text$1(`<${ph.tag}>`);
|
|
const startEx = new Tag(_EXAMPLE_TAG, {}, [startTagAsText]);
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
const startTagPh = new Tag(_PLACEHOLDER_TAG$3, { name: ph.startName }, [
|
|
startEx,
|
|
startTagAsText,
|
|
]);
|
|
if (ph.isVoid) {
|
|
// void tags have no children nor closing tags
|
|
return [startTagPh];
|
|
}
|
|
const closeTagAsText = new Text$1(`</${ph.tag}>`);
|
|
const closeEx = new Tag(_EXAMPLE_TAG, {}, [closeTagAsText]);
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
const closeTagPh = new Tag(_PLACEHOLDER_TAG$3, { name: ph.closeName }, [
|
|
closeEx,
|
|
closeTagAsText,
|
|
]);
|
|
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
const interpolationAsText = new Text$1(`{{${ph.value}}}`);
|
|
// Example tag needs to be not-empty for TC.
|
|
const exTag = new Tag(_EXAMPLE_TAG, {}, [interpolationAsText]);
|
|
return [
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
new Tag(_PLACEHOLDER_TAG$3, { name: ph.name }, [exTag, interpolationAsText]),
|
|
];
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
const startAsText = new Text$1(`@${ph.name}`);
|
|
const startEx = new Tag(_EXAMPLE_TAG, {}, [startAsText]);
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
const startTagPh = new Tag(_PLACEHOLDER_TAG$3, { name: ph.startName }, [startEx, startAsText]);
|
|
const closeAsText = new Text$1(`}`);
|
|
const closeEx = new Tag(_EXAMPLE_TAG, {}, [closeAsText]);
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
const closeTagPh = new Tag(_PLACEHOLDER_TAG$3, { name: ph.closeName }, [closeEx, closeAsText]);
|
|
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
const icuExpression = ph.value.expression;
|
|
const icuType = ph.value.type;
|
|
const icuCases = Object.keys(ph.value.cases)
|
|
.map((value) => value + ' {...}')
|
|
.join(' ');
|
|
const icuAsText = new Text$1(`{${icuExpression}, ${icuType}, ${icuCases}}`);
|
|
const exTag = new Tag(_EXAMPLE_TAG, {}, [icuAsText]);
|
|
return [
|
|
// TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
|
|
new Tag(_PLACEHOLDER_TAG$3, { name: ph.name }, [exTag, icuAsText]),
|
|
];
|
|
}
|
|
serialize(nodes) {
|
|
return [].concat(...nodes.map((node) => node.visit(this)));
|
|
}
|
|
};
|
|
function digest(message) {
|
|
return decimalDigest(message);
|
|
}
|
|
// TC requires at least one non-empty example on placeholders
|
|
class ExampleVisitor {
|
|
addDefaultExamples(node) {
|
|
node.visit(this);
|
|
return node;
|
|
}
|
|
visitTag(tag) {
|
|
if (tag.name === _PLACEHOLDER_TAG$3) {
|
|
if (!tag.children || tag.children.length == 0) {
|
|
const exText = new Text$1(tag.attrs['name'] || '...');
|
|
tag.children = [new Tag(_EXAMPLE_TAG, {}, [exText])];
|
|
}
|
|
}
|
|
else if (tag.children) {
|
|
tag.children.forEach((node) => node.visit(this));
|
|
}
|
|
}
|
|
visitText(text) { }
|
|
visitDeclaration(decl) { }
|
|
visitDoctype(doctype) { }
|
|
}
|
|
// XMB/XTB placeholders can only contain A-Z, 0-9 and _
|
|
function toPublicName(internalName) {
|
|
return internalName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
|
|
}
|
|
|
|
/** Name of the i18n attributes **/
|
|
const I18N_ATTR = 'i18n';
|
|
const I18N_ATTR_PREFIX = 'i18n-';
|
|
/** Prefix of var expressions used in ICUs */
|
|
const I18N_ICU_VAR_PREFIX = 'VAR_';
|
|
function isI18nAttribute(name) {
|
|
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
|
|
}
|
|
function hasI18nAttrs(node) {
|
|
return node.attrs.some((attr) => isI18nAttribute(attr.name));
|
|
}
|
|
function icuFromI18nMessage(message) {
|
|
return message.nodes[0];
|
|
}
|
|
/**
|
|
* Format the placeholder names in a map of placeholders to expressions.
|
|
*
|
|
* The placeholder names are converted from "internal" format (e.g. `START_TAG_DIV_1`) to "external"
|
|
* format (e.g. `startTagDiv_1`).
|
|
*
|
|
* @param params A map of placeholder names to expressions.
|
|
* @param useCamelCase whether to camelCase the placeholder name when formatting.
|
|
* @returns A new map of formatted placeholder names to expressions.
|
|
*/
|
|
function formatI18nPlaceholderNamesInMap(params = {}, useCamelCase) {
|
|
const _params = {};
|
|
if (params && Object.keys(params).length) {
|
|
Object.keys(params).forEach((key) => (_params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]));
|
|
}
|
|
return _params;
|
|
}
|
|
/**
|
|
* Converts internal placeholder names to public-facing format
|
|
* (for example to use in goog.getMsg call).
|
|
* Example: `START_TAG_DIV_1` is converted to `startTagDiv_1`.
|
|
*
|
|
* @param name The placeholder name that should be formatted
|
|
* @returns Formatted placeholder name
|
|
*/
|
|
function formatI18nPlaceholderName(name, useCamelCase = true) {
|
|
const publicName = toPublicName(name);
|
|
if (!useCamelCase) {
|
|
return publicName;
|
|
}
|
|
const chunks = publicName.split('_');
|
|
if (chunks.length === 1) {
|
|
// if no "_" found - just lowercase the value
|
|
return name.toLowerCase();
|
|
}
|
|
let postfix;
|
|
// eject last element if it's a number
|
|
if (/^\d+$/.test(chunks[chunks.length - 1])) {
|
|
postfix = chunks.pop();
|
|
}
|
|
let raw = chunks.shift().toLowerCase();
|
|
if (chunks.length) {
|
|
raw += chunks.map((c) => c.charAt(0).toUpperCase() + c.slice(1).toLowerCase()).join('');
|
|
}
|
|
return postfix ? `${raw}_${postfix}` : raw;
|
|
}
|
|
|
|
/**
|
|
* Checks whether an object key contains potentially unsafe chars, thus the key should be wrapped in
|
|
* quotes. Note: we do not wrap all keys into quotes, as it may have impact on minification and may
|
|
* not work in some cases when object keys are mangled by a minifier.
|
|
*
|
|
* TODO(FW-1136): this is a temporary solution, we need to come up with a better way of working with
|
|
* inputs that contain potentially unsafe chars.
|
|
*/
|
|
const UNSAFE_OBJECT_KEY_NAME_REGEXP = /[-.]/;
|
|
/** Name of the temporary to use during data binding */
|
|
const TEMPORARY_NAME = '_t';
|
|
/** Name of the context parameter passed into a template function */
|
|
const CONTEXT_NAME = 'ctx';
|
|
/** Name of the RenderFlag passed into a template function */
|
|
const RENDER_FLAGS = 'rf';
|
|
/**
|
|
* Creates an allocator for a temporary variable.
|
|
*
|
|
* A variable declaration is added to the statements the first time the allocator is invoked.
|
|
*/
|
|
function temporaryAllocator(pushStatement, name) {
|
|
let temp = null;
|
|
return () => {
|
|
if (!temp) {
|
|
pushStatement(new DeclareVarStmt(TEMPORARY_NAME, undefined, DYNAMIC_TYPE));
|
|
temp = variable(name);
|
|
}
|
|
return temp;
|
|
};
|
|
}
|
|
function asLiteral(value) {
|
|
if (Array.isArray(value)) {
|
|
return literalArr(value.map(asLiteral));
|
|
}
|
|
return literal(value, INFERRED_TYPE);
|
|
}
|
|
/**
|
|
* Serializes inputs and outputs for `defineDirective` and `defineComponent`.
|
|
*
|
|
* This will attempt to generate optimized data structures to minimize memory or
|
|
* file size of fully compiled applications.
|
|
*/
|
|
function conditionallyCreateDirectiveBindingLiteral(map, forInputs) {
|
|
const keys = Object.getOwnPropertyNames(map);
|
|
if (keys.length === 0) {
|
|
return null;
|
|
}
|
|
return literalMap(keys.map((key) => {
|
|
const value = map[key];
|
|
let declaredName;
|
|
let publicName;
|
|
let minifiedName;
|
|
let expressionValue;
|
|
if (typeof value === 'string') {
|
|
// canonical syntax: `dirProp: publicProp`
|
|
declaredName = key;
|
|
minifiedName = key;
|
|
publicName = value;
|
|
expressionValue = asLiteral(publicName);
|
|
}
|
|
else {
|
|
minifiedName = key;
|
|
declaredName = value.classPropertyName;
|
|
publicName = value.bindingPropertyName;
|
|
const differentDeclaringName = publicName !== declaredName;
|
|
const hasDecoratorInputTransform = value.transformFunction !== null;
|
|
let flags = InputFlags.None;
|
|
// Build up input flags
|
|
if (value.isSignal) {
|
|
flags |= InputFlags.SignalBased;
|
|
}
|
|
if (hasDecoratorInputTransform) {
|
|
flags |= InputFlags.HasDecoratorInputTransform;
|
|
}
|
|
// Inputs, compared to outputs, will track their declared name (for `ngOnChanges`), support
|
|
// decorator input transform functions, or store flag information if there is any.
|
|
if (forInputs &&
|
|
(differentDeclaringName || hasDecoratorInputTransform || flags !== InputFlags.None)) {
|
|
const result = [literal(flags), asLiteral(publicName)];
|
|
if (differentDeclaringName || hasDecoratorInputTransform) {
|
|
result.push(asLiteral(declaredName));
|
|
if (hasDecoratorInputTransform) {
|
|
result.push(value.transformFunction);
|
|
}
|
|
}
|
|
expressionValue = literalArr(result);
|
|
}
|
|
else {
|
|
expressionValue = asLiteral(publicName);
|
|
}
|
|
}
|
|
return {
|
|
key: minifiedName,
|
|
// put quotes around keys that contain potentially unsafe characters
|
|
quoted: UNSAFE_OBJECT_KEY_NAME_REGEXP.test(minifiedName),
|
|
value: expressionValue,
|
|
};
|
|
}));
|
|
}
|
|
/**
|
|
* A representation for an object literal used during codegen of definition objects. The generic
|
|
* type `T` allows to reference a documented type of the generated structure, such that the
|
|
* property names that are set can be resolved to their documented declaration.
|
|
*/
|
|
class DefinitionMap {
|
|
values = [];
|
|
set(key, value) {
|
|
if (value) {
|
|
const existing = this.values.find((value) => value.key === key);
|
|
if (existing) {
|
|
existing.value = value;
|
|
}
|
|
else {
|
|
this.values.push({ key: key, value, quoted: false });
|
|
}
|
|
}
|
|
}
|
|
toLiteralMap() {
|
|
return literalMap(this.values);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a `CssSelector` from an AST node.
|
|
*/
|
|
function createCssSelectorFromNode(node) {
|
|
const elementName = node instanceof Element$1 ? node.name : 'ng-template';
|
|
const attributes = getAttrsForDirectiveMatching(node);
|
|
const cssSelector = new CssSelector();
|
|
const elementNameNoNs = splitNsName(elementName)[1];
|
|
cssSelector.setElement(elementNameNoNs);
|
|
Object.getOwnPropertyNames(attributes).forEach((name) => {
|
|
const nameNoNs = splitNsName(name)[1];
|
|
const value = attributes[name];
|
|
cssSelector.addAttribute(nameNoNs, value);
|
|
if (name.toLowerCase() === 'class') {
|
|
const classes = value.trim().split(/\s+/);
|
|
classes.forEach((className) => cssSelector.addClassName(className));
|
|
}
|
|
});
|
|
return cssSelector;
|
|
}
|
|
/**
|
|
* Extract a map of properties to values for a given element or template node, which can be used
|
|
* by the directive matching machinery.
|
|
*
|
|
* @param elOrTpl the element or template in question
|
|
* @return an object set up for directive matching. For attributes on the element/template, this
|
|
* object maps a property name to its (static) value. For any bindings, this map simply maps the
|
|
* property name to an empty string.
|
|
*/
|
|
function getAttrsForDirectiveMatching(elOrTpl) {
|
|
const attributesMap = {};
|
|
if (elOrTpl instanceof Template && elOrTpl.tagName !== 'ng-template') {
|
|
elOrTpl.templateAttrs.forEach((a) => (attributesMap[a.name] = ''));
|
|
}
|
|
else {
|
|
elOrTpl.attributes.forEach((a) => {
|
|
if (!isI18nAttribute(a.name)) {
|
|
attributesMap[a.name] = a.value;
|
|
}
|
|
});
|
|
elOrTpl.inputs.forEach((i) => {
|
|
if (i.type === BindingType.Property || i.type === BindingType.TwoWay) {
|
|
attributesMap[i.name] = '';
|
|
}
|
|
});
|
|
elOrTpl.outputs.forEach((o) => {
|
|
attributesMap[o.name] = '';
|
|
});
|
|
}
|
|
return attributesMap;
|
|
}
|
|
|
|
function compileInjectable(meta, resolveForwardRefs) {
|
|
let result = null;
|
|
const factoryMeta = {
|
|
name: meta.name,
|
|
type: meta.type,
|
|
typeArgumentCount: meta.typeArgumentCount,
|
|
deps: [],
|
|
target: FactoryTarget.Injectable,
|
|
};
|
|
if (meta.useClass !== undefined) {
|
|
// meta.useClass has two modes of operation. Either deps are specified, in which case `new` is
|
|
// used to instantiate the class with dependencies injected, or deps are not specified and
|
|
// the factory of the class is used to instantiate it.
|
|
//
|
|
// A special case exists for useClass: Type where Type is the injectable type itself and no
|
|
// deps are specified, in which case 'useClass' is effectively ignored.
|
|
const useClassOnSelf = meta.useClass.expression.isEquivalent(meta.type.value);
|
|
let deps = undefined;
|
|
if (meta.deps !== undefined) {
|
|
deps = meta.deps;
|
|
}
|
|
if (deps !== undefined) {
|
|
// factory: () => new meta.useClass(...deps)
|
|
result = compileFactoryFunction({
|
|
...factoryMeta,
|
|
delegate: meta.useClass.expression,
|
|
delegateDeps: deps,
|
|
delegateType: R3FactoryDelegateType.Class,
|
|
});
|
|
}
|
|
else if (useClassOnSelf) {
|
|
result = compileFactoryFunction(factoryMeta);
|
|
}
|
|
else {
|
|
result = {
|
|
statements: [],
|
|
expression: delegateToFactory(meta.type.value, meta.useClass.expression, resolveForwardRefs),
|
|
};
|
|
}
|
|
}
|
|
else if (meta.useFactory !== undefined) {
|
|
if (meta.deps !== undefined) {
|
|
result = compileFactoryFunction({
|
|
...factoryMeta,
|
|
delegate: meta.useFactory,
|
|
delegateDeps: meta.deps || [],
|
|
delegateType: R3FactoryDelegateType.Function,
|
|
});
|
|
}
|
|
else {
|
|
result = { statements: [], expression: arrowFn([], meta.useFactory.callFn([])) };
|
|
}
|
|
}
|
|
else if (meta.useValue !== undefined) {
|
|
// Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for
|
|
// client code because meta.useValue is an Expression which will be defined even if the actual
|
|
// value is undefined.
|
|
result = compileFactoryFunction({
|
|
...factoryMeta,
|
|
expression: meta.useValue.expression,
|
|
});
|
|
}
|
|
else if (meta.useExisting !== undefined) {
|
|
// useExisting is an `inject` call on the existing token.
|
|
result = compileFactoryFunction({
|
|
...factoryMeta,
|
|
expression: importExpr(Identifiers.inject).callFn([meta.useExisting.expression]),
|
|
});
|
|
}
|
|
else {
|
|
result = {
|
|
statements: [],
|
|
expression: delegateToFactory(meta.type.value, meta.type.value, resolveForwardRefs),
|
|
};
|
|
}
|
|
const token = meta.type.value;
|
|
const injectableProps = new DefinitionMap();
|
|
injectableProps.set('token', token);
|
|
injectableProps.set('factory', result.expression);
|
|
// Only generate providedIn property if it has a non-null value
|
|
if (meta.providedIn.expression.value !== null) {
|
|
injectableProps.set('providedIn', convertFromMaybeForwardRefExpression(meta.providedIn));
|
|
}
|
|
const expression = importExpr(Identifiers.ɵɵdefineInjectable)
|
|
.callFn([injectableProps.toLiteralMap()], undefined, true);
|
|
return {
|
|
expression,
|
|
type: createInjectableType(meta),
|
|
statements: result.statements,
|
|
};
|
|
}
|
|
function createInjectableType(meta) {
|
|
return new ExpressionType(importExpr(Identifiers.InjectableDeclaration, [
|
|
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
|
]));
|
|
}
|
|
function delegateToFactory(type, useType, unwrapForwardRefs) {
|
|
if (type.node === useType.node) {
|
|
// The types are the same, so we can simply delegate directly to the type's factory.
|
|
// ```
|
|
// factory: type.ɵfac
|
|
// ```
|
|
return useType.prop('ɵfac');
|
|
}
|
|
if (!unwrapForwardRefs) {
|
|
// The type is not wrapped in a `forwardRef()`, so we create a simple factory function that
|
|
// accepts a sub-type as an argument.
|
|
// ```
|
|
// factory: function(t) { return useType.ɵfac(t); }
|
|
// ```
|
|
return createFactoryFunction(useType);
|
|
}
|
|
// The useType is actually wrapped in a `forwardRef()` so we need to resolve that before
|
|
// calling its factory.
|
|
// ```
|
|
// factory: function(t) { return core.resolveForwardRef(type).ɵfac(t); }
|
|
// ```
|
|
const unwrappedType = importExpr(Identifiers.resolveForwardRef).callFn([useType]);
|
|
return createFactoryFunction(unwrappedType);
|
|
}
|
|
function createFactoryFunction(type) {
|
|
const t = new FnParam('__ngFactoryType__', DYNAMIC_TYPE);
|
|
return arrowFn([t], type.prop('ɵfac').callFn([variable(t.name)]));
|
|
}
|
|
|
|
const UNUSABLE_INTERPOLATION_REGEXPS = [
|
|
/@/, // control flow reserved symbol
|
|
/^\s*$/, // empty
|
|
/[<>]/, // html tag
|
|
/^[{}]$/, // i18n expansion
|
|
/&(#|[a-z])/i, // character reference,
|
|
/^\/\//, // comment
|
|
];
|
|
function assertInterpolationSymbols(identifier, value) {
|
|
if (value != null && !(Array.isArray(value) && value.length == 2)) {
|
|
throw new Error(`Expected '${identifier}' to be an array, [start, end].`);
|
|
}
|
|
else if (value != null) {
|
|
const start = value[0];
|
|
const end = value[1];
|
|
// Check for unusable interpolation symbols
|
|
UNUSABLE_INTERPOLATION_REGEXPS.forEach((regexp) => {
|
|
if (regexp.test(start) || regexp.test(end)) {
|
|
throw new Error(`['${start}', '${end}'] contains unusable interpolation symbol.`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class InterpolationConfig {
|
|
start;
|
|
end;
|
|
static fromArray(markers) {
|
|
if (!markers) {
|
|
return DEFAULT_INTERPOLATION_CONFIG;
|
|
}
|
|
assertInterpolationSymbols('interpolation', markers);
|
|
return new InterpolationConfig(markers[0], markers[1]);
|
|
}
|
|
constructor(start, end) {
|
|
this.start = start;
|
|
this.end = end;
|
|
}
|
|
}
|
|
const DEFAULT_INTERPOLATION_CONFIG = new InterpolationConfig('{{', '}}');
|
|
const DEFAULT_CONTAINER_BLOCKS = new Set(['switch']);
|
|
|
|
const $EOF = 0;
|
|
const $BSPACE = 8;
|
|
const $TAB = 9;
|
|
const $LF = 10;
|
|
const $VTAB = 11;
|
|
const $FF = 12;
|
|
const $CR = 13;
|
|
const $SPACE = 32;
|
|
const $BANG = 33;
|
|
const $DQ = 34;
|
|
const $HASH = 35;
|
|
const $$ = 36;
|
|
const $PERCENT = 37;
|
|
const $AMPERSAND = 38;
|
|
const $SQ = 39;
|
|
const $LPAREN = 40;
|
|
const $RPAREN = 41;
|
|
const $STAR = 42;
|
|
const $PLUS = 43;
|
|
const $COMMA = 44;
|
|
const $MINUS = 45;
|
|
const $PERIOD = 46;
|
|
const $SLASH = 47;
|
|
const $COLON = 58;
|
|
const $SEMICOLON = 59;
|
|
const $LT = 60;
|
|
const $EQ = 61;
|
|
const $GT = 62;
|
|
const $QUESTION = 63;
|
|
const $0 = 48;
|
|
const $7 = 55;
|
|
const $9 = 57;
|
|
const $A = 65;
|
|
const $E = 69;
|
|
const $F = 70;
|
|
const $X = 88;
|
|
const $Z = 90;
|
|
const $LBRACKET = 91;
|
|
const $BACKSLASH = 92;
|
|
const $RBRACKET = 93;
|
|
const $CARET = 94;
|
|
const $_ = 95;
|
|
const $a = 97;
|
|
const $b = 98;
|
|
const $e = 101;
|
|
const $f = 102;
|
|
const $n = 110;
|
|
const $r = 114;
|
|
const $t = 116;
|
|
const $u = 117;
|
|
const $v = 118;
|
|
const $x = 120;
|
|
const $z = 122;
|
|
const $LBRACE = 123;
|
|
const $BAR = 124;
|
|
const $RBRACE = 125;
|
|
const $NBSP = 160;
|
|
const $AT = 64;
|
|
const $BT = 96;
|
|
function isWhitespace(code) {
|
|
return (code >= $TAB && code <= $SPACE) || code == $NBSP;
|
|
}
|
|
function isDigit(code) {
|
|
return $0 <= code && code <= $9;
|
|
}
|
|
function isAsciiLetter(code) {
|
|
return (code >= $a && code <= $z) || (code >= $A && code <= $Z);
|
|
}
|
|
function isAsciiHexDigit(code) {
|
|
return (code >= $a && code <= $f) || (code >= $A && code <= $F) || isDigit(code);
|
|
}
|
|
function isNewLine(code) {
|
|
return code === $LF || code === $CR;
|
|
}
|
|
function isOctalDigit(code) {
|
|
return $0 <= code && code <= $7;
|
|
}
|
|
function isQuote(code) {
|
|
return code === $SQ || code === $DQ || code === $BT;
|
|
}
|
|
|
|
class ParseLocation {
|
|
file;
|
|
offset;
|
|
line;
|
|
col;
|
|
constructor(file, offset, line, col) {
|
|
this.file = file;
|
|
this.offset = offset;
|
|
this.line = line;
|
|
this.col = col;
|
|
}
|
|
toString() {
|
|
return this.offset != null ? `${this.file.url}@${this.line}:${this.col}` : this.file.url;
|
|
}
|
|
moveBy(delta) {
|
|
const source = this.file.content;
|
|
const len = source.length;
|
|
let offset = this.offset;
|
|
let line = this.line;
|
|
let col = this.col;
|
|
while (offset > 0 && delta < 0) {
|
|
offset--;
|
|
delta++;
|
|
const ch = source.charCodeAt(offset);
|
|
if (ch == $LF) {
|
|
line--;
|
|
const priorLine = source
|
|
.substring(0, offset - 1)
|
|
.lastIndexOf(String.fromCharCode($LF));
|
|
col = priorLine > 0 ? offset - priorLine : offset;
|
|
}
|
|
else {
|
|
col--;
|
|
}
|
|
}
|
|
while (offset < len && delta > 0) {
|
|
const ch = source.charCodeAt(offset);
|
|
offset++;
|
|
delta--;
|
|
if (ch == $LF) {
|
|
line++;
|
|
col = 0;
|
|
}
|
|
else {
|
|
col++;
|
|
}
|
|
}
|
|
return new ParseLocation(this.file, offset, line, col);
|
|
}
|
|
// Return the source around the location
|
|
// Up to `maxChars` or `maxLines` on each side of the location
|
|
getContext(maxChars, maxLines) {
|
|
const content = this.file.content;
|
|
let startOffset = this.offset;
|
|
if (startOffset != null) {
|
|
if (startOffset > content.length - 1) {
|
|
startOffset = content.length - 1;
|
|
}
|
|
let endOffset = startOffset;
|
|
let ctxChars = 0;
|
|
let ctxLines = 0;
|
|
while (ctxChars < maxChars && startOffset > 0) {
|
|
startOffset--;
|
|
ctxChars++;
|
|
if (content[startOffset] == '\n') {
|
|
if (++ctxLines == maxLines) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
ctxChars = 0;
|
|
ctxLines = 0;
|
|
while (ctxChars < maxChars && endOffset < content.length - 1) {
|
|
endOffset++;
|
|
ctxChars++;
|
|
if (content[endOffset] == '\n') {
|
|
if (++ctxLines == maxLines) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
before: content.substring(startOffset, this.offset),
|
|
after: content.substring(this.offset, endOffset + 1),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
class ParseSourceFile {
|
|
content;
|
|
url;
|
|
constructor(content, url) {
|
|
this.content = content;
|
|
this.url = url;
|
|
}
|
|
}
|
|
class ParseSourceSpan {
|
|
start;
|
|
end;
|
|
fullStart;
|
|
details;
|
|
/**
|
|
* Create an object that holds information about spans of tokens/nodes captured during
|
|
* lexing/parsing of text.
|
|
*
|
|
* @param start
|
|
* The location of the start of the span (having skipped leading trivia).
|
|
* Skipping leading trivia makes source-spans more "user friendly", since things like HTML
|
|
* elements will appear to begin at the start of the opening tag, rather than at the start of any
|
|
* leading trivia, which could include newlines.
|
|
*
|
|
* @param end
|
|
* The location of the end of the span.
|
|
*
|
|
* @param fullStart
|
|
* The start of the token without skipping the leading trivia.
|
|
* This is used by tooling that splits tokens further, such as extracting Angular interpolations
|
|
* from text tokens. Such tooling creates new source-spans relative to the original token's
|
|
* source-span. If leading trivia characters have been skipped then the new source-spans may be
|
|
* incorrectly offset.
|
|
*
|
|
* @param details
|
|
* Additional information (such as identifier names) that should be associated with the span.
|
|
*/
|
|
constructor(start, end, fullStart = start, details = null) {
|
|
this.start = start;
|
|
this.end = end;
|
|
this.fullStart = fullStart;
|
|
this.details = details;
|
|
}
|
|
toString() {
|
|
return this.start.file.content.substring(this.start.offset, this.end.offset);
|
|
}
|
|
}
|
|
var ParseErrorLevel;
|
|
(function (ParseErrorLevel) {
|
|
ParseErrorLevel[ParseErrorLevel["WARNING"] = 0] = "WARNING";
|
|
ParseErrorLevel[ParseErrorLevel["ERROR"] = 1] = "ERROR";
|
|
})(ParseErrorLevel || (ParseErrorLevel = {}));
|
|
class ParseError extends Error {
|
|
span;
|
|
msg;
|
|
level;
|
|
relatedError;
|
|
constructor(
|
|
/** Location of the error. */
|
|
span,
|
|
/** Error message. */
|
|
msg,
|
|
/** Severity level of the error. */
|
|
level = ParseErrorLevel.ERROR,
|
|
/**
|
|
* Error that caused the error to be surfaced. For example, an error in a sub-expression that
|
|
* couldn't be parsed. Not guaranteed to be defined, but can be used to provide more context.
|
|
*/
|
|
relatedError) {
|
|
super(msg);
|
|
this.span = span;
|
|
this.msg = msg;
|
|
this.level = level;
|
|
this.relatedError = relatedError;
|
|
// Extending `Error` ends up breaking some internal tests. This appears to be a known issue
|
|
// when extending errors in TS and the workaround is to explicitly set the prototype.
|
|
// https://stackoverflow.com/questions/41102060/typescript-extending-error-class
|
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
}
|
|
contextualMessage() {
|
|
const ctx = this.span.start.getContext(100, 3);
|
|
return ctx
|
|
? `${this.msg} ("${ctx.before}[${ParseErrorLevel[this.level]} ->]${ctx.after}")`
|
|
: this.msg;
|
|
}
|
|
toString() {
|
|
const details = this.span.details ? `, ${this.span.details}` : '';
|
|
return `${this.contextualMessage()}: ${this.span.start}${details}`;
|
|
}
|
|
}
|
|
/**
|
|
* Generates Source Span object for a given R3 Type for JIT mode.
|
|
*
|
|
* @param kind Component or Directive.
|
|
* @param typeName name of the Component or Directive.
|
|
* @param sourceUrl reference to Component or Directive source.
|
|
* @returns instance of ParseSourceSpan that represent a given Component or Directive.
|
|
*/
|
|
function r3JitTypeSourceSpan(kind, typeName, sourceUrl) {
|
|
const sourceFileName = `in ${kind} ${typeName} in ${sourceUrl}`;
|
|
const sourceFile = new ParseSourceFile('', sourceFileName);
|
|
return new ParseSourceSpan(new ParseLocation(sourceFile, -1, -1, -1), new ParseLocation(sourceFile, -1, -1, -1));
|
|
}
|
|
let _anonymousTypeIndex = 0;
|
|
function identifierName(compileIdentifier) {
|
|
if (!compileIdentifier || !compileIdentifier.reference) {
|
|
return null;
|
|
}
|
|
const ref = compileIdentifier.reference;
|
|
if (ref['__anonymousType']) {
|
|
return ref['__anonymousType'];
|
|
}
|
|
if (ref['__forward_ref__']) {
|
|
// We do not want to try to stringify a `forwardRef()` function because that would cause the
|
|
// inner function to be evaluated too early, defeating the whole point of the `forwardRef`.
|
|
return '__forward_ref__';
|
|
}
|
|
let identifier = stringify(ref);
|
|
if (identifier.indexOf('(') >= 0) {
|
|
// case: anonymous functions!
|
|
identifier = `anonymous_${_anonymousTypeIndex++}`;
|
|
ref['__anonymousType'] = identifier;
|
|
}
|
|
else {
|
|
identifier = sanitizeIdentifier(identifier);
|
|
}
|
|
return identifier;
|
|
}
|
|
function sanitizeIdentifier(name) {
|
|
return name.replace(/\W/g, '_');
|
|
}
|
|
|
|
/**
|
|
* In TypeScript, tagged template functions expect a "template object", which is an array of
|
|
* "cooked" strings plus a `raw` property that contains an array of "raw" strings. This is
|
|
* typically constructed with a function called `__makeTemplateObject(cooked, raw)`, but it may not
|
|
* be available in all environments.
|
|
*
|
|
* This is a JavaScript polyfill that uses __makeTemplateObject when it's available, but otherwise
|
|
* creates an inline helper with the same functionality.
|
|
*
|
|
* In the inline function, if `Object.defineProperty` is available we use that to attach the `raw`
|
|
* array.
|
|
*/
|
|
const makeTemplateObjectPolyfill = '(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})';
|
|
class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
|
|
constructor() {
|
|
super(false);
|
|
}
|
|
visitWrappedNodeExpr(ast, ctx) {
|
|
throw new Error('Cannot emit a WrappedNodeExpr in Javascript.');
|
|
}
|
|
visitDeclareVarStmt(stmt, ctx) {
|
|
ctx.print(stmt, `var ${stmt.name}`);
|
|
if (stmt.value) {
|
|
ctx.print(stmt, ' = ');
|
|
stmt.value.visitExpression(this, ctx);
|
|
}
|
|
ctx.println(stmt, `;`);
|
|
return null;
|
|
}
|
|
visitTaggedTemplateLiteralExpr(ast, ctx) {
|
|
// The following convoluted piece of code is effectively the downlevelled equivalent of
|
|
// ```
|
|
// tag`...`
|
|
// ```
|
|
// which is effectively like:
|
|
// ```
|
|
// tag(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
|
|
// ```
|
|
const elements = ast.template.elements;
|
|
ast.tag.visitExpression(this, ctx);
|
|
ctx.print(ast, `(${makeTemplateObjectPolyfill}(`);
|
|
ctx.print(ast, `[${elements.map((part) => escapeIdentifier(part.text, false)).join(', ')}], `);
|
|
ctx.print(ast, `[${elements.map((part) => escapeIdentifier(part.rawText, false)).join(', ')}])`);
|
|
ast.template.expressions.forEach((expression) => {
|
|
ctx.print(ast, ', ');
|
|
expression.visitExpression(this, ctx);
|
|
});
|
|
ctx.print(ast, ')');
|
|
return null;
|
|
}
|
|
visitTemplateLiteralExpr(expr, ctx) {
|
|
ctx.print(expr, '`');
|
|
for (let i = 0; i < expr.elements.length; i++) {
|
|
expr.elements[i].visitExpression(this, ctx);
|
|
const expression = i < expr.expressions.length ? expr.expressions[i] : null;
|
|
if (expression !== null) {
|
|
ctx.print(expression, '${');
|
|
expression.visitExpression(this, ctx);
|
|
ctx.print(expression, '}');
|
|
}
|
|
}
|
|
ctx.print(expr, '`');
|
|
}
|
|
visitTemplateLiteralElementExpr(expr, ctx) {
|
|
ctx.print(expr, expr.rawText);
|
|
return null;
|
|
}
|
|
visitFunctionExpr(ast, ctx) {
|
|
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
|
|
this._visitParams(ast.params, ctx);
|
|
ctx.println(ast, `) {`);
|
|
ctx.incIndent();
|
|
this.visitAllStatements(ast.statements, ctx);
|
|
ctx.decIndent();
|
|
ctx.print(ast, `}`);
|
|
return null;
|
|
}
|
|
visitArrowFunctionExpr(ast, ctx) {
|
|
ctx.print(ast, '(');
|
|
this._visitParams(ast.params, ctx);
|
|
ctx.print(ast, ') =>');
|
|
if (Array.isArray(ast.body)) {
|
|
ctx.println(ast, `{`);
|
|
ctx.incIndent();
|
|
this.visitAllStatements(ast.body, ctx);
|
|
ctx.decIndent();
|
|
ctx.print(ast, `}`);
|
|
}
|
|
else {
|
|
const isObjectLiteral = ast.body instanceof LiteralMapExpr;
|
|
if (isObjectLiteral) {
|
|
ctx.print(ast, '(');
|
|
}
|
|
ast.body.visitExpression(this, ctx);
|
|
if (isObjectLiteral) {
|
|
ctx.print(ast, ')');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
visitDeclareFunctionStmt(stmt, ctx) {
|
|
ctx.print(stmt, `function ${stmt.name}(`);
|
|
this._visitParams(stmt.params, ctx);
|
|
ctx.println(stmt, `) {`);
|
|
ctx.incIndent();
|
|
this.visitAllStatements(stmt.statements, ctx);
|
|
ctx.decIndent();
|
|
ctx.println(stmt, `}`);
|
|
return null;
|
|
}
|
|
visitLocalizedString(ast, ctx) {
|
|
// The following convoluted piece of code is effectively the downlevelled equivalent of
|
|
// ```
|
|
// $localize `...`
|
|
// ```
|
|
// which is effectively like:
|
|
// ```
|
|
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
|
|
// ```
|
|
ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`);
|
|
const parts = [ast.serializeI18nHead()];
|
|
for (let i = 1; i < ast.messageParts.length; i++) {
|
|
parts.push(ast.serializeI18nTemplatePart(i));
|
|
}
|
|
ctx.print(ast, `[${parts.map((part) => escapeIdentifier(part.cooked, false)).join(', ')}], `);
|
|
ctx.print(ast, `[${parts.map((part) => escapeIdentifier(part.raw, false)).join(', ')}])`);
|
|
ast.expressions.forEach((expression) => {
|
|
ctx.print(ast, ', ');
|
|
expression.visitExpression(this, ctx);
|
|
});
|
|
ctx.print(ast, ')');
|
|
return null;
|
|
}
|
|
_visitParams(params, ctx) {
|
|
this.visitAllObjects((param) => ctx.print(null, param.name), params, ctx, ',');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @fileoverview
|
|
* A module to facilitate use of a Trusted Types policy within the JIT
|
|
* compiler. It lazily constructs the Trusted Types policy, providing helper
|
|
* utilities for promoting strings to Trusted Types. When Trusted Types are not
|
|
* available, strings are used as a fallback.
|
|
* @security All use of this module is security-sensitive and should go through
|
|
* security review.
|
|
*/
|
|
/**
|
|
* The Trusted Types policy, or null if Trusted Types are not
|
|
* enabled/supported, or undefined if the policy has not been created yet.
|
|
*/
|
|
let policy;
|
|
/**
|
|
* Returns the Trusted Types policy, or null if Trusted Types are not
|
|
* enabled/supported. The first call to this function will create the policy.
|
|
*/
|
|
function getPolicy() {
|
|
if (policy === undefined) {
|
|
const trustedTypes = _global['trustedTypes'];
|
|
policy = null;
|
|
if (trustedTypes) {
|
|
try {
|
|
policy = trustedTypes.createPolicy('angular#unsafe-jit', {
|
|
createScript: (s) => s,
|
|
});
|
|
}
|
|
catch {
|
|
// trustedTypes.createPolicy throws if called with a name that is
|
|
// already registered, even in report-only mode. Until the API changes,
|
|
// catch the error not to break the applications functionally. In such
|
|
// cases, the code will fall back to using strings.
|
|
}
|
|
}
|
|
}
|
|
return policy;
|
|
}
|
|
/**
|
|
* Unsafely promote a string to a TrustedScript, falling back to strings when
|
|
* Trusted Types are not available.
|
|
* @security In particular, it must be assured that the provided string will
|
|
* never cause an XSS vulnerability if used in a context that will be
|
|
* interpreted and executed as a script by a browser, e.g. when calling eval.
|
|
*/
|
|
function trustedScriptFromString(script) {
|
|
return getPolicy()?.createScript(script) || script;
|
|
}
|
|
/**
|
|
* Unsafely call the Function constructor with the given string arguments.
|
|
* @security This is a security-sensitive function; any use of this function
|
|
* must go through security review. In particular, it must be assured that it
|
|
* is only called from the JIT compiler, as use in other code can lead to XSS
|
|
* vulnerabilities.
|
|
*/
|
|
function newTrustedFunctionForJIT(...args) {
|
|
if (!_global['trustedTypes']) {
|
|
// In environments that don't support Trusted Types, fall back to the most
|
|
// straightforward implementation:
|
|
return new Function(...args);
|
|
}
|
|
// Chrome currently does not support passing TrustedScript to the Function
|
|
// constructor. The following implements the workaround proposed on the page
|
|
// below, where the Chromium bug is also referenced:
|
|
// https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor
|
|
const fnArgs = args.slice(0, -1).join(',');
|
|
const fnBody = args[args.length - 1];
|
|
const body = `(function anonymous(${fnArgs}
|
|
) { ${fnBody}
|
|
})`;
|
|
// Using eval directly confuses the compiler and prevents this module from
|
|
// being stripped out of JS binaries even if not used. The global['eval']
|
|
// indirection fixes that.
|
|
const fn = _global['eval'](trustedScriptFromString(body));
|
|
if (fn.bind === undefined) {
|
|
// Workaround for a browser bug that only exists in Chrome 83, where passing
|
|
// a TrustedScript to eval just returns the TrustedScript back without
|
|
// evaluating it. In that case, fall back to the most straightforward
|
|
// implementation:
|
|
return new Function(...args);
|
|
}
|
|
// To completely mimic the behavior of calling "new Function", two more
|
|
// things need to happen:
|
|
// 1. Stringifying the resulting function should return its source code
|
|
fn.toString = () => body;
|
|
// 2. When calling the resulting function, `this` should refer to `global`
|
|
return fn.bind(_global);
|
|
// When Trusted Types support in Function constructors is widely available,
|
|
// the implementation of this function can be simplified to:
|
|
// return new Function(...args.map(a => trustedScriptFromString(a)));
|
|
}
|
|
|
|
/**
|
|
* A helper class to manage the evaluation of JIT generated code.
|
|
*/
|
|
class JitEvaluator {
|
|
/**
|
|
*
|
|
* @param sourceUrl The URL of the generated code.
|
|
* @param statements An array of Angular statement AST nodes to be evaluated.
|
|
* @param refResolver Resolves `o.ExternalReference`s into values.
|
|
* @param createSourceMaps If true then create a source-map for the generated code and include it
|
|
* inline as a source-map comment.
|
|
* @returns A map of all the variables in the generated code.
|
|
*/
|
|
evaluateStatements(sourceUrl, statements, refResolver, createSourceMaps) {
|
|
const converter = new JitEmitterVisitor(refResolver);
|
|
const ctx = EmitterVisitorContext.createRoot();
|
|
// Ensure generated code is in strict mode
|
|
if (statements.length > 0 && !isUseStrictStatement(statements[0])) {
|
|
statements = [literal('use strict').toStmt(), ...statements];
|
|
}
|
|
converter.visitAllStatements(statements, ctx);
|
|
converter.createReturnStmt(ctx);
|
|
return this.evaluateCode(sourceUrl, ctx, converter.getArgs(), createSourceMaps);
|
|
}
|
|
/**
|
|
* Evaluate a piece of JIT generated code.
|
|
* @param sourceUrl The URL of this generated code.
|
|
* @param ctx A context object that contains an AST of the code to be evaluated.
|
|
* @param vars A map containing the names and values of variables that the evaluated code might
|
|
* reference.
|
|
* @param createSourceMap If true then create a source-map for the generated code and include it
|
|
* inline as a source-map comment.
|
|
* @returns The result of evaluating the code.
|
|
*/
|
|
evaluateCode(sourceUrl, ctx, vars, createSourceMap) {
|
|
let fnBody = `"use strict";${ctx.toSource()}\n//# sourceURL=${sourceUrl}`;
|
|
const fnArgNames = [];
|
|
const fnArgValues = [];
|
|
for (const argName in vars) {
|
|
fnArgValues.push(vars[argName]);
|
|
fnArgNames.push(argName);
|
|
}
|
|
if (createSourceMap) {
|
|
// using `new Function(...)` generates a header, 1 line of no arguments, 2 lines otherwise
|
|
// E.g. ```
|
|
// function anonymous(a,b,c
|
|
// /**/) { ... }```
|
|
// We don't want to hard code this fact, so we auto detect it via an empty function first.
|
|
const emptyFn = newTrustedFunctionForJIT(...fnArgNames.concat('return null;')).toString();
|
|
const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1;
|
|
fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`;
|
|
}
|
|
const fn = newTrustedFunctionForJIT(...fnArgNames.concat(fnBody));
|
|
return this.executeFunction(fn, fnArgValues);
|
|
}
|
|
/**
|
|
* Execute a JIT generated function by calling it.
|
|
*
|
|
* This method can be overridden in tests to capture the functions that are generated
|
|
* by this `JitEvaluator` class.
|
|
*
|
|
* @param fn A function to execute.
|
|
* @param args The arguments to pass to the function being executed.
|
|
* @returns The return value of the executed function.
|
|
*/
|
|
executeFunction(fn, args) {
|
|
return fn(...args);
|
|
}
|
|
}
|
|
/**
|
|
* An Angular AST visitor that converts AST nodes into executable JavaScript code.
|
|
*/
|
|
class JitEmitterVisitor extends AbstractJsEmitterVisitor {
|
|
refResolver;
|
|
_evalArgNames = [];
|
|
_evalArgValues = [];
|
|
_evalExportedVars = [];
|
|
constructor(refResolver) {
|
|
super();
|
|
this.refResolver = refResolver;
|
|
}
|
|
createReturnStmt(ctx) {
|
|
const stmt = new ReturnStatement(new LiteralMapExpr(this._evalExportedVars.map((resultVar) => new LiteralMapEntry(resultVar, variable(resultVar), false))));
|
|
stmt.visitStatement(this, ctx);
|
|
}
|
|
getArgs() {
|
|
const result = {};
|
|
for (let i = 0; i < this._evalArgNames.length; i++) {
|
|
result[this._evalArgNames[i]] = this._evalArgValues[i];
|
|
}
|
|
return result;
|
|
}
|
|
visitExternalExpr(ast, ctx) {
|
|
this._emitReferenceToExternal(ast, this.refResolver.resolveExternalReference(ast.value), ctx);
|
|
return null;
|
|
}
|
|
visitWrappedNodeExpr(ast, ctx) {
|
|
this._emitReferenceToExternal(ast, ast.node, ctx);
|
|
return null;
|
|
}
|
|
visitDeclareVarStmt(stmt, ctx) {
|
|
if (stmt.hasModifier(StmtModifier.Exported)) {
|
|
this._evalExportedVars.push(stmt.name);
|
|
}
|
|
return super.visitDeclareVarStmt(stmt, ctx);
|
|
}
|
|
visitDeclareFunctionStmt(stmt, ctx) {
|
|
if (stmt.hasModifier(StmtModifier.Exported)) {
|
|
this._evalExportedVars.push(stmt.name);
|
|
}
|
|
return super.visitDeclareFunctionStmt(stmt, ctx);
|
|
}
|
|
_emitReferenceToExternal(ast, value, ctx) {
|
|
let id = this._evalArgValues.indexOf(value);
|
|
if (id === -1) {
|
|
id = this._evalArgValues.length;
|
|
this._evalArgValues.push(value);
|
|
const name = identifierName({ reference: value }) || 'val';
|
|
this._evalArgNames.push(`jit_${name}_${id}`);
|
|
}
|
|
ctx.print(ast, this._evalArgNames[id]);
|
|
}
|
|
}
|
|
function isUseStrictStatement(statement) {
|
|
return statement.isEquivalent(literal('use strict').toStmt());
|
|
}
|
|
|
|
function compileInjector(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
if (meta.providers !== null) {
|
|
definitionMap.set('providers', meta.providers);
|
|
}
|
|
if (meta.imports.length > 0) {
|
|
definitionMap.set('imports', literalArr(meta.imports));
|
|
}
|
|
const expression = importExpr(Identifiers.defineInjector)
|
|
.callFn([definitionMap.toLiteralMap()], undefined, true);
|
|
const type = createInjectorType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
function createInjectorType(meta) {
|
|
return new ExpressionType(importExpr(Identifiers.InjectorDeclaration, [new ExpressionType(meta.type.type)]));
|
|
}
|
|
|
|
/**
|
|
* Implementation of `CompileReflector` which resolves references to @angular/core
|
|
* symbols at runtime, according to a consumer-provided mapping.
|
|
*
|
|
* Only supports `resolveExternalReference`, all other methods throw.
|
|
*/
|
|
class R3JitReflector {
|
|
context;
|
|
constructor(context) {
|
|
this.context = context;
|
|
}
|
|
resolveExternalReference(ref) {
|
|
// This reflector only handles @angular/core imports.
|
|
if (ref.moduleName !== '@angular/core') {
|
|
throw new Error(`Cannot resolve external reference to ${ref.moduleName}, only references to @angular/core are supported.`);
|
|
}
|
|
if (!this.context.hasOwnProperty(ref.name)) {
|
|
throw new Error(`No value provided for @angular/core symbol '${ref.name}'.`);
|
|
}
|
|
return this.context[ref.name];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* How the selector scope of an NgModule (its declarations, imports, and exports) should be emitted
|
|
* as a part of the NgModule definition.
|
|
*/
|
|
var R3SelectorScopeMode;
|
|
(function (R3SelectorScopeMode) {
|
|
/**
|
|
* Emit the declarations inline into the module definition.
|
|
*
|
|
* This option is useful in certain contexts where it's known that JIT support is required. The
|
|
* tradeoff here is that this emit style prevents directives and pipes from being tree-shaken if
|
|
* they are unused, but the NgModule is used.
|
|
*/
|
|
R3SelectorScopeMode[R3SelectorScopeMode["Inline"] = 0] = "Inline";
|
|
/**
|
|
* Emit the declarations using a side effectful function call, `ɵɵsetNgModuleScope`, that is
|
|
* guarded with the `ngJitMode` flag.
|
|
*
|
|
* This form of emit supports JIT and can be optimized away if the `ngJitMode` flag is set to
|
|
* false, which allows unused directives and pipes to be tree-shaken.
|
|
*/
|
|
R3SelectorScopeMode[R3SelectorScopeMode["SideEffect"] = 1] = "SideEffect";
|
|
/**
|
|
* Don't generate selector scopes at all.
|
|
*
|
|
* This is useful for contexts where JIT support is known to be unnecessary.
|
|
*/
|
|
R3SelectorScopeMode[R3SelectorScopeMode["Omit"] = 2] = "Omit";
|
|
})(R3SelectorScopeMode || (R3SelectorScopeMode = {}));
|
|
/**
|
|
* The type of the NgModule meta data.
|
|
* - Global: Used for full and partial compilation modes which mainly includes R3References.
|
|
* - Local: Used for the local compilation mode which mainly includes the raw expressions as appears
|
|
* in the NgModule decorator.
|
|
*/
|
|
var R3NgModuleMetadataKind;
|
|
(function (R3NgModuleMetadataKind) {
|
|
R3NgModuleMetadataKind[R3NgModuleMetadataKind["Global"] = 0] = "Global";
|
|
R3NgModuleMetadataKind[R3NgModuleMetadataKind["Local"] = 1] = "Local";
|
|
})(R3NgModuleMetadataKind || (R3NgModuleMetadataKind = {}));
|
|
/**
|
|
* Construct an `R3NgModuleDef` for the given `R3NgModuleMetadata`.
|
|
*/
|
|
function compileNgModule(meta) {
|
|
const statements = [];
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('type', meta.type.value);
|
|
// Assign bootstrap definition. In local compilation mode (i.e., for
|
|
// `R3NgModuleMetadataKind.LOCAL`) we assign the bootstrap field using the runtime
|
|
// `ɵɵsetNgModuleScope`.
|
|
if (meta.kind === R3NgModuleMetadataKind.Global && meta.bootstrap.length > 0) {
|
|
definitionMap.set('bootstrap', refsToArray(meta.bootstrap, meta.containsForwardDecls));
|
|
}
|
|
if (meta.selectorScopeMode === R3SelectorScopeMode.Inline) {
|
|
// If requested to emit scope information inline, pass the `declarations`, `imports` and
|
|
// `exports` to the `ɵɵdefineNgModule()` call directly.
|
|
if (meta.declarations.length > 0) {
|
|
definitionMap.set('declarations', refsToArray(meta.declarations, meta.containsForwardDecls));
|
|
}
|
|
if (meta.imports.length > 0) {
|
|
definitionMap.set('imports', refsToArray(meta.imports, meta.containsForwardDecls));
|
|
}
|
|
if (meta.exports.length > 0) {
|
|
definitionMap.set('exports', refsToArray(meta.exports, meta.containsForwardDecls));
|
|
}
|
|
}
|
|
else if (meta.selectorScopeMode === R3SelectorScopeMode.SideEffect) {
|
|
// In this mode, scope information is not passed into `ɵɵdefineNgModule` as it
|
|
// would prevent tree-shaking of the declarations, imports and exports references. Instead, it's
|
|
// patched onto the NgModule definition with a `ɵɵsetNgModuleScope` call that's guarded by the
|
|
// `ngJitMode` flag.
|
|
const setNgModuleScopeCall = generateSetNgModuleScopeCall(meta);
|
|
if (setNgModuleScopeCall !== null) {
|
|
statements.push(setNgModuleScopeCall);
|
|
}
|
|
}
|
|
else ;
|
|
if (meta.schemas !== null && meta.schemas.length > 0) {
|
|
definitionMap.set('schemas', literalArr(meta.schemas.map((ref) => ref.value)));
|
|
}
|
|
if (meta.id !== null) {
|
|
definitionMap.set('id', meta.id);
|
|
// Generate a side-effectful call to register this NgModule by its id, as per the semantics of
|
|
// NgModule ids.
|
|
statements.push(importExpr(Identifiers.registerNgModuleType).callFn([meta.type.value, meta.id]).toStmt());
|
|
}
|
|
const expression = importExpr(Identifiers.defineNgModule)
|
|
.callFn([definitionMap.toLiteralMap()], undefined, true);
|
|
const type = createNgModuleType(meta);
|
|
return { expression, type, statements };
|
|
}
|
|
/**
|
|
* This function is used in JIT mode to generate the call to `ɵɵdefineNgModule()` from a call to
|
|
* `ɵɵngDeclareNgModule()`.
|
|
*/
|
|
function compileNgModuleDeclarationExpression(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('type', new WrappedNodeExpr(meta.type));
|
|
if (meta.bootstrap !== undefined) {
|
|
definitionMap.set('bootstrap', new WrappedNodeExpr(meta.bootstrap));
|
|
}
|
|
if (meta.declarations !== undefined) {
|
|
definitionMap.set('declarations', new WrappedNodeExpr(meta.declarations));
|
|
}
|
|
if (meta.imports !== undefined) {
|
|
definitionMap.set('imports', new WrappedNodeExpr(meta.imports));
|
|
}
|
|
if (meta.exports !== undefined) {
|
|
definitionMap.set('exports', new WrappedNodeExpr(meta.exports));
|
|
}
|
|
if (meta.schemas !== undefined) {
|
|
definitionMap.set('schemas', new WrappedNodeExpr(meta.schemas));
|
|
}
|
|
if (meta.id !== undefined) {
|
|
definitionMap.set('id', new WrappedNodeExpr(meta.id));
|
|
}
|
|
return importExpr(Identifiers.defineNgModule).callFn([definitionMap.toLiteralMap()]);
|
|
}
|
|
function createNgModuleType(meta) {
|
|
if (meta.kind === R3NgModuleMetadataKind.Local) {
|
|
return new ExpressionType(meta.type.value);
|
|
}
|
|
const { type: moduleType, declarations, exports, imports, includeImportTypes, publicDeclarationTypes, } = meta;
|
|
return new ExpressionType(importExpr(Identifiers.NgModuleDeclaration, [
|
|
new ExpressionType(moduleType.type),
|
|
publicDeclarationTypes === null
|
|
? tupleTypeOf(declarations)
|
|
: tupleOfTypes(publicDeclarationTypes),
|
|
includeImportTypes ? tupleTypeOf(imports) : NONE_TYPE,
|
|
tupleTypeOf(exports),
|
|
]));
|
|
}
|
|
/**
|
|
* Generates a function call to `ɵɵsetNgModuleScope` with all necessary information so that the
|
|
* transitive module scope can be computed during runtime in JIT mode. This call is marked pure
|
|
* such that the references to declarations, imports and exports may be elided causing these
|
|
* symbols to become tree-shakeable.
|
|
*/
|
|
function generateSetNgModuleScopeCall(meta) {
|
|
const scopeMap = new DefinitionMap();
|
|
if (meta.kind === R3NgModuleMetadataKind.Global) {
|
|
if (meta.declarations.length > 0) {
|
|
scopeMap.set('declarations', refsToArray(meta.declarations, meta.containsForwardDecls));
|
|
}
|
|
}
|
|
else {
|
|
if (meta.declarationsExpression) {
|
|
scopeMap.set('declarations', meta.declarationsExpression);
|
|
}
|
|
}
|
|
if (meta.kind === R3NgModuleMetadataKind.Global) {
|
|
if (meta.imports.length > 0) {
|
|
scopeMap.set('imports', refsToArray(meta.imports, meta.containsForwardDecls));
|
|
}
|
|
}
|
|
else {
|
|
if (meta.importsExpression) {
|
|
scopeMap.set('imports', meta.importsExpression);
|
|
}
|
|
}
|
|
if (meta.kind === R3NgModuleMetadataKind.Global) {
|
|
if (meta.exports.length > 0) {
|
|
scopeMap.set('exports', refsToArray(meta.exports, meta.containsForwardDecls));
|
|
}
|
|
}
|
|
else {
|
|
if (meta.exportsExpression) {
|
|
scopeMap.set('exports', meta.exportsExpression);
|
|
}
|
|
}
|
|
if (meta.kind === R3NgModuleMetadataKind.Local && meta.bootstrapExpression) {
|
|
scopeMap.set('bootstrap', meta.bootstrapExpression);
|
|
}
|
|
if (Object.keys(scopeMap.values).length === 0) {
|
|
return null;
|
|
}
|
|
// setNgModuleScope(...)
|
|
const fnCall = new InvokeFunctionExpr(
|
|
/* fn */ importExpr(Identifiers.setNgModuleScope),
|
|
/* args */ [meta.type.value, scopeMap.toLiteralMap()]);
|
|
// (ngJitMode guard) && setNgModuleScope(...)
|
|
const guardedCall = jitOnlyGuardedExpression(fnCall);
|
|
// function() { (ngJitMode guard) && setNgModuleScope(...); }
|
|
const iife = new FunctionExpr(/* params */ [], /* statements */ [guardedCall.toStmt()]);
|
|
// (function() { (ngJitMode guard) && setNgModuleScope(...); })()
|
|
const iifeCall = new InvokeFunctionExpr(/* fn */ iife, /* args */ []);
|
|
return iifeCall.toStmt();
|
|
}
|
|
function tupleTypeOf(exp) {
|
|
const types = exp.map((ref) => typeofExpr(ref.type));
|
|
return exp.length > 0 ? expressionType(literalArr(types)) : NONE_TYPE;
|
|
}
|
|
function tupleOfTypes(types) {
|
|
const typeofTypes = types.map((type) => typeofExpr(type));
|
|
return types.length > 0 ? expressionType(literalArr(typeofTypes)) : NONE_TYPE;
|
|
}
|
|
|
|
function compilePipeFromMetadata(metadata) {
|
|
const definitionMapValues = [];
|
|
// e.g. `name: 'myPipe'`
|
|
definitionMapValues.push({
|
|
key: 'name',
|
|
value: literal(metadata.pipeName ?? metadata.name),
|
|
quoted: false,
|
|
});
|
|
// e.g. `type: MyPipe`
|
|
definitionMapValues.push({ key: 'type', value: metadata.type.value, quoted: false });
|
|
// e.g. `pure: true`
|
|
definitionMapValues.push({ key: 'pure', value: literal(metadata.pure), quoted: false });
|
|
if (metadata.isStandalone === false) {
|
|
definitionMapValues.push({ key: 'standalone', value: literal(false), quoted: false });
|
|
}
|
|
const expression = importExpr(Identifiers.definePipe)
|
|
.callFn([literalMap(definitionMapValues)], undefined, true);
|
|
const type = createPipeType(metadata);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
function createPipeType(metadata) {
|
|
return new ExpressionType(importExpr(Identifiers.PipeDeclaration, [
|
|
typeWithParameters(metadata.type.type, metadata.typeArgumentCount),
|
|
new ExpressionType(new LiteralExpr(metadata.pipeName)),
|
|
new ExpressionType(new LiteralExpr(metadata.isStandalone)),
|
|
]));
|
|
}
|
|
|
|
var R3TemplateDependencyKind;
|
|
(function (R3TemplateDependencyKind) {
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["Directive"] = 0] = "Directive";
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["Pipe"] = 1] = "Pipe";
|
|
R3TemplateDependencyKind[R3TemplateDependencyKind["NgModule"] = 2] = "NgModule";
|
|
})(R3TemplateDependencyKind || (R3TemplateDependencyKind = {}));
|
|
|
|
/**
|
|
* The following set contains all keywords that can be used in the animation css shorthand
|
|
* property and is used during the scoping of keyframes to make sure such keywords
|
|
* are not modified.
|
|
*/
|
|
const animationKeywords = new Set([
|
|
// global values
|
|
'inherit',
|
|
'initial',
|
|
'revert',
|
|
'unset',
|
|
// animation-direction
|
|
'alternate',
|
|
'alternate-reverse',
|
|
'normal',
|
|
'reverse',
|
|
// animation-fill-mode
|
|
'backwards',
|
|
'both',
|
|
'forwards',
|
|
'none',
|
|
// animation-play-state
|
|
'paused',
|
|
'running',
|
|
// animation-timing-function
|
|
'ease',
|
|
'ease-in',
|
|
'ease-in-out',
|
|
'ease-out',
|
|
'linear',
|
|
'step-start',
|
|
'step-end',
|
|
// `steps()` function
|
|
'end',
|
|
'jump-both',
|
|
'jump-end',
|
|
'jump-none',
|
|
'jump-start',
|
|
'start',
|
|
]);
|
|
/**
|
|
* The following array contains all of the CSS at-rule identifiers which are scoped.
|
|
*/
|
|
const scopedAtRuleIdentifiers = [
|
|
'@media',
|
|
'@supports',
|
|
'@document',
|
|
'@layer',
|
|
'@container',
|
|
'@scope',
|
|
'@starting-style',
|
|
];
|
|
/**
|
|
* The following class has its origin from a port of shadowCSS from webcomponents.js to TypeScript.
|
|
* It has since diverge in many ways to tailor Angular's needs.
|
|
*
|
|
* Source:
|
|
* https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
|
|
*
|
|
* The original file level comment is reproduced below
|
|
*/
|
|
/*
|
|
This is a limited shim for ShadowDOM css styling.
|
|
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
|
|
|
|
The intention here is to support only the styling features which can be
|
|
relatively simply implemented. The goal is to allow users to avoid the
|
|
most obvious pitfalls and do so without compromising performance significantly.
|
|
For ShadowDOM styling that's not covered here, a set of best practices
|
|
can be provided that should allow users to accomplish more complex styling.
|
|
|
|
The following is a list of specific ShadowDOM styling features and a brief
|
|
discussion of the approach used to shim.
|
|
|
|
Shimmed features:
|
|
|
|
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
|
|
element using the :host rule. To shim this feature, the :host styles are
|
|
reformatted and prefixed with a given scope name and promoted to a
|
|
document level stylesheet.
|
|
For example, given a scope name of .foo, a rule like this:
|
|
|
|
:host {
|
|
background: red;
|
|
}
|
|
}
|
|
|
|
becomes:
|
|
|
|
.foo {
|
|
background: red;
|
|
}
|
|
|
|
* encapsulation: Styles defined within ShadowDOM, apply only to
|
|
dom inside the ShadowDOM.
|
|
The selectors are scoped by adding an attribute selector suffix to each
|
|
simple selector that contains the host element tag name. Each element
|
|
in the element's ShadowDOM template is also given the scope attribute.
|
|
Thus, these rules match only elements that have the scope attribute.
|
|
For example, given a scope name of x-foo, a rule like this:
|
|
|
|
div {
|
|
font-weight: bold;
|
|
}
|
|
|
|
becomes:
|
|
|
|
div[x-foo] {
|
|
font-weight: bold;
|
|
}
|
|
|
|
Note that elements that are dynamically added to a scope must have the scope
|
|
selector added to them manually.
|
|
|
|
* upper/lower bound encapsulation: Styles which are defined outside a
|
|
shadowRoot should not cross the ShadowDOM boundary and should not apply
|
|
inside a shadowRoot.
|
|
|
|
This styling behavior is not emulated. Some possible ways to do this that
|
|
were rejected due to complexity and/or performance concerns include: (1) reset
|
|
every possible property for every possible selector for a given scope name;
|
|
(2) re-implement css in javascript.
|
|
|
|
As an alternative, users should make sure to use selectors
|
|
specific to the scope in which they are working.
|
|
|
|
* ::distributed: This behavior is not emulated. It's often not necessary
|
|
to style the contents of a specific insertion point and instead, descendants
|
|
of the host element can be styled selectively. Users can also create an
|
|
extra node around an insertion point and style that node's contents
|
|
via descendent selectors. For example, with a shadowRoot like this:
|
|
|
|
<style>
|
|
::content(div) {
|
|
background: red;
|
|
}
|
|
</style>
|
|
<content></content>
|
|
|
|
could become:
|
|
|
|
<style>
|
|
/ *@polyfill .content-container div * /
|
|
::content(div) {
|
|
background: red;
|
|
}
|
|
</style>
|
|
<div class="content-container">
|
|
<content></content>
|
|
</div>
|
|
|
|
Note the use of @polyfill in the comment above a ShadowDOM specific style
|
|
declaration. This is a directive to the styling shim to use the selector
|
|
in comments in lieu of the next selector when running under polyfill.
|
|
*/
|
|
class ShadowCss {
|
|
/*
|
|
* Shim some cssText with the given selector. Returns cssText that can be included in the document
|
|
*
|
|
* The selector is the attribute added to all elements inside the host,
|
|
* The hostSelector is the attribute added to the host itself.
|
|
*/
|
|
shimCssText(cssText, selector, hostSelector = '') {
|
|
// **NOTE**: Do not strip comments as this will cause component sourcemaps to break
|
|
// due to shift in lines.
|
|
// Collect comments and replace them with a placeholder, this is done to avoid complicating
|
|
// the rule parsing RegExp and keep it safer.
|
|
const comments = [];
|
|
cssText = cssText.replace(_commentRe, (m) => {
|
|
if (m.match(_commentWithHashRe)) {
|
|
comments.push(m);
|
|
}
|
|
else {
|
|
// Replace non hash comments with empty lines.
|
|
// This is done so that we do not leak any sensitive data in comments.
|
|
const newLinesMatches = m.match(_newLinesRe);
|
|
comments.push((newLinesMatches?.join('') ?? '') + '\n');
|
|
}
|
|
return COMMENT_PLACEHOLDER;
|
|
});
|
|
cssText = this._insertDirectives(cssText);
|
|
const scopedCssText = this._scopeCssText(cssText, selector, hostSelector);
|
|
// Add back comments at the original position.
|
|
let commentIdx = 0;
|
|
return scopedCssText.replace(_commentWithHashPlaceHolderRe, () => comments[commentIdx++]);
|
|
}
|
|
_insertDirectives(cssText) {
|
|
cssText = this._insertPolyfillDirectivesInCssText(cssText);
|
|
return this._insertPolyfillRulesInCssText(cssText);
|
|
}
|
|
/**
|
|
* Process styles to add scope to keyframes.
|
|
*
|
|
* Modify both the names of the keyframes defined in the component styles and also the css
|
|
* animation rules using them.
|
|
*
|
|
* Animation rules using keyframes defined elsewhere are not modified to allow for globally
|
|
* defined keyframes.
|
|
*
|
|
* For example, we convert this css:
|
|
*
|
|
* ```scss
|
|
* .box {
|
|
* animation: box-animation 1s forwards;
|
|
* }
|
|
*
|
|
* @keyframes box-animation {
|
|
* to {
|
|
* background-color: green;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* to this:
|
|
*
|
|
* ```scss
|
|
* .box {
|
|
* animation: scopeName_box-animation 1s forwards;
|
|
* }
|
|
*
|
|
* @keyframes scopeName_box-animation {
|
|
* to {
|
|
* background-color: green;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param cssText the component's css text that needs to be scoped.
|
|
* @param scopeSelector the component's scope selector.
|
|
*
|
|
* @returns the scoped css text.
|
|
*/
|
|
_scopeKeyframesRelatedCss(cssText, scopeSelector) {
|
|
const unscopedKeyframesSet = new Set();
|
|
const scopedKeyframesCssText = processRules(cssText, (rule) => this._scopeLocalKeyframeDeclarations(rule, scopeSelector, unscopedKeyframesSet));
|
|
return processRules(scopedKeyframesCssText, (rule) => this._scopeAnimationRule(rule, scopeSelector, unscopedKeyframesSet));
|
|
}
|
|
/**
|
|
* Scopes local keyframes names, returning the updated css rule and it also
|
|
* adds the original keyframe name to a provided set to collect all keyframes names
|
|
* so that it can later be used to scope the animation rules.
|
|
*
|
|
* For example, it takes a rule such as:
|
|
*
|
|
* ```scss
|
|
* @keyframes box-animation {
|
|
* to {
|
|
* background-color: green;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* and returns:
|
|
*
|
|
* ```scss
|
|
* @keyframes scopeName_box-animation {
|
|
* to {
|
|
* background-color: green;
|
|
* }
|
|
* }
|
|
* ```
|
|
* and as a side effect it adds "box-animation" to the `unscopedKeyframesSet` set
|
|
*
|
|
* @param cssRule the css rule to process.
|
|
* @param scopeSelector the component's scope selector.
|
|
* @param unscopedKeyframesSet the set of unscoped keyframes names (which can be
|
|
* modified as a side effect)
|
|
*
|
|
* @returns the css rule modified with the scoped keyframes name.
|
|
*/
|
|
_scopeLocalKeyframeDeclarations(rule, scopeSelector, unscopedKeyframesSet) {
|
|
return {
|
|
...rule,
|
|
selector: rule.selector.replace(/(^@(?:-webkit-)?keyframes(?:\s+))(['"]?)(.+)\2(\s*)$/, (_, start, quote, keyframeName, endSpaces) => {
|
|
unscopedKeyframesSet.add(unescapeQuotes(keyframeName, quote));
|
|
return `${start}${quote}${scopeSelector}_${keyframeName}${quote}${endSpaces}`;
|
|
}),
|
|
};
|
|
}
|
|
/**
|
|
* Function used to scope a keyframes name (obtained from an animation declaration)
|
|
* using an existing set of unscopedKeyframes names to discern if the scoping needs to be
|
|
* performed (keyframes names of keyframes not defined in the component's css need not to be
|
|
* scoped).
|
|
*
|
|
* @param keyframe the keyframes name to check.
|
|
* @param scopeSelector the component's scope selector.
|
|
* @param unscopedKeyframesSet the set of unscoped keyframes names.
|
|
*
|
|
* @returns the scoped name of the keyframe, or the original name is the name need not to be
|
|
* scoped.
|
|
*/
|
|
_scopeAnimationKeyframe(keyframe, scopeSelector, unscopedKeyframesSet) {
|
|
return keyframe.replace(/^(\s*)(['"]?)(.+?)\2(\s*)$/, (_, spaces1, quote, name, spaces2) => {
|
|
name = `${unscopedKeyframesSet.has(unescapeQuotes(name, quote)) ? scopeSelector + '_' : ''}${name}`;
|
|
return `${spaces1}${quote}${name}${quote}${spaces2}`;
|
|
});
|
|
}
|
|
/**
|
|
* Regular expression used to extrapolate the possible keyframes from an
|
|
* animation declaration (with possibly multiple animation definitions)
|
|
*
|
|
* The regular expression can be divided in three parts
|
|
* - (^|\s+|,)
|
|
* captures how many (if any) leading whitespaces are present or a comma
|
|
* - (?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))
|
|
* captures two different possible keyframes, ones which are quoted or ones which are valid css
|
|
* indents (custom properties excluded)
|
|
* - (?=[,\s;]|$)
|
|
* simply matches the end of the possible keyframe, valid endings are: a comma, a space, a
|
|
* semicolon or the end of the string
|
|
*/
|
|
_animationDeclarationKeyframesRe = /(^|\s+|,)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;
|
|
/**
|
|
* Scope an animation rule so that the keyframes mentioned in such rule
|
|
* are scoped if defined in the component's css and left untouched otherwise.
|
|
*
|
|
* It can scope values of both the 'animation' and 'animation-name' properties.
|
|
*
|
|
* @param rule css rule to scope.
|
|
* @param scopeSelector the component's scope selector.
|
|
* @param unscopedKeyframesSet the set of unscoped keyframes names.
|
|
*
|
|
* @returns the updated css rule.
|
|
**/
|
|
_scopeAnimationRule(rule, scopeSelector, unscopedKeyframesSet) {
|
|
let content = rule.content.replace(/((?:^|\s+|;)(?:-webkit-)?animation\s*:\s*),*([^;]+)/g, (_, start, animationDeclarations) => start +
|
|
animationDeclarations.replace(this._animationDeclarationKeyframesRe, (original, leadingSpaces, quote = '', quotedName, nonQuotedName) => {
|
|
if (quotedName) {
|
|
return `${leadingSpaces}${this._scopeAnimationKeyframe(`${quote}${quotedName}${quote}`, scopeSelector, unscopedKeyframesSet)}`;
|
|
}
|
|
else {
|
|
return animationKeywords.has(nonQuotedName)
|
|
? original
|
|
: `${leadingSpaces}${this._scopeAnimationKeyframe(nonQuotedName, scopeSelector, unscopedKeyframesSet)}`;
|
|
}
|
|
}));
|
|
content = content.replace(/((?:^|\s+|;)(?:-webkit-)?animation-name(?:\s*):(?:\s*))([^;]+)/g, (_match, start, commaSeparatedKeyframes) => `${start}${commaSeparatedKeyframes
|
|
.split(',')
|
|
.map((keyframe) => this._scopeAnimationKeyframe(keyframe, scopeSelector, unscopedKeyframesSet))
|
|
.join(',')}`);
|
|
return { ...rule, content };
|
|
}
|
|
/*
|
|
* Process styles to convert native ShadowDOM rules that will trip
|
|
* up the css parser; we rely on decorating the stylesheet with inert rules.
|
|
*
|
|
* For example, we convert this rule:
|
|
*
|
|
* polyfill-next-selector { content: ':host menu-item'; }
|
|
* ::content menu-item {
|
|
*
|
|
* to this:
|
|
*
|
|
* scopeName menu-item {
|
|
*
|
|
**/
|
|
_insertPolyfillDirectivesInCssText(cssText) {
|
|
return cssText.replace(_cssContentNextSelectorRe, function (...m) {
|
|
return m[2] + '{';
|
|
});
|
|
}
|
|
/*
|
|
* Process styles to add rules which will only apply under the polyfill
|
|
*
|
|
* For example, we convert this rule:
|
|
*
|
|
* polyfill-rule {
|
|
* content: ':host menu-item';
|
|
* ...
|
|
* }
|
|
*
|
|
* to this:
|
|
*
|
|
* scopeName menu-item {...}
|
|
*
|
|
**/
|
|
_insertPolyfillRulesInCssText(cssText) {
|
|
return cssText.replace(_cssContentRuleRe, (...m) => {
|
|
const rule = m[0].replace(m[1], '').replace(m[2], '');
|
|
return m[4] + rule;
|
|
});
|
|
}
|
|
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
|
|
*
|
|
* .foo {... }
|
|
*
|
|
* and converts this to
|
|
*
|
|
* scopeName .foo { ... }
|
|
*/
|
|
_scopeCssText(cssText, scopeSelector, hostSelector) {
|
|
const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
|
|
// replace :host and :host-context with -shadowcsshost and -shadowcsshostcontext respectively
|
|
cssText = this._insertPolyfillHostInCssText(cssText);
|
|
cssText = this._convertColonHost(cssText);
|
|
cssText = this._convertColonHostContext(cssText);
|
|
cssText = this._convertShadowDOMSelectors(cssText);
|
|
if (scopeSelector) {
|
|
cssText = this._scopeKeyframesRelatedCss(cssText, scopeSelector);
|
|
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
|
|
}
|
|
cssText = cssText + '\n' + unscopedRules;
|
|
return cssText.trim();
|
|
}
|
|
/*
|
|
* Process styles to add rules which will only apply under the polyfill
|
|
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
|
|
* occasions, e.g. -webkit-calc on Safari.)
|
|
* For example, we convert this rule:
|
|
*
|
|
* @polyfill-unscoped-rule {
|
|
* content: 'menu-item';
|
|
* ... }
|
|
*
|
|
* to this:
|
|
*
|
|
* menu-item {...}
|
|
*
|
|
**/
|
|
_extractUnscopedRulesFromCssText(cssText) {
|
|
let r = '';
|
|
let m;
|
|
_cssContentUnscopedRuleRe.lastIndex = 0;
|
|
while ((m = _cssContentUnscopedRuleRe.exec(cssText)) !== null) {
|
|
const rule = m[0].replace(m[2], '').replace(m[1], m[4]);
|
|
r += rule + '\n\n';
|
|
}
|
|
return r;
|
|
}
|
|
/*
|
|
* convert a rule like :host(.foo) > .bar { }
|
|
*
|
|
* to
|
|
*
|
|
* .foo<scopeName> > .bar
|
|
*/
|
|
_convertColonHost(cssText) {
|
|
return cssText.replace(_cssColonHostRe, (_, hostSelectors, otherSelectors) => {
|
|
if (hostSelectors) {
|
|
const convertedSelectors = [];
|
|
for (const hostSelector of this._splitOnTopLevelCommas(hostSelectors)) {
|
|
const trimmedHostSelector = hostSelector.trim();
|
|
if (!trimmedHostSelector)
|
|
break;
|
|
const convertedSelector = _polyfillHostNoCombinator +
|
|
trimmedHostSelector.replace(_polyfillHost, '') +
|
|
otherSelectors;
|
|
convertedSelectors.push(convertedSelector);
|
|
}
|
|
return convertedSelectors.join(',');
|
|
}
|
|
else {
|
|
return _polyfillHostNoCombinator + otherSelectors;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Generator function that splits a string on top-level commas (commas that are not inside parentheses).
|
|
* Yields each part of the string between top-level commas. Terminates if an extra closing paren is found.
|
|
*
|
|
* @param text The string to split
|
|
*/
|
|
*_splitOnTopLevelCommas(text) {
|
|
const length = text.length;
|
|
let parens = 0;
|
|
let prev = 0;
|
|
for (let i = 0; i < length; i++) {
|
|
const charCode = text.charCodeAt(i);
|
|
if (charCode === $LPAREN) {
|
|
parens++;
|
|
}
|
|
else if (charCode === $RPAREN) {
|
|
parens--;
|
|
if (parens < 0) {
|
|
// Found an extra closing paren. Assume we want the list terminated here
|
|
yield text.slice(prev, i);
|
|
return;
|
|
}
|
|
}
|
|
else if (charCode === $COMMA && parens === 0) {
|
|
// Found a top-level comma, yield the current chunk
|
|
yield text.slice(prev, i);
|
|
prev = i + 1;
|
|
}
|
|
}
|
|
// Yield the final chunk
|
|
yield text.slice(prev);
|
|
}
|
|
/*
|
|
* convert a rule like :host-context(.foo) > .bar { }
|
|
*
|
|
* to
|
|
*
|
|
* .foo<scopeName> > .bar, .foo <scopeName> > .bar { }
|
|
*
|
|
* and
|
|
*
|
|
* :host-context(.foo:host) .bar { ... }
|
|
*
|
|
* to
|
|
*
|
|
* .foo<scopeName> .bar { ... }
|
|
*/
|
|
_convertColonHostContext(cssText) {
|
|
// Splits up the selectors on their top-level commas, processes the :host-context in them
|
|
// individually and stitches them back together. This ensures that individual selectors don't
|
|
// affect each other.
|
|
const results = [];
|
|
for (const part of this._splitOnTopLevelCommas(cssText)) {
|
|
results.push(this._convertColonHostContextInSelectorPart(part));
|
|
}
|
|
return results.join(',');
|
|
}
|
|
_convertColonHostContextInSelectorPart(cssText) {
|
|
return cssText.replace(_cssColonHostContextReGlobal, (selectorText, pseudoPrefix) => {
|
|
// We have captured a selector that contains a `:host-context` rule.
|
|
// For backward compatibility `:host-context` may contain a comma separated list of selectors.
|
|
// Each context selector group will contain a list of host-context selectors that must match
|
|
// an ancestor of the host.
|
|
// (Normally `contextSelectorGroups` will only contain a single array of context selectors.)
|
|
const contextSelectorGroups = [[]];
|
|
// There may be more than `:host-context` in this selector so `selectorText` could look like:
|
|
// `:host-context(.one):host-context(.two)`.
|
|
// Loop until every :host-context in the compound selector has been processed.
|
|
let startIndex = selectorText.indexOf(_polyfillHostContext);
|
|
while (startIndex !== -1) {
|
|
const afterPrefix = selectorText.substring(startIndex + _polyfillHostContext.length);
|
|
if (!afterPrefix || afterPrefix[0] !== '(') {
|
|
// Edge case of :host-context with no parens (e.g. `:host-context .inner`)
|
|
selectorText = afterPrefix;
|
|
startIndex = selectorText.indexOf(_polyfillHostContext);
|
|
continue;
|
|
}
|
|
// Extract comma-separated selectors between the parentheses
|
|
const newContextSelectors = [];
|
|
let endIndex = 0; // Index of the closing paren of the :host-context()
|
|
for (const selector of this._splitOnTopLevelCommas(afterPrefix.substring(1))) {
|
|
endIndex = endIndex + selector.length + 1;
|
|
const trimmed = selector.trim();
|
|
if (trimmed) {
|
|
newContextSelectors.push(trimmed);
|
|
}
|
|
}
|
|
// We must duplicate the current selector group for each of these new selectors.
|
|
// For example if the current groups are:
|
|
// ```
|
|
// [
|
|
// ['a', 'b', 'c'],
|
|
// ['x', 'y', 'z'],
|
|
// ]
|
|
// ```
|
|
// And we have a new set of comma separated selectors: `:host-context(m,n)` then the new
|
|
// groups are:
|
|
// ```
|
|
// [
|
|
// ['a', 'b', 'c', 'm'],
|
|
// ['x', 'y', 'z', 'm'],
|
|
// ['a', 'b', 'c', 'n'],
|
|
// ['x', 'y', 'z', 'n'],
|
|
// ]
|
|
// ```
|
|
const contextSelectorGroupsLength = contextSelectorGroups.length;
|
|
repeatGroups(contextSelectorGroups, newContextSelectors.length);
|
|
for (let i = 0; i < newContextSelectors.length; i++) {
|
|
for (let j = 0; j < contextSelectorGroupsLength; j++) {
|
|
contextSelectorGroups[j + i * contextSelectorGroupsLength].push(newContextSelectors[i]);
|
|
}
|
|
}
|
|
// Update the `selectorText` and see repeat to see if there are more `:host-context`s.
|
|
selectorText = afterPrefix.substring(endIndex + 1);
|
|
startIndex = selectorText.indexOf(_polyfillHostContext);
|
|
}
|
|
// The context selectors now must be combined with each other to capture all the possible
|
|
// selectors that `:host-context` can match. See `_combineHostContextSelectors()` for more
|
|
// info about how this is done.
|
|
return contextSelectorGroups
|
|
.map((contextSelectors) => _combineHostContextSelectors(contextSelectors, selectorText, pseudoPrefix))
|
|
.join(', ');
|
|
});
|
|
}
|
|
/*
|
|
* Convert combinators like ::shadow and pseudo-elements like ::content
|
|
* by replacing with space.
|
|
*/
|
|
_convertShadowDOMSelectors(cssText) {
|
|
return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
|
|
}
|
|
// change a selector like 'div' to 'name div'
|
|
_scopeSelectors(cssText, scopeSelector, hostSelector) {
|
|
return processRules(cssText, (rule) => {
|
|
let selector = rule.selector;
|
|
let content = rule.content;
|
|
if (rule.selector[0] !== '@') {
|
|
selector = this._scopeSelector({
|
|
selector,
|
|
scopeSelector,
|
|
hostSelector,
|
|
isParentSelector: true,
|
|
});
|
|
}
|
|
else if (scopedAtRuleIdentifiers.some((atRule) => rule.selector.startsWith(atRule))) {
|
|
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
|
|
}
|
|
else if (rule.selector.startsWith('@font-face') || rule.selector.startsWith('@page')) {
|
|
content = this._stripScopingSelectors(rule.content);
|
|
}
|
|
return new CssRule(selector, content);
|
|
});
|
|
}
|
|
/**
|
|
* Handle a css text that is within a rule that should not contain scope selectors by simply
|
|
* removing them! An example of such a rule is `@font-face`.
|
|
*
|
|
* `@font-face` rules cannot contain nested selectors. Nor can they be nested under a selector.
|
|
* Normally this would be a syntax error by the author of the styles. But in some rare cases, such
|
|
* as importing styles from a library, and applying `:host ::ng-deep` to the imported styles, we
|
|
* can end up with broken css if the imported styles happen to contain @font-face rules.
|
|
*
|
|
* For example:
|
|
*
|
|
* ```
|
|
* :host ::ng-deep {
|
|
* import 'some/lib/containing/font-face';
|
|
* }
|
|
*
|
|
* Similar logic applies to `@page` rules which can contain a particular set of properties,
|
|
* as well as some specific at-rules. Since they can't be encapsulated, we have to strip
|
|
* any scoping selectors from them. For more information: https://www.w3.org/TR/css-page-3
|
|
* ```
|
|
*/
|
|
_stripScopingSelectors(cssText) {
|
|
return processRules(cssText, (rule) => {
|
|
const selector = rule.selector
|
|
.replace(_shadowDeepSelectors, ' ')
|
|
.replace(_polyfillHostNoCombinatorRe, ' ');
|
|
return new CssRule(selector, rule.content);
|
|
});
|
|
}
|
|
_safeSelector;
|
|
_shouldScopeIndicator;
|
|
// `isParentSelector` is used to distinguish the selectors which are coming from
|
|
// the initial selector string and any nested selectors, parsed recursively,
|
|
// for example `selector = 'a:where(.one)'` could be the parent, while recursive call
|
|
// would have `selector = '.one'`.
|
|
_scopeSelector({ selector, scopeSelector, hostSelector, isParentSelector = false, }) {
|
|
// Split the selector into independent parts by `,` (comma) unless
|
|
// comma is within parenthesis, for example `:is(.one, two)`.
|
|
// Negative lookup after comma allows not splitting inside nested parenthesis,
|
|
// up to three levels (((,))).
|
|
const selectorSplitRe = / ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;
|
|
return selector
|
|
.split(selectorSplitRe)
|
|
.map((part) => part.split(_shadowDeepSelectors))
|
|
.map((deepParts) => {
|
|
const [shallowPart, ...otherParts] = deepParts;
|
|
const applyScope = (shallowPart) => {
|
|
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
|
|
return this._applySelectorScope({
|
|
selector: shallowPart,
|
|
scopeSelector,
|
|
hostSelector,
|
|
isParentSelector,
|
|
});
|
|
}
|
|
else {
|
|
return shallowPart;
|
|
}
|
|
};
|
|
return [applyScope(shallowPart), ...otherParts].join(' ');
|
|
})
|
|
.join(', ');
|
|
}
|
|
_selectorNeedsScoping(selector, scopeSelector) {
|
|
const re = this._makeScopeMatcher(scopeSelector);
|
|
return !re.test(selector);
|
|
}
|
|
_makeScopeMatcher(scopeSelector) {
|
|
const lre = /\[/g;
|
|
const rre = /\]/g;
|
|
scopeSelector = scopeSelector.replace(lre, '\\[').replace(rre, '\\]');
|
|
return new RegExp('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
|
|
}
|
|
// scope via name and [is=name]
|
|
_applySimpleSelectorScope(selector, scopeSelector, hostSelector) {
|
|
// In Android browser, the lastIndex is not reset when the regex is used in String.replace()
|
|
_polyfillHostRe.lastIndex = 0;
|
|
if (_polyfillHostRe.test(selector)) {
|
|
const replaceBy = `[${hostSelector}]`;
|
|
let result = selector;
|
|
while (result.match(_polyfillHostNoCombinatorRe)) {
|
|
result = result.replace(_polyfillHostNoCombinatorRe, (_hnc, selector) => {
|
|
return selector.replace(/([^:\)]*)(:*)(.*)/, (_, before, colon, after) => {
|
|
return before + replaceBy + colon + after;
|
|
});
|
|
});
|
|
}
|
|
return result.replace(_polyfillHostRe, replaceBy);
|
|
}
|
|
return scopeSelector + ' ' + selector;
|
|
}
|
|
// return a selector with [name] suffix on each simple selector
|
|
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */
|
|
_applySelectorScope({ selector, scopeSelector, hostSelector, isParentSelector, }) {
|
|
const isRe = /\[is=([^\]]*)\]/g;
|
|
scopeSelector = scopeSelector.replace(isRe, (_, ...parts) => parts[0]);
|
|
const attrName = `[${scopeSelector}]`;
|
|
const _scopeSelectorPart = (p) => {
|
|
let scopedP = p.trim();
|
|
if (!scopedP) {
|
|
return p;
|
|
}
|
|
if (p.includes(_polyfillHostNoCombinator)) {
|
|
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
|
|
if (!p.match(_polyfillHostNoCombinatorOutsidePseudoFunction)) {
|
|
const [_, before, colon, after] = scopedP.match(/([^:]*)(:*)([\s\S]*)/);
|
|
scopedP = before + attrName + colon + after;
|
|
}
|
|
}
|
|
else {
|
|
// remove :host since it should be unnecessary
|
|
const t = p.replace(_polyfillHostRe, '');
|
|
if (t.length > 0) {
|
|
const matches = t.match(/([^:]*)(:*)([\s\S]*)/);
|
|
if (matches) {
|
|
scopedP = matches[1] + attrName + matches[2] + matches[3];
|
|
}
|
|
}
|
|
}
|
|
return scopedP;
|
|
};
|
|
// Wraps `_scopeSelectorPart()` to not use it directly on selectors with
|
|
// pseudo selector functions like `:where()`. Selectors within pseudo selector
|
|
// functions are recursively sent to `_scopeSelector()`.
|
|
const _pseudoFunctionAwareScopeSelectorPart = (selectorPart) => {
|
|
let scopedPart = '';
|
|
// Collect all outer `:where()` and `:is()` selectors,
|
|
// counting parenthesis to keep nested selectors intact.
|
|
const pseudoSelectorParts = [];
|
|
let pseudoSelectorMatch;
|
|
while ((pseudoSelectorMatch = _cssPrefixWithPseudoSelectorFunction.exec(selectorPart)) !== null) {
|
|
let openedBrackets = 1;
|
|
let index = _cssPrefixWithPseudoSelectorFunction.lastIndex;
|
|
while (index < selectorPart.length) {
|
|
const currentSymbol = selectorPart[index];
|
|
index++;
|
|
if (currentSymbol === '(') {
|
|
openedBrackets++;
|
|
continue;
|
|
}
|
|
if (currentSymbol === ')') {
|
|
openedBrackets--;
|
|
if (openedBrackets === 0) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
pseudoSelectorParts.push(`${pseudoSelectorMatch[0]}${selectorPart.slice(_cssPrefixWithPseudoSelectorFunction.lastIndex, index)}`);
|
|
_cssPrefixWithPseudoSelectorFunction.lastIndex = index;
|
|
}
|
|
// If selector consists of only `:where()` and `:is()` on the outer level
|
|
// scope those pseudo-selectors individually, otherwise scope the whole
|
|
// selector.
|
|
if (pseudoSelectorParts.join('') === selectorPart) {
|
|
scopedPart = pseudoSelectorParts
|
|
.map((selectorPart) => {
|
|
const [cssPseudoSelectorFunction] = selectorPart.match(_cssPrefixWithPseudoSelectorFunction) ?? [];
|
|
// Unwrap the pseudo selector to scope its contents.
|
|
// For example,
|
|
// - `:where(selectorToScope)` -> `selectorToScope`;
|
|
// - `:is(.foo, .bar)` -> `.foo, .bar`.
|
|
const selectorToScope = selectorPart.slice(cssPseudoSelectorFunction?.length, -1);
|
|
if (selectorToScope.includes(_polyfillHostNoCombinator)) {
|
|
this._shouldScopeIndicator = true;
|
|
}
|
|
const scopedInnerPart = this._scopeSelector({
|
|
selector: selectorToScope,
|
|
scopeSelector,
|
|
hostSelector,
|
|
});
|
|
// Put the result back into the pseudo selector function.
|
|
return `${cssPseudoSelectorFunction}${scopedInnerPart})`;
|
|
})
|
|
.join('');
|
|
}
|
|
else {
|
|
this._shouldScopeIndicator =
|
|
this._shouldScopeIndicator || selectorPart.includes(_polyfillHostNoCombinator);
|
|
scopedPart = this._shouldScopeIndicator ? _scopeSelectorPart(selectorPart) : selectorPart;
|
|
}
|
|
return scopedPart;
|
|
};
|
|
if (isParentSelector) {
|
|
this._safeSelector = new SafeSelector(selector);
|
|
selector = this._safeSelector.content();
|
|
}
|
|
let scopedSelector = '';
|
|
let startIndex = 0;
|
|
let res;
|
|
// Combinators aren't used as a delimiter if they are within parenthesis,
|
|
// for example `:where(.one .two)` stays intact.
|
|
// Similarly to selector separation by comma initially, negative lookahead
|
|
// is used here to not break selectors within nested parenthesis up to three
|
|
// nested layers.
|
|
const sep = /( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g;
|
|
// If a selector appears before :host it should not be shimmed as it
|
|
// matches on ancestor elements and not on elements in the host's shadow
|
|
// `:host-context(div)` is transformed to
|
|
// `-shadowcsshost-no-combinatordiv, div -shadowcsshost-no-combinator`
|
|
// the `div` is not part of the component in the 2nd selectors and should not be scoped.
|
|
// Historically `component-tag:host` was matching the component so we also want to preserve
|
|
// this behavior to avoid breaking legacy apps (it should not match).
|
|
// The behavior should be:
|
|
// - `tag:host` -> `tag[h]` (this is to avoid breaking legacy apps, should not match anything)
|
|
// - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
|
|
// `:host-context(tag)`)
|
|
const hasHost = selector.includes(_polyfillHostNoCombinator);
|
|
// Only scope parts after or on the same level as the first `-shadowcsshost-no-combinator`
|
|
// when it is present. The selector has the same level when it is a part of a pseudo
|
|
// selector, like `:where()`, for example `:where(:host, .foo)` would result in `.foo`
|
|
// being scoped.
|
|
if (isParentSelector || this._shouldScopeIndicator) {
|
|
this._shouldScopeIndicator = !hasHost;
|
|
}
|
|
while ((res = sep.exec(selector)) !== null) {
|
|
const separator = res[1];
|
|
// Do not trim the selector, as otherwise this will break sourcemaps
|
|
// when they are defined on multiple lines
|
|
// Example:
|
|
// div,
|
|
// p { color: red}
|
|
const part = selector.slice(startIndex, res.index);
|
|
// A space following an escaped hex value and followed by another hex character
|
|
// (ie: ".\fc ber" for ".über") is not a separator between 2 selectors
|
|
// also keep in mind that backslashes are replaced by a placeholder by SafeSelector
|
|
// These escaped selectors happen for example when esbuild runs with optimization.minify.
|
|
if (part.match(/__esc-ph-(\d+)__/) && selector[res.index + 1]?.match(/[a-fA-F\d]/)) {
|
|
continue;
|
|
}
|
|
const scopedPart = _pseudoFunctionAwareScopeSelectorPart(part);
|
|
scopedSelector += `${scopedPart} ${separator} `;
|
|
startIndex = sep.lastIndex;
|
|
}
|
|
const part = selector.substring(startIndex);
|
|
scopedSelector += _pseudoFunctionAwareScopeSelectorPart(part);
|
|
// replace the placeholders with their original values
|
|
// using values stored inside the `safeSelector` instance.
|
|
return this._safeSelector.restore(scopedSelector);
|
|
}
|
|
_insertPolyfillHostInCssText(selector) {
|
|
return selector
|
|
.replace(_colonHostContextRe, _polyfillHostContext)
|
|
.replace(_colonHostRe, _polyfillHost);
|
|
}
|
|
}
|
|
class SafeSelector {
|
|
placeholders = [];
|
|
index = 0;
|
|
_content;
|
|
constructor(selector) {
|
|
// Replaces attribute selectors with placeholders.
|
|
// The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
|
|
selector = this._escapeRegexMatches(selector, /(\[[^\]]*\])/g);
|
|
// CSS allows for certain special characters to be used in selectors if they're escaped.
|
|
// E.g. `.foo:blue` won't match a class called `foo:blue`, because the colon denotes a
|
|
// pseudo-class, but writing `.foo\:blue` will match, because the colon was escaped.
|
|
// Replace all escape sequences (`\` followed by a character) with a placeholder so
|
|
// that our handling of pseudo-selectors doesn't mess with them.
|
|
// Escaped characters have a specific placeholder so they can be detected separately.
|
|
selector = selector.replace(/(\\.)/g, (_, keep) => {
|
|
const replaceBy = `__esc-ph-${this.index}__`;
|
|
this.placeholders.push(keep);
|
|
this.index++;
|
|
return replaceBy;
|
|
});
|
|
// Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
|
|
// WS and "+" would otherwise be interpreted as selector separators.
|
|
this._content = selector.replace(nthRegex, (_, pseudo, exp) => {
|
|
const replaceBy = `__ph-${this.index}__`;
|
|
this.placeholders.push(`(${exp})`);
|
|
this.index++;
|
|
return pseudo + replaceBy;
|
|
});
|
|
}
|
|
restore(content) {
|
|
return content.replace(/__(?:ph|esc-ph)-(\d+)__/g, (_ph, index) => this.placeholders[+index]);
|
|
}
|
|
content() {
|
|
return this._content;
|
|
}
|
|
/**
|
|
* Replaces all of the substrings that match a regex within a
|
|
* special string (e.g. `__ph-0__`, `__ph-1__`, etc).
|
|
*/
|
|
_escapeRegexMatches(content, pattern) {
|
|
return content.replace(pattern, (_, keep) => {
|
|
const replaceBy = `__ph-${this.index}__`;
|
|
this.placeholders.push(keep);
|
|
this.index++;
|
|
return replaceBy;
|
|
});
|
|
}
|
|
}
|
|
const _cssScopedPseudoFunctionPrefix = '(:(where|is)\\()?';
|
|
const _cssPrefixWithPseudoSelectorFunction = /:(where|is)\(/gi;
|
|
const _cssContentNextSelectorRe = /polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
|
|
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
|
|
const _cssContentUnscopedRuleRe = /(polyfill-unscoped-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
|
|
const _polyfillHost = '-shadowcsshost';
|
|
// note: :host-context pre-processed to -shadowcsshostcontext.
|
|
const _polyfillHostContext = '-shadowcsscontext';
|
|
// Matches text content with no parentheses, e.g., "foo"
|
|
const _noParens = '[^)(]*';
|
|
// Matches content with at most ONE level of nesting, e.g., "a(b)c"
|
|
const _level1Parens = String.raw `(?:\(${_noParens}\)|${_noParens})+?`;
|
|
// Matches content with at most TWO levels of nesting, e.g., "a(b(c)d)e"
|
|
const _level2Parens = String.raw `(?:\(${_level1Parens}\)|${_noParens})+?`;
|
|
const _parenSuffix = String.raw `(?:\((${_level2Parens})\))`;
|
|
const nthRegex = new RegExp(String.raw `(:nth-[-\w]+)` + _parenSuffix, 'g');
|
|
const _cssColonHostRe = new RegExp(_polyfillHost + _parenSuffix + '?([^,{]*)', 'gim');
|
|
// note: :host-context patterns are terminated with `{`, as opposed to :host which
|
|
// is both `{` and `,` because :host-context handles top-level commas differently.
|
|
const _hostContextPattern = _polyfillHostContext + _parenSuffix + '?([^{]*)';
|
|
const _cssColonHostContextReGlobal = new RegExp(`${_cssScopedPseudoFunctionPrefix}(${_hostContextPattern})`, 'gim');
|
|
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
|
const _polyfillHostNoCombinatorOutsidePseudoFunction = new RegExp(`${_polyfillHostNoCombinator}(?![^(]*\\))`, 'g');
|
|
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s,]*)/;
|
|
const _shadowDOMSelectorsRe = [
|
|
/::shadow/g,
|
|
/::content/g,
|
|
// Deprecated selectors
|
|
/\/shadow-deep\//g,
|
|
/\/shadow\//g,
|
|
];
|
|
// The deep combinator is deprecated in the CSS spec
|
|
// Support for `>>>`, `deep`, `::ng-deep` is then also deprecated and will be removed in the future.
|
|
// see https://github.com/angular/angular/pull/17677
|
|
const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)|(?:::ng-deep)/g;
|
|
const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
|
|
const _polyfillHostRe = /-shadowcsshost/gim;
|
|
const _colonHostRe = /:host/gim;
|
|
const _colonHostContextRe = /:host-context/gim;
|
|
const _newLinesRe = /\r?\n/g;
|
|
const _commentRe = /\/\*[\s\S]*?\*\//g;
|
|
const _commentWithHashRe = /\/\*\s*#\s*source(Mapping)?URL=/g;
|
|
const COMMENT_PLACEHOLDER = '%COMMENT%';
|
|
const _commentWithHashPlaceHolderRe = new RegExp(COMMENT_PLACEHOLDER, 'g');
|
|
const BLOCK_PLACEHOLDER = '%BLOCK%';
|
|
const _ruleRe = new RegExp(`(\\s*(?:${COMMENT_PLACEHOLDER}\\s*)*)([^;\\{\\}]+?)(\\s*)((?:{%BLOCK%}?\\s*;?)|(?:\\s*;))`, 'g');
|
|
const CONTENT_PAIRS = new Map([['{', '}']]);
|
|
const COMMA_IN_PLACEHOLDER = '%COMMA_IN_PLACEHOLDER%';
|
|
const SEMI_IN_PLACEHOLDER = '%SEMI_IN_PLACEHOLDER%';
|
|
const COLON_IN_PLACEHOLDER = '%COLON_IN_PLACEHOLDER%';
|
|
const _cssCommaInPlaceholderReGlobal = new RegExp(COMMA_IN_PLACEHOLDER, 'g');
|
|
const _cssSemiInPlaceholderReGlobal = new RegExp(SEMI_IN_PLACEHOLDER, 'g');
|
|
const _cssColonInPlaceholderReGlobal = new RegExp(COLON_IN_PLACEHOLDER, 'g');
|
|
class CssRule {
|
|
selector;
|
|
content;
|
|
constructor(selector, content) {
|
|
this.selector = selector;
|
|
this.content = content;
|
|
}
|
|
}
|
|
function processRules(input, ruleCallback) {
|
|
const escaped = escapeInStrings(input);
|
|
const inputWithEscapedBlocks = escapeBlocks(escaped, CONTENT_PAIRS, BLOCK_PLACEHOLDER);
|
|
let nextBlockIndex = 0;
|
|
const escapedResult = inputWithEscapedBlocks.escapedString.replace(_ruleRe, (...m) => {
|
|
const selector = m[2];
|
|
let content = '';
|
|
let suffix = m[4];
|
|
let contentPrefix = '';
|
|
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
|
|
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
|
|
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
|
|
contentPrefix = '{';
|
|
}
|
|
const rule = ruleCallback(new CssRule(selector, content));
|
|
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
|
|
});
|
|
return unescapeInStrings(escapedResult);
|
|
}
|
|
class StringWithEscapedBlocks {
|
|
escapedString;
|
|
blocks;
|
|
constructor(escapedString, blocks) {
|
|
this.escapedString = escapedString;
|
|
this.blocks = blocks;
|
|
}
|
|
}
|
|
function escapeBlocks(input, charPairs, placeholder) {
|
|
const resultParts = [];
|
|
const escapedBlocks = [];
|
|
let openCharCount = 0;
|
|
let nonBlockStartIndex = 0;
|
|
let blockStartIndex = -1;
|
|
let openChar;
|
|
let closeChar;
|
|
for (let i = 0; i < input.length; i++) {
|
|
const char = input[i];
|
|
if (char === '\\') {
|
|
i++;
|
|
}
|
|
else if (char === closeChar) {
|
|
openCharCount--;
|
|
if (openCharCount === 0) {
|
|
escapedBlocks.push(input.substring(blockStartIndex, i));
|
|
resultParts.push(placeholder);
|
|
nonBlockStartIndex = i;
|
|
blockStartIndex = -1;
|
|
openChar = closeChar = undefined;
|
|
}
|
|
}
|
|
else if (char === openChar) {
|
|
openCharCount++;
|
|
}
|
|
else if (openCharCount === 0 && charPairs.has(char)) {
|
|
openChar = char;
|
|
closeChar = charPairs.get(char);
|
|
openCharCount = 1;
|
|
blockStartIndex = i + 1;
|
|
resultParts.push(input.substring(nonBlockStartIndex, blockStartIndex));
|
|
}
|
|
}
|
|
if (blockStartIndex !== -1) {
|
|
escapedBlocks.push(input.substring(blockStartIndex));
|
|
resultParts.push(placeholder);
|
|
}
|
|
else {
|
|
resultParts.push(input.substring(nonBlockStartIndex));
|
|
}
|
|
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
|
|
}
|
|
/**
|
|
* Object containing as keys characters that should be substituted by placeholders
|
|
* when found in strings during the css text parsing, and as values the respective
|
|
* placeholders
|
|
*/
|
|
const ESCAPE_IN_STRING_MAP = {
|
|
';': SEMI_IN_PLACEHOLDER,
|
|
',': COMMA_IN_PLACEHOLDER,
|
|
':': COLON_IN_PLACEHOLDER,
|
|
};
|
|
/**
|
|
* Parse the provided css text and inside strings (meaning, inside pairs of unescaped single or
|
|
* double quotes) replace specific characters with their respective placeholders as indicated
|
|
* by the `ESCAPE_IN_STRING_MAP` map.
|
|
*
|
|
* For example convert the text
|
|
* `animation: "my-anim:at\"ion" 1s;`
|
|
* to
|
|
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
|
|
*
|
|
* This is necessary in order to remove the meaning of some characters when found inside strings
|
|
* (for example `;` indicates the end of a css declaration, `,` the sequence of values and `:` the
|
|
* division between property and value during a declaration, none of these meanings apply when such
|
|
* characters are within strings and so in order to prevent parsing issues they need to be replaced
|
|
* with placeholder text for the duration of the css manipulation process).
|
|
*
|
|
* @param input the original css text.
|
|
*
|
|
* @returns the css text with specific characters in strings replaced by placeholders.
|
|
**/
|
|
function escapeInStrings(input) {
|
|
let result = input;
|
|
let currentQuoteChar = null;
|
|
for (let i = 0; i < result.length; i++) {
|
|
const char = result[i];
|
|
if (char === '\\') {
|
|
i++;
|
|
}
|
|
else {
|
|
if (currentQuoteChar !== null) {
|
|
// index i is inside a quoted sub-string
|
|
if (char === currentQuoteChar) {
|
|
currentQuoteChar = null;
|
|
}
|
|
else {
|
|
const placeholder = ESCAPE_IN_STRING_MAP[char];
|
|
if (placeholder) {
|
|
result = `${result.substr(0, i)}${placeholder}${result.substr(i + 1)}`;
|
|
i += placeholder.length - 1;
|
|
}
|
|
}
|
|
}
|
|
else if (char === "'" || char === '"') {
|
|
currentQuoteChar = char;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Replace in a string all occurrences of keys in the `ESCAPE_IN_STRING_MAP` map with their
|
|
* original representation, this is simply used to revert the changes applied by the
|
|
* escapeInStrings function.
|
|
*
|
|
* For example it reverts the text:
|
|
* `animation: "my-anim%COLON_IN_PLACEHOLDER%at\"ion" 1s;`
|
|
* to it's original form of:
|
|
* `animation: "my-anim:at\"ion" 1s;`
|
|
*
|
|
* Note: For the sake of simplicity this function does not check that the placeholders are
|
|
* actually inside strings as it would anyway be extremely unlikely to find them outside of strings.
|
|
*
|
|
* @param input the css text containing the placeholders.
|
|
*
|
|
* @returns the css text without the placeholders.
|
|
*/
|
|
function unescapeInStrings(input) {
|
|
let result = input.replace(_cssCommaInPlaceholderReGlobal, ',');
|
|
result = result.replace(_cssSemiInPlaceholderReGlobal, ';');
|
|
result = result.replace(_cssColonInPlaceholderReGlobal, ':');
|
|
return result;
|
|
}
|
|
/**
|
|
* Unescape all quotes present in a string, but only if the string was actually already
|
|
* quoted.
|
|
*
|
|
* This generates a "canonical" representation of strings which can be used to match strings
|
|
* which would otherwise only differ because of differently escaped quotes.
|
|
*
|
|
* For example it converts the string (assumed to be quoted):
|
|
* `this \\"is\\" a \\'\\\\'test`
|
|
* to:
|
|
* `this "is" a '\\\\'test`
|
|
* (note that the latter backslashes are not removed as they are not actually escaping the single
|
|
* quote)
|
|
*
|
|
*
|
|
* @param input the string possibly containing escaped quotes.
|
|
* @param isQuoted boolean indicating whether the string was quoted inside a bigger string (if not
|
|
* then it means that it doesn't represent an inner string and thus no unescaping is required)
|
|
*
|
|
* @returns the string in the "canonical" representation without escaped quotes.
|
|
*/
|
|
function unescapeQuotes(str, isQuoted) {
|
|
return !isQuoted ? str : str.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=['"])/g, '$1');
|
|
}
|
|
/**
|
|
* Combine the `contextSelectors` with the `hostMarker` and the `otherSelectors`
|
|
* to create a selector that matches the same as `:host-context()`.
|
|
*
|
|
* Given a single context selector `A` we need to output selectors that match on the host and as an
|
|
* ancestor of the host:
|
|
*
|
|
* ```
|
|
* A <hostMarker>, A<hostMarker> {}
|
|
* ```
|
|
*
|
|
* When there is more than one context selector we also have to create combinations of those
|
|
* selectors with each other. For example if there are `A` and `B` selectors the output is:
|
|
*
|
|
* ```
|
|
* AB<hostMarker>, AB <hostMarker>, A B<hostMarker>,
|
|
* B A<hostMarker>, A B <hostMarker>, B A <hostMarker> {}
|
|
* ```
|
|
*
|
|
* And so on...
|
|
*
|
|
* @param contextSelectors an array of context selectors that will be combined.
|
|
* @param otherSelectors the rest of the selectors that are not context selectors.
|
|
*/
|
|
function _combineHostContextSelectors(contextSelectors, otherSelectors, pseudoPrefix = '') {
|
|
const hostMarker = _polyfillHostNoCombinator;
|
|
_polyfillHostRe.lastIndex = 0; // reset the regex to ensure we get an accurate test
|
|
const otherSelectorsHasHost = _polyfillHostRe.test(otherSelectors);
|
|
// If there are no context selectors then just output a host marker
|
|
if (contextSelectors.length === 0) {
|
|
return hostMarker + otherSelectors;
|
|
}
|
|
const combined = [contextSelectors.pop() || ''];
|
|
while (contextSelectors.length > 0) {
|
|
const length = combined.length;
|
|
const contextSelector = contextSelectors.pop();
|
|
for (let i = 0; i < length; i++) {
|
|
const previousSelectors = combined[i];
|
|
// Add the new selector as a descendant of the previous selectors
|
|
combined[length * 2 + i] = previousSelectors + ' ' + contextSelector;
|
|
// Add the new selector as an ancestor of the previous selectors
|
|
combined[length + i] = contextSelector + ' ' + previousSelectors;
|
|
// Add the new selector to act on the same element as the previous selectors
|
|
combined[i] = contextSelector + previousSelectors;
|
|
}
|
|
}
|
|
// Finally connect the selector to the `hostMarker`s: either acting directly on the host
|
|
// (A<hostMarker>) or as an ancestor (A <hostMarker>).
|
|
return combined
|
|
.map((s) => otherSelectorsHasHost
|
|
? `${pseudoPrefix}${s}${otherSelectors}`
|
|
: `${pseudoPrefix}${s}${hostMarker}${otherSelectors}, ${pseudoPrefix}${s} ${hostMarker}${otherSelectors}`)
|
|
.join(',');
|
|
}
|
|
/**
|
|
* Mutate the given `groups` array so that there are `multiples` clones of the original array
|
|
* stored.
|
|
*
|
|
* For example `repeatGroups([a, b], 3)` will result in `[a, b, a, b, a, b]` - but importantly the
|
|
* newly added groups will be clones of the original.
|
|
*
|
|
* @param groups An array of groups of strings that will be repeated. This array is mutated
|
|
* in-place.
|
|
* @param multiples The number of times the current groups should appear.
|
|
*/
|
|
function repeatGroups(groups, multiples) {
|
|
const length = groups.length;
|
|
for (let i = 1; i < multiples; i++) {
|
|
for (let j = 0; j < length; j++) {
|
|
groups[j + i * length] = groups[j].slice(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Distinguishes different kinds of IR operations.
|
|
*
|
|
* Includes both creation and update operations.
|
|
*/
|
|
var OpKind;
|
|
(function (OpKind) {
|
|
/**
|
|
* A special operation type which is used to represent the beginning and end nodes of a linked
|
|
* list of operations.
|
|
*/
|
|
OpKind[OpKind["ListEnd"] = 0] = "ListEnd";
|
|
/**
|
|
* An operation which wraps an output AST statement.
|
|
*/
|
|
OpKind[OpKind["Statement"] = 1] = "Statement";
|
|
/**
|
|
* An operation which declares and initializes a `SemanticVariable`.
|
|
*/
|
|
OpKind[OpKind["Variable"] = 2] = "Variable";
|
|
/**
|
|
* An operation to begin rendering of an element.
|
|
*/
|
|
OpKind[OpKind["ElementStart"] = 3] = "ElementStart";
|
|
/**
|
|
* An operation to render an element with no children.
|
|
*/
|
|
OpKind[OpKind["Element"] = 4] = "Element";
|
|
/**
|
|
* An operation which declares an embedded view.
|
|
*/
|
|
OpKind[OpKind["Template"] = 5] = "Template";
|
|
/**
|
|
* An operation to end rendering of an element previously started with `ElementStart`.
|
|
*/
|
|
OpKind[OpKind["ElementEnd"] = 6] = "ElementEnd";
|
|
/**
|
|
* An operation to begin an `ng-container`.
|
|
*/
|
|
OpKind[OpKind["ContainerStart"] = 7] = "ContainerStart";
|
|
/**
|
|
* An operation for an `ng-container` with no children.
|
|
*/
|
|
OpKind[OpKind["Container"] = 8] = "Container";
|
|
/**
|
|
* An operation to end an `ng-container`.
|
|
*/
|
|
OpKind[OpKind["ContainerEnd"] = 9] = "ContainerEnd";
|
|
/**
|
|
* An operation disable binding for subsequent elements, which are descendants of a non-bindable
|
|
* node.
|
|
*/
|
|
OpKind[OpKind["DisableBindings"] = 10] = "DisableBindings";
|
|
/**
|
|
* Create a conditional creation instruction op.
|
|
*/
|
|
OpKind[OpKind["ConditionalCreate"] = 11] = "ConditionalCreate";
|
|
/**
|
|
* Create a conditional branch creation instruction op.
|
|
*/
|
|
OpKind[OpKind["ConditionalBranchCreate"] = 12] = "ConditionalBranchCreate";
|
|
/**
|
|
* An op to conditionally render a template.
|
|
*/
|
|
OpKind[OpKind["Conditional"] = 13] = "Conditional";
|
|
/**
|
|
* An operation to re-enable binding, after it was previously disabled.
|
|
*/
|
|
OpKind[OpKind["EnableBindings"] = 14] = "EnableBindings";
|
|
/**
|
|
* An operation to render a text node.
|
|
*/
|
|
OpKind[OpKind["Text"] = 15] = "Text";
|
|
/**
|
|
* An operation declaring an event listener for an element.
|
|
*/
|
|
OpKind[OpKind["Listener"] = 16] = "Listener";
|
|
/**
|
|
* An operation to interpolate text into a text node.
|
|
*/
|
|
OpKind[OpKind["InterpolateText"] = 17] = "InterpolateText";
|
|
/**
|
|
* An intermediate binding op, that has not yet been processed into an individual property,
|
|
* attribute, style, etc.
|
|
*/
|
|
OpKind[OpKind["Binding"] = 18] = "Binding";
|
|
/**
|
|
* An operation to bind an expression to a property of an element.
|
|
*/
|
|
OpKind[OpKind["Property"] = 19] = "Property";
|
|
/**
|
|
* An operation to bind an expression to a style property of an element.
|
|
*/
|
|
OpKind[OpKind["StyleProp"] = 20] = "StyleProp";
|
|
/**
|
|
* An operation to bind an expression to a class property of an element.
|
|
*/
|
|
OpKind[OpKind["ClassProp"] = 21] = "ClassProp";
|
|
/**
|
|
* An operation to bind an expression to the styles of an element.
|
|
*/
|
|
OpKind[OpKind["StyleMap"] = 22] = "StyleMap";
|
|
/**
|
|
* An operation to bind an expression to the classes of an element.
|
|
*/
|
|
OpKind[OpKind["ClassMap"] = 23] = "ClassMap";
|
|
/**
|
|
* An operation to advance the runtime's implicit slot context during the update phase of a view.
|
|
*/
|
|
OpKind[OpKind["Advance"] = 24] = "Advance";
|
|
/**
|
|
* An operation to instantiate a pipe.
|
|
*/
|
|
OpKind[OpKind["Pipe"] = 25] = "Pipe";
|
|
/**
|
|
* An operation to associate an attribute with an element.
|
|
*/
|
|
OpKind[OpKind["Attribute"] = 26] = "Attribute";
|
|
/**
|
|
* An attribute that has been extracted for inclusion in the consts array.
|
|
*/
|
|
OpKind[OpKind["ExtractedAttribute"] = 27] = "ExtractedAttribute";
|
|
/**
|
|
* An operation that configures a `@defer` block.
|
|
*/
|
|
OpKind[OpKind["Defer"] = 28] = "Defer";
|
|
/**
|
|
* An operation that controls when a `@defer` loads.
|
|
*/
|
|
OpKind[OpKind["DeferOn"] = 29] = "DeferOn";
|
|
/**
|
|
* An operation that controls when a `@defer` loads, using a custom expression as the condition.
|
|
*/
|
|
OpKind[OpKind["DeferWhen"] = 30] = "DeferWhen";
|
|
/**
|
|
* An i18n message that has been extracted for inclusion in the consts array.
|
|
*/
|
|
OpKind[OpKind["I18nMessage"] = 31] = "I18nMessage";
|
|
/**
|
|
* A binding to a native DOM property.
|
|
*/
|
|
OpKind[OpKind["DomProperty"] = 32] = "DomProperty";
|
|
/**
|
|
* A namespace change, which causes the subsequent elements to be processed as either HTML or SVG.
|
|
*/
|
|
OpKind[OpKind["Namespace"] = 33] = "Namespace";
|
|
/**
|
|
* Configure a content projeciton definition for the view.
|
|
*/
|
|
OpKind[OpKind["ProjectionDef"] = 34] = "ProjectionDef";
|
|
/**
|
|
* Create a content projection slot.
|
|
*/
|
|
OpKind[OpKind["Projection"] = 35] = "Projection";
|
|
/**
|
|
* Create a repeater creation instruction op.
|
|
*/
|
|
OpKind[OpKind["RepeaterCreate"] = 36] = "RepeaterCreate";
|
|
/**
|
|
* An update up for a repeater.
|
|
*/
|
|
OpKind[OpKind["Repeater"] = 37] = "Repeater";
|
|
/**
|
|
* An operation to bind an expression to the property side of a two-way binding.
|
|
*/
|
|
OpKind[OpKind["TwoWayProperty"] = 38] = "TwoWayProperty";
|
|
/**
|
|
* An operation declaring the event side of a two-way binding.
|
|
*/
|
|
OpKind[OpKind["TwoWayListener"] = 39] = "TwoWayListener";
|
|
/**
|
|
* A creation-time operation that initializes the slot for a `@let` declaration.
|
|
*/
|
|
OpKind[OpKind["DeclareLet"] = 40] = "DeclareLet";
|
|
/**
|
|
* An update-time operation that stores the current value of a `@let` declaration.
|
|
*/
|
|
OpKind[OpKind["StoreLet"] = 41] = "StoreLet";
|
|
/**
|
|
* The start of an i18n block.
|
|
*/
|
|
OpKind[OpKind["I18nStart"] = 42] = "I18nStart";
|
|
/**
|
|
* A self-closing i18n on a single element.
|
|
*/
|
|
OpKind[OpKind["I18n"] = 43] = "I18n";
|
|
/**
|
|
* The end of an i18n block.
|
|
*/
|
|
OpKind[OpKind["I18nEnd"] = 44] = "I18nEnd";
|
|
/**
|
|
* An expression in an i18n message.
|
|
*/
|
|
OpKind[OpKind["I18nExpression"] = 45] = "I18nExpression";
|
|
/**
|
|
* An instruction that applies a set of i18n expressions.
|
|
*/
|
|
OpKind[OpKind["I18nApply"] = 46] = "I18nApply";
|
|
/**
|
|
* An instruction to create an ICU expression.
|
|
*/
|
|
OpKind[OpKind["IcuStart"] = 47] = "IcuStart";
|
|
/**
|
|
* An instruction to update an ICU expression.
|
|
*/
|
|
OpKind[OpKind["IcuEnd"] = 48] = "IcuEnd";
|
|
/**
|
|
* An instruction representing a placeholder in an ICU expression.
|
|
*/
|
|
OpKind[OpKind["IcuPlaceholder"] = 49] = "IcuPlaceholder";
|
|
/**
|
|
* An i18n context containing information needed to generate an i18n message.
|
|
*/
|
|
OpKind[OpKind["I18nContext"] = 50] = "I18nContext";
|
|
/**
|
|
* A creation op that corresponds to i18n attributes on an element.
|
|
*/
|
|
OpKind[OpKind["I18nAttributes"] = 51] = "I18nAttributes";
|
|
/**
|
|
* Creation op that attaches the location at which an element was defined in a template to it.
|
|
*/
|
|
OpKind[OpKind["SourceLocation"] = 52] = "SourceLocation";
|
|
/**
|
|
* An operation to bind animation css classes to an element.
|
|
*/
|
|
OpKind[OpKind["Animation"] = 53] = "Animation";
|
|
/**
|
|
* An operation to bind animation css classes to an element.
|
|
*/
|
|
OpKind[OpKind["AnimationString"] = 54] = "AnimationString";
|
|
/**
|
|
* An operation to bind animation css classes to an element.
|
|
*/
|
|
OpKind[OpKind["AnimationBinding"] = 55] = "AnimationBinding";
|
|
/**
|
|
* An operation to bind animation events to an element.
|
|
*/
|
|
OpKind[OpKind["AnimationListener"] = 56] = "AnimationListener";
|
|
})(OpKind || (OpKind = {}));
|
|
/**
|
|
* Distinguishes different kinds of IR expressions.
|
|
*/
|
|
var ExpressionKind;
|
|
(function (ExpressionKind) {
|
|
/**
|
|
* Read of a variable in a lexical scope.
|
|
*/
|
|
ExpressionKind[ExpressionKind["LexicalRead"] = 0] = "LexicalRead";
|
|
/**
|
|
* A reference to the current view context.
|
|
*/
|
|
ExpressionKind[ExpressionKind["Context"] = 1] = "Context";
|
|
/**
|
|
* A reference to the view context, for use inside a track function.
|
|
*/
|
|
ExpressionKind[ExpressionKind["TrackContext"] = 2] = "TrackContext";
|
|
/**
|
|
* Read of a variable declared in a `VariableOp`.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ReadVariable"] = 3] = "ReadVariable";
|
|
/**
|
|
* Runtime operation to navigate to the next view context in the view hierarchy.
|
|
*/
|
|
ExpressionKind[ExpressionKind["NextContext"] = 4] = "NextContext";
|
|
/**
|
|
* Runtime operation to retrieve the value of a local reference.
|
|
*/
|
|
ExpressionKind[ExpressionKind["Reference"] = 5] = "Reference";
|
|
/**
|
|
* A call storing the value of a `@let` declaration.
|
|
*/
|
|
ExpressionKind[ExpressionKind["StoreLet"] = 6] = "StoreLet";
|
|
/**
|
|
* A reference to a `@let` declaration read from the context view.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ContextLetReference"] = 7] = "ContextLetReference";
|
|
/**
|
|
* Runtime operation to snapshot the current view context.
|
|
*/
|
|
ExpressionKind[ExpressionKind["GetCurrentView"] = 8] = "GetCurrentView";
|
|
/**
|
|
* Runtime operation to restore a snapshotted view.
|
|
*/
|
|
ExpressionKind[ExpressionKind["RestoreView"] = 9] = "RestoreView";
|
|
/**
|
|
* Runtime operation to reset the current view context after `RestoreView`.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ResetView"] = 10] = "ResetView";
|
|
/**
|
|
* Defines and calls a function with change-detected arguments.
|
|
*/
|
|
ExpressionKind[ExpressionKind["PureFunctionExpr"] = 11] = "PureFunctionExpr";
|
|
/**
|
|
* Indicates a positional parameter to a pure function definition.
|
|
*/
|
|
ExpressionKind[ExpressionKind["PureFunctionParameterExpr"] = 12] = "PureFunctionParameterExpr";
|
|
/**
|
|
* Binding to a pipe transformation.
|
|
*/
|
|
ExpressionKind[ExpressionKind["PipeBinding"] = 13] = "PipeBinding";
|
|
/**
|
|
* Binding to a pipe transformation with a variable number of arguments.
|
|
*/
|
|
ExpressionKind[ExpressionKind["PipeBindingVariadic"] = 14] = "PipeBindingVariadic";
|
|
/*
|
|
* A safe property read requiring expansion into a null check.
|
|
*/
|
|
ExpressionKind[ExpressionKind["SafePropertyRead"] = 15] = "SafePropertyRead";
|
|
/**
|
|
* A safe keyed read requiring expansion into a null check.
|
|
*/
|
|
ExpressionKind[ExpressionKind["SafeKeyedRead"] = 16] = "SafeKeyedRead";
|
|
/**
|
|
* A safe function call requiring expansion into a null check.
|
|
*/
|
|
ExpressionKind[ExpressionKind["SafeInvokeFunction"] = 17] = "SafeInvokeFunction";
|
|
/**
|
|
* An intermediate expression that will be expanded from a safe read into an explicit ternary.
|
|
*/
|
|
ExpressionKind[ExpressionKind["SafeTernaryExpr"] = 18] = "SafeTernaryExpr";
|
|
/**
|
|
* An empty expression that will be stipped before generating the final output.
|
|
*/
|
|
ExpressionKind[ExpressionKind["EmptyExpr"] = 19] = "EmptyExpr";
|
|
/*
|
|
* An assignment to a temporary variable.
|
|
*/
|
|
ExpressionKind[ExpressionKind["AssignTemporaryExpr"] = 20] = "AssignTemporaryExpr";
|
|
/**
|
|
* A reference to a temporary variable.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ReadTemporaryExpr"] = 21] = "ReadTemporaryExpr";
|
|
/**
|
|
* An expression that will cause a literal slot index to be emitted.
|
|
*/
|
|
ExpressionKind[ExpressionKind["SlotLiteralExpr"] = 22] = "SlotLiteralExpr";
|
|
/**
|
|
* A test expression for a conditional op.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ConditionalCase"] = 23] = "ConditionalCase";
|
|
/**
|
|
* An expression that will be automatically extracted to the component const array.
|
|
*/
|
|
ExpressionKind[ExpressionKind["ConstCollected"] = 24] = "ConstCollected";
|
|
/**
|
|
* Operation that sets the value of a two-way binding.
|
|
*/
|
|
ExpressionKind[ExpressionKind["TwoWayBindingSet"] = 25] = "TwoWayBindingSet";
|
|
})(ExpressionKind || (ExpressionKind = {}));
|
|
var VariableFlags;
|
|
(function (VariableFlags) {
|
|
VariableFlags[VariableFlags["None"] = 0] = "None";
|
|
/**
|
|
* Always inline this variable, regardless of the number of times it's used.
|
|
* An `AlwaysInline` variable may not depend on context, because doing so may cause side effects
|
|
* that are illegal when multi-inlined. (The optimizer will enforce this constraint.)
|
|
*/
|
|
VariableFlags[VariableFlags["AlwaysInline"] = 1] = "AlwaysInline";
|
|
})(VariableFlags || (VariableFlags = {}));
|
|
/**
|
|
* Distinguishes between different kinds of `SemanticVariable`s.
|
|
*/
|
|
var SemanticVariableKind;
|
|
(function (SemanticVariableKind) {
|
|
/**
|
|
* Represents the context of a particular view.
|
|
*/
|
|
SemanticVariableKind[SemanticVariableKind["Context"] = 0] = "Context";
|
|
/**
|
|
* Represents an identifier declared in the lexical scope of a view.
|
|
*/
|
|
SemanticVariableKind[SemanticVariableKind["Identifier"] = 1] = "Identifier";
|
|
/**
|
|
* Represents a saved state that can be used to restore a view in a listener handler function.
|
|
*/
|
|
SemanticVariableKind[SemanticVariableKind["SavedView"] = 2] = "SavedView";
|
|
/**
|
|
* An alias generated by a special embedded view type (e.g. a `@for` block).
|
|
*/
|
|
SemanticVariableKind[SemanticVariableKind["Alias"] = 3] = "Alias";
|
|
})(SemanticVariableKind || (SemanticVariableKind = {}));
|
|
/**
|
|
* Whether to compile in compatibilty mode. In compatibility mode, the template pipeline will
|
|
* attempt to match the output of `TemplateDefinitionBuilder` as exactly as possible, at the cost
|
|
* of producing quirky or larger code in some cases.
|
|
*/
|
|
var CompatibilityMode;
|
|
(function (CompatibilityMode) {
|
|
CompatibilityMode[CompatibilityMode["Normal"] = 0] = "Normal";
|
|
CompatibilityMode[CompatibilityMode["TemplateDefinitionBuilder"] = 1] = "TemplateDefinitionBuilder";
|
|
})(CompatibilityMode || (CompatibilityMode = {}));
|
|
/**
|
|
* Enumeration of the types of attributes which can be applied to an element.
|
|
*/
|
|
var BindingKind;
|
|
(function (BindingKind) {
|
|
/**
|
|
* Static attributes.
|
|
*/
|
|
BindingKind[BindingKind["Attribute"] = 0] = "Attribute";
|
|
/**
|
|
* Class bindings.
|
|
*/
|
|
BindingKind[BindingKind["ClassName"] = 1] = "ClassName";
|
|
/**
|
|
* Style bindings.
|
|
*/
|
|
BindingKind[BindingKind["StyleProperty"] = 2] = "StyleProperty";
|
|
/**
|
|
* Dynamic property bindings.
|
|
*/
|
|
BindingKind[BindingKind["Property"] = 3] = "Property";
|
|
/**
|
|
* Property or attribute bindings on a template.
|
|
*/
|
|
BindingKind[BindingKind["Template"] = 4] = "Template";
|
|
/**
|
|
* Internationalized attributes.
|
|
*/
|
|
BindingKind[BindingKind["I18n"] = 5] = "I18n";
|
|
/**
|
|
* Legacy animation property bindings.
|
|
*/
|
|
BindingKind[BindingKind["LegacyAnimation"] = 6] = "LegacyAnimation";
|
|
/**
|
|
* Property side of a two-way binding.
|
|
*/
|
|
BindingKind[BindingKind["TwoWayProperty"] = 7] = "TwoWayProperty";
|
|
/**
|
|
* Property side of an animation binding.
|
|
*/
|
|
BindingKind[BindingKind["Animation"] = 8] = "Animation";
|
|
})(BindingKind || (BindingKind = {}));
|
|
/**
|
|
* Enumeration of possible times i18n params can be resolved.
|
|
*/
|
|
var I18nParamResolutionTime;
|
|
(function (I18nParamResolutionTime) {
|
|
/**
|
|
* Param is resolved at message creation time. Most params should be resolved at message creation
|
|
* time. However, ICU params need to be handled in post-processing.
|
|
*/
|
|
I18nParamResolutionTime[I18nParamResolutionTime["Creation"] = 0] = "Creation";
|
|
/**
|
|
* Param is resolved during post-processing. This should be used for params whose value comes from
|
|
* an ICU.
|
|
*/
|
|
I18nParamResolutionTime[I18nParamResolutionTime["Postproccessing"] = 1] = "Postproccessing";
|
|
})(I18nParamResolutionTime || (I18nParamResolutionTime = {}));
|
|
/**
|
|
* The contexts in which an i18n expression can be used.
|
|
*/
|
|
var I18nExpressionFor;
|
|
(function (I18nExpressionFor) {
|
|
/**
|
|
* This expression is used as a value (i.e. inside an i18n block).
|
|
*/
|
|
I18nExpressionFor[I18nExpressionFor["I18nText"] = 0] = "I18nText";
|
|
/**
|
|
* This expression is used in a binding.
|
|
*/
|
|
I18nExpressionFor[I18nExpressionFor["I18nAttribute"] = 1] = "I18nAttribute";
|
|
})(I18nExpressionFor || (I18nExpressionFor = {}));
|
|
/**
|
|
* Flags that describe what an i18n param value. These determine how the value is serialized into
|
|
* the final map.
|
|
*/
|
|
var I18nParamValueFlags;
|
|
(function (I18nParamValueFlags) {
|
|
I18nParamValueFlags[I18nParamValueFlags["None"] = 0] = "None";
|
|
/**
|
|
* This value represents an element tag.
|
|
*/
|
|
I18nParamValueFlags[I18nParamValueFlags["ElementTag"] = 1] = "ElementTag";
|
|
/**
|
|
* This value represents a template tag.
|
|
*/
|
|
I18nParamValueFlags[I18nParamValueFlags["TemplateTag"] = 2] = "TemplateTag";
|
|
/**
|
|
* This value represents the opening of a tag.
|
|
*/
|
|
I18nParamValueFlags[I18nParamValueFlags["OpenTag"] = 4] = "OpenTag";
|
|
/**
|
|
* This value represents the closing of a tag.
|
|
*/
|
|
I18nParamValueFlags[I18nParamValueFlags["CloseTag"] = 8] = "CloseTag";
|
|
/**
|
|
* This value represents an i18n expression index.
|
|
*/
|
|
I18nParamValueFlags[I18nParamValueFlags["ExpressionIndex"] = 16] = "ExpressionIndex";
|
|
})(I18nParamValueFlags || (I18nParamValueFlags = {}));
|
|
/**
|
|
* Whether the active namespace is HTML, MathML, or SVG mode.
|
|
*/
|
|
var Namespace;
|
|
(function (Namespace) {
|
|
Namespace[Namespace["HTML"] = 0] = "HTML";
|
|
Namespace[Namespace["SVG"] = 1] = "SVG";
|
|
Namespace[Namespace["Math"] = 2] = "Math";
|
|
})(Namespace || (Namespace = {}));
|
|
/**
|
|
* The type of a `@defer` trigger, for use in the ir.
|
|
*/
|
|
var DeferTriggerKind;
|
|
(function (DeferTriggerKind) {
|
|
DeferTriggerKind[DeferTriggerKind["Idle"] = 0] = "Idle";
|
|
DeferTriggerKind[DeferTriggerKind["Immediate"] = 1] = "Immediate";
|
|
DeferTriggerKind[DeferTriggerKind["Timer"] = 2] = "Timer";
|
|
DeferTriggerKind[DeferTriggerKind["Hover"] = 3] = "Hover";
|
|
DeferTriggerKind[DeferTriggerKind["Interaction"] = 4] = "Interaction";
|
|
DeferTriggerKind[DeferTriggerKind["Viewport"] = 5] = "Viewport";
|
|
DeferTriggerKind[DeferTriggerKind["Never"] = 6] = "Never";
|
|
})(DeferTriggerKind || (DeferTriggerKind = {}));
|
|
/**
|
|
* Kinds of i18n contexts. They can be created because of root i18n blocks, or ICUs.
|
|
*/
|
|
var I18nContextKind;
|
|
(function (I18nContextKind) {
|
|
I18nContextKind[I18nContextKind["RootI18n"] = 0] = "RootI18n";
|
|
I18nContextKind[I18nContextKind["Icu"] = 1] = "Icu";
|
|
I18nContextKind[I18nContextKind["Attr"] = 2] = "Attr";
|
|
})(I18nContextKind || (I18nContextKind = {}));
|
|
var TemplateKind;
|
|
(function (TemplateKind) {
|
|
TemplateKind[TemplateKind["NgTemplate"] = 0] = "NgTemplate";
|
|
TemplateKind[TemplateKind["Structural"] = 1] = "Structural";
|
|
TemplateKind[TemplateKind["Block"] = 2] = "Block";
|
|
})(TemplateKind || (TemplateKind = {}));
|
|
|
|
/**
|
|
* Marker symbol for `ConsumesSlotOpTrait`.
|
|
*/
|
|
const ConsumesSlot = Symbol('ConsumesSlot');
|
|
/**
|
|
* Marker symbol for `DependsOnSlotContextOpTrait`.
|
|
*/
|
|
const DependsOnSlotContext = Symbol('DependsOnSlotContext');
|
|
/**
|
|
* Marker symbol for `ConsumesVars` trait.
|
|
*/
|
|
const ConsumesVarsTrait = Symbol('ConsumesVars');
|
|
/**
|
|
* Marker symbol for `UsesVarOffset` trait.
|
|
*/
|
|
const UsesVarOffset = Symbol('UsesVarOffset');
|
|
/**
|
|
* Default values for most `ConsumesSlotOpTrait` fields (used with the spread operator to initialize
|
|
* implementors of the trait).
|
|
*/
|
|
const TRAIT_CONSUMES_SLOT = {
|
|
[ConsumesSlot]: true,
|
|
numSlotsUsed: 1,
|
|
};
|
|
/**
|
|
* Default values for most `DependsOnSlotContextOpTrait` fields (used with the spread operator to
|
|
* initialize implementors of the trait).
|
|
*/
|
|
const TRAIT_DEPENDS_ON_SLOT_CONTEXT = {
|
|
[DependsOnSlotContext]: true,
|
|
};
|
|
/**
|
|
* Default values for `UsesVars` fields (used with the spread operator to initialize
|
|
* implementors of the trait).
|
|
*/
|
|
const TRAIT_CONSUMES_VARS = {
|
|
[ConsumesVarsTrait]: true,
|
|
};
|
|
/**
|
|
* Test whether an operation implements `ConsumesSlotOpTrait`.
|
|
*/
|
|
function hasConsumesSlotTrait(op) {
|
|
return op[ConsumesSlot] === true;
|
|
}
|
|
function hasDependsOnSlotContextTrait(value) {
|
|
return value[DependsOnSlotContext] === true;
|
|
}
|
|
function hasConsumesVarsTrait(value) {
|
|
return value[ConsumesVarsTrait] === true;
|
|
}
|
|
/**
|
|
* Test whether an expression implements `UsesVarOffsetTrait`.
|
|
*/
|
|
function hasUsesVarOffsetTrait(expr) {
|
|
return expr[UsesVarOffset] === true;
|
|
}
|
|
|
|
/**
|
|
* Create a `StatementOp`.
|
|
*/
|
|
function createStatementOp(statement) {
|
|
return {
|
|
kind: OpKind.Statement,
|
|
statement,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `VariableOp`.
|
|
*/
|
|
function createVariableOp(xref, variable, initializer, flags) {
|
|
return {
|
|
kind: OpKind.Variable,
|
|
xref,
|
|
variable,
|
|
initializer,
|
|
flags,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Static structure shared by all operations.
|
|
*
|
|
* Used as a convenience via the spread operator (`...NEW_OP`) when creating new operations, and
|
|
* ensures the fields are always in the same order.
|
|
*/
|
|
const NEW_OP = {
|
|
debugListId: null,
|
|
prev: null,
|
|
next: null,
|
|
};
|
|
|
|
/**
|
|
* Create an `InterpolationTextOp`.
|
|
*/
|
|
function createInterpolateTextOp(xref, interpolation, sourceSpan) {
|
|
return {
|
|
kind: OpKind.InterpolateText,
|
|
target: xref,
|
|
interpolation,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
class Interpolation {
|
|
strings;
|
|
expressions;
|
|
i18nPlaceholders;
|
|
constructor(strings, expressions, i18nPlaceholders) {
|
|
this.strings = strings;
|
|
this.expressions = expressions;
|
|
this.i18nPlaceholders = i18nPlaceholders;
|
|
if (i18nPlaceholders.length !== 0 && i18nPlaceholders.length !== expressions.length) {
|
|
throw new Error(`Expected ${expressions.length} placeholders to match interpolation expression count, but got ${i18nPlaceholders.length}`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Create a `BindingOp`, not yet transformed into a particular type of binding.
|
|
*/
|
|
function createBindingOp(target, kind, name, expression, unit, securityContext, isTextAttribute, isStructuralTemplateAttribute, templateKind, i18nMessage, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Binding,
|
|
bindingKind: kind,
|
|
target,
|
|
name,
|
|
expression,
|
|
unit,
|
|
securityContext,
|
|
isTextAttribute,
|
|
isStructuralTemplateAttribute,
|
|
templateKind,
|
|
i18nContext: null,
|
|
i18nMessage,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `PropertyOp`.
|
|
*/
|
|
function createPropertyOp(target, name, expression, bindingKind, securityContext, isStructuralTemplateAttribute, templateKind, i18nContext, i18nMessage, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Property,
|
|
target,
|
|
name,
|
|
expression,
|
|
bindingKind,
|
|
securityContext,
|
|
sanitizer: null,
|
|
isStructuralTemplateAttribute,
|
|
templateKind,
|
|
i18nContext,
|
|
i18nMessage,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `TwoWayPropertyOp`.
|
|
*/
|
|
function createTwoWayPropertyOp(target, name, expression, securityContext, isStructuralTemplateAttribute, templateKind, i18nContext, i18nMessage, sourceSpan) {
|
|
return {
|
|
kind: OpKind.TwoWayProperty,
|
|
target,
|
|
name,
|
|
expression,
|
|
securityContext,
|
|
sanitizer: null,
|
|
isStructuralTemplateAttribute,
|
|
templateKind,
|
|
i18nContext,
|
|
i18nMessage,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/** Create a `StylePropOp`. */
|
|
function createStylePropOp(xref, name, expression, unit, sourceSpan) {
|
|
return {
|
|
kind: OpKind.StyleProp,
|
|
target: xref,
|
|
name,
|
|
expression,
|
|
unit,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `ClassPropOp`.
|
|
*/
|
|
function createClassPropOp(xref, name, expression, sourceSpan) {
|
|
return {
|
|
kind: OpKind.ClassProp,
|
|
target: xref,
|
|
name,
|
|
expression,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/** Create a `StyleMapOp`. */
|
|
function createStyleMapOp(xref, expression, sourceSpan) {
|
|
return {
|
|
kind: OpKind.StyleMap,
|
|
target: xref,
|
|
expression,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `ClassMapOp`.
|
|
*/
|
|
function createClassMapOp(xref, expression, sourceSpan) {
|
|
return {
|
|
kind: OpKind.ClassMap,
|
|
target: xref,
|
|
expression,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `AttributeOp`.
|
|
*/
|
|
function createAttributeOp(target, namespace, name, expression, securityContext, isTextAttribute, isStructuralTemplateAttribute, templateKind, i18nMessage, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Attribute,
|
|
target,
|
|
namespace,
|
|
name,
|
|
expression,
|
|
securityContext,
|
|
sanitizer: null,
|
|
isTextAttribute,
|
|
isStructuralTemplateAttribute,
|
|
templateKind,
|
|
i18nContext: null,
|
|
i18nMessage,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `AdvanceOp`.
|
|
*/
|
|
function createAdvanceOp(delta, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Advance,
|
|
delta,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a conditional op, which will display an embedded view according to a condtion.
|
|
*/
|
|
function createConditionalOp(target, test, conditions, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Conditional,
|
|
target,
|
|
test,
|
|
conditions,
|
|
processed: null,
|
|
sourceSpan,
|
|
contextValue: null,
|
|
...NEW_OP,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
};
|
|
}
|
|
function createRepeaterOp(repeaterCreate, targetSlot, collection, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Repeater,
|
|
target: repeaterCreate,
|
|
targetSlot,
|
|
collection,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `AnimationBindingOp`.
|
|
*/
|
|
function createAnimationBindingOp(name, target, animationKind, expression, securityContext, sourceSpan, animationBindingKind) {
|
|
return {
|
|
kind: OpKind.AnimationBinding,
|
|
name,
|
|
target,
|
|
animationKind,
|
|
expression,
|
|
i18nMessage: null,
|
|
securityContext,
|
|
sanitizer: null,
|
|
sourceSpan,
|
|
animationBindingKind,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createDeferWhenOp(target, expr, modifier, sourceSpan) {
|
|
return {
|
|
kind: OpKind.DeferWhen,
|
|
target,
|
|
expr,
|
|
modifier,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
};
|
|
}
|
|
/**
|
|
* Create an i18n expression op.
|
|
*/
|
|
function createI18nExpressionOp(context, target, i18nOwner, handle, expression, icuPlaceholder, i18nPlaceholder, resolutionTime, usage, name, sourceSpan) {
|
|
return {
|
|
kind: OpKind.I18nExpression,
|
|
context,
|
|
target,
|
|
i18nOwner,
|
|
handle,
|
|
expression,
|
|
icuPlaceholder,
|
|
i18nPlaceholder,
|
|
resolutionTime,
|
|
usage,
|
|
name,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
};
|
|
}
|
|
/**
|
|
* Creates an op to apply i18n expression ops.
|
|
*/
|
|
function createI18nApplyOp(owner, handle, sourceSpan) {
|
|
return {
|
|
kind: OpKind.I18nApply,
|
|
owner,
|
|
handle,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Creates a `StoreLetOp`.
|
|
*/
|
|
function createStoreLetOp(target, declaredName, value, sourceSpan) {
|
|
return {
|
|
kind: OpKind.StoreLet,
|
|
target,
|
|
declaredName,
|
|
value,
|
|
sourceSpan,
|
|
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check whether a given `o.Expression` is a logical IR expression type.
|
|
*/
|
|
function isIrExpression(expr) {
|
|
return expr instanceof ExpressionBase;
|
|
}
|
|
/**
|
|
* Base type used for all logical IR expressions.
|
|
*/
|
|
class ExpressionBase extends Expression {
|
|
constructor(sourceSpan = null) {
|
|
super(null, sourceSpan);
|
|
}
|
|
}
|
|
/**
|
|
* Logical expression representing a lexical read of a variable name.
|
|
*/
|
|
class LexicalReadExpr extends ExpressionBase {
|
|
name;
|
|
kind = ExpressionKind.LexicalRead;
|
|
constructor(name) {
|
|
super();
|
|
this.name = name;
|
|
}
|
|
visitExpression(visitor, context) { }
|
|
isEquivalent(other) {
|
|
// We assume that the lexical reads are in the same context, which must be true for parent
|
|
// expressions to be equivalent.
|
|
// TODO: is this generally safe?
|
|
return this.name === other.name;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new LexicalReadExpr(this.name);
|
|
}
|
|
}
|
|
/**
|
|
* Runtime operation to retrieve the value of a local reference.
|
|
*/
|
|
class ReferenceExpr extends ExpressionBase {
|
|
target;
|
|
targetSlot;
|
|
offset;
|
|
kind = ExpressionKind.Reference;
|
|
constructor(target, targetSlot, offset) {
|
|
super();
|
|
this.target = target;
|
|
this.targetSlot = targetSlot;
|
|
this.offset = offset;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof ReferenceExpr && e.target === this.target;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new ReferenceExpr(this.target, this.targetSlot, this.offset);
|
|
}
|
|
}
|
|
class StoreLetExpr extends ExpressionBase {
|
|
target;
|
|
value;
|
|
sourceSpan;
|
|
kind = ExpressionKind.StoreLet;
|
|
[ConsumesVarsTrait] = true;
|
|
[DependsOnSlotContext] = true;
|
|
constructor(target, value, sourceSpan) {
|
|
super();
|
|
this.target = target;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return (e instanceof StoreLetExpr && e.target === this.target && e.value.isEquivalent(this.value));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.value = transformExpressionsInExpression(this.value, transform, flags);
|
|
}
|
|
clone() {
|
|
return new StoreLetExpr(this.target, this.value, this.sourceSpan);
|
|
}
|
|
}
|
|
class ContextLetReferenceExpr extends ExpressionBase {
|
|
target;
|
|
targetSlot;
|
|
kind = ExpressionKind.ContextLetReference;
|
|
constructor(target, targetSlot) {
|
|
super();
|
|
this.target = target;
|
|
this.targetSlot = targetSlot;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof ContextLetReferenceExpr && e.target === this.target;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new ContextLetReferenceExpr(this.target, this.targetSlot);
|
|
}
|
|
}
|
|
/**
|
|
* A reference to the current view context (usually the `ctx` variable in a template function).
|
|
*/
|
|
class ContextExpr extends ExpressionBase {
|
|
view;
|
|
kind = ExpressionKind.Context;
|
|
constructor(view) {
|
|
super();
|
|
this.view = view;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof ContextExpr && e.view === this.view;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new ContextExpr(this.view);
|
|
}
|
|
}
|
|
/**
|
|
* A reference to the current view context inside a track function.
|
|
*/
|
|
class TrackContextExpr extends ExpressionBase {
|
|
view;
|
|
kind = ExpressionKind.TrackContext;
|
|
constructor(view) {
|
|
super();
|
|
this.view = view;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof TrackContextExpr && e.view === this.view;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new TrackContextExpr(this.view);
|
|
}
|
|
}
|
|
/**
|
|
* Runtime operation to navigate to the next view context in the view hierarchy.
|
|
*/
|
|
class NextContextExpr extends ExpressionBase {
|
|
kind = ExpressionKind.NextContext;
|
|
steps = 1;
|
|
constructor() {
|
|
super();
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof NextContextExpr && e.steps === this.steps;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
const expr = new NextContextExpr();
|
|
expr.steps = this.steps;
|
|
return expr;
|
|
}
|
|
}
|
|
/**
|
|
* Runtime operation to snapshot the current view context.
|
|
*
|
|
* The result of this operation can be stored in a variable and later used with the `RestoreView`
|
|
* operation.
|
|
*/
|
|
class GetCurrentViewExpr extends ExpressionBase {
|
|
kind = ExpressionKind.GetCurrentView;
|
|
constructor() {
|
|
super();
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(e) {
|
|
return e instanceof GetCurrentViewExpr;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new GetCurrentViewExpr();
|
|
}
|
|
}
|
|
/**
|
|
* Runtime operation to restore a snapshotted view.
|
|
*/
|
|
class RestoreViewExpr extends ExpressionBase {
|
|
view;
|
|
kind = ExpressionKind.RestoreView;
|
|
constructor(view) {
|
|
super();
|
|
this.view = view;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
if (typeof this.view !== 'number') {
|
|
this.view.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent(e) {
|
|
if (!(e instanceof RestoreViewExpr) || typeof e.view !== typeof this.view) {
|
|
return false;
|
|
}
|
|
if (typeof this.view === 'number') {
|
|
return this.view === e.view;
|
|
}
|
|
else {
|
|
return this.view.isEquivalent(e.view);
|
|
}
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
if (typeof this.view !== 'number') {
|
|
this.view = transformExpressionsInExpression(this.view, transform, flags);
|
|
}
|
|
}
|
|
clone() {
|
|
return new RestoreViewExpr(this.view instanceof Expression ? this.view.clone() : this.view);
|
|
}
|
|
}
|
|
/**
|
|
* Runtime operation to reset the current view context after `RestoreView`.
|
|
*/
|
|
class ResetViewExpr extends ExpressionBase {
|
|
expr;
|
|
kind = ExpressionKind.ResetView;
|
|
constructor(expr) {
|
|
super();
|
|
this.expr = expr;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.expr.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof ResetViewExpr && this.expr.isEquivalent(e.expr);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.expr = transformExpressionsInExpression(this.expr, transform, flags);
|
|
}
|
|
clone() {
|
|
return new ResetViewExpr(this.expr.clone());
|
|
}
|
|
}
|
|
class TwoWayBindingSetExpr extends ExpressionBase {
|
|
target;
|
|
value;
|
|
kind = ExpressionKind.TwoWayBindingSet;
|
|
constructor(target, value) {
|
|
super();
|
|
this.target = target;
|
|
this.value = value;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.target.visitExpression(visitor, context);
|
|
this.value.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent(other) {
|
|
return this.target.isEquivalent(other.target) && this.value.isEquivalent(other.value);
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.target = transformExpressionsInExpression(this.target, transform, flags);
|
|
this.value = transformExpressionsInExpression(this.value, transform, flags);
|
|
}
|
|
clone() {
|
|
return new TwoWayBindingSetExpr(this.target, this.value);
|
|
}
|
|
}
|
|
/**
|
|
* Read of a variable declared as an `ir.VariableOp` and referenced through its `ir.XrefId`.
|
|
*/
|
|
class ReadVariableExpr extends ExpressionBase {
|
|
xref;
|
|
kind = ExpressionKind.ReadVariable;
|
|
name = null;
|
|
constructor(xref) {
|
|
super();
|
|
this.xref = xref;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(other) {
|
|
return other instanceof ReadVariableExpr && other.xref === this.xref;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
const expr = new ReadVariableExpr(this.xref);
|
|
expr.name = this.name;
|
|
return expr;
|
|
}
|
|
}
|
|
class PureFunctionExpr extends ExpressionBase {
|
|
kind = ExpressionKind.PureFunctionExpr;
|
|
[ConsumesVarsTrait] = true;
|
|
[UsesVarOffset] = true;
|
|
varOffset = null;
|
|
/**
|
|
* The expression which should be memoized as a pure computation.
|
|
*
|
|
* This expression contains internal `PureFunctionParameterExpr`s, which are placeholders for the
|
|
* positional argument expressions in `args.
|
|
*/
|
|
body;
|
|
/**
|
|
* Positional arguments to the pure function which will memoize the `body` expression, which act
|
|
* as memoization keys.
|
|
*/
|
|
args;
|
|
/**
|
|
* Once extracted to the `ConstantPool`, a reference to the function which defines the computation
|
|
* of `body`.
|
|
*/
|
|
fn = null;
|
|
constructor(expression, args) {
|
|
super();
|
|
this.body = expression;
|
|
this.args = args;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.body?.visitExpression(visitor, context);
|
|
for (const arg of this.args) {
|
|
arg.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent(other) {
|
|
if (!(other instanceof PureFunctionExpr) || other.args.length !== this.args.length) {
|
|
return false;
|
|
}
|
|
return (other.body !== null &&
|
|
this.body !== null &&
|
|
other.body.isEquivalent(this.body) &&
|
|
other.args.every((arg, idx) => arg.isEquivalent(this.args[idx])));
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
if (this.body !== null) {
|
|
// TODO: figure out if this is the right flag to pass here.
|
|
this.body = transformExpressionsInExpression(this.body, transform, flags | VisitorContextFlag.InChildOperation);
|
|
}
|
|
else if (this.fn !== null) {
|
|
this.fn = transformExpressionsInExpression(this.fn, transform, flags);
|
|
}
|
|
for (let i = 0; i < this.args.length; i++) {
|
|
this.args[i] = transformExpressionsInExpression(this.args[i], transform, flags);
|
|
}
|
|
}
|
|
clone() {
|
|
const expr = new PureFunctionExpr(this.body?.clone() ?? null, this.args.map((arg) => arg.clone()));
|
|
expr.fn = this.fn?.clone() ?? null;
|
|
expr.varOffset = this.varOffset;
|
|
return expr;
|
|
}
|
|
}
|
|
class PureFunctionParameterExpr extends ExpressionBase {
|
|
index;
|
|
kind = ExpressionKind.PureFunctionParameterExpr;
|
|
constructor(index) {
|
|
super();
|
|
this.index = index;
|
|
}
|
|
visitExpression() { }
|
|
isEquivalent(other) {
|
|
return other instanceof PureFunctionParameterExpr && other.index === this.index;
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
transformInternalExpressions() { }
|
|
clone() {
|
|
return new PureFunctionParameterExpr(this.index);
|
|
}
|
|
}
|
|
class PipeBindingExpr extends ExpressionBase {
|
|
target;
|
|
targetSlot;
|
|
name;
|
|
args;
|
|
kind = ExpressionKind.PipeBinding;
|
|
[ConsumesVarsTrait] = true;
|
|
[UsesVarOffset] = true;
|
|
varOffset = null;
|
|
constructor(target, targetSlot, name, args) {
|
|
super();
|
|
this.target = target;
|
|
this.targetSlot = targetSlot;
|
|
this.name = name;
|
|
this.args = args;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
for (const arg of this.args) {
|
|
arg.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
for (let idx = 0; idx < this.args.length; idx++) {
|
|
this.args[idx] = transformExpressionsInExpression(this.args[idx], transform, flags);
|
|
}
|
|
}
|
|
clone() {
|
|
const r = new PipeBindingExpr(this.target, this.targetSlot, this.name, this.args.map((a) => a.clone()));
|
|
r.varOffset = this.varOffset;
|
|
return r;
|
|
}
|
|
}
|
|
class PipeBindingVariadicExpr extends ExpressionBase {
|
|
target;
|
|
targetSlot;
|
|
name;
|
|
args;
|
|
numArgs;
|
|
kind = ExpressionKind.PipeBindingVariadic;
|
|
[ConsumesVarsTrait] = true;
|
|
[UsesVarOffset] = true;
|
|
varOffset = null;
|
|
constructor(target, targetSlot, name, args, numArgs) {
|
|
super();
|
|
this.target = target;
|
|
this.targetSlot = targetSlot;
|
|
this.name = name;
|
|
this.args = args;
|
|
this.numArgs = numArgs;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.args.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.args = transformExpressionsInExpression(this.args, transform, flags);
|
|
}
|
|
clone() {
|
|
const r = new PipeBindingVariadicExpr(this.target, this.targetSlot, this.name, this.args.clone(), this.numArgs);
|
|
r.varOffset = this.varOffset;
|
|
return r;
|
|
}
|
|
}
|
|
class SafePropertyReadExpr extends ExpressionBase {
|
|
receiver;
|
|
name;
|
|
kind = ExpressionKind.SafePropertyRead;
|
|
constructor(receiver, name) {
|
|
super();
|
|
this.receiver = receiver;
|
|
this.name = name;
|
|
}
|
|
// An alias for name, which allows other logic to handle property reads and keyed reads together.
|
|
get index() {
|
|
return this.name;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.receiver.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.receiver = transformExpressionsInExpression(this.receiver, transform, flags);
|
|
}
|
|
clone() {
|
|
return new SafePropertyReadExpr(this.receiver.clone(), this.name);
|
|
}
|
|
}
|
|
class SafeKeyedReadExpr extends ExpressionBase {
|
|
receiver;
|
|
index;
|
|
kind = ExpressionKind.SafeKeyedRead;
|
|
constructor(receiver, index, sourceSpan) {
|
|
super(sourceSpan);
|
|
this.receiver = receiver;
|
|
this.index = index;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.receiver.visitExpression(visitor, context);
|
|
this.index.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.receiver = transformExpressionsInExpression(this.receiver, transform, flags);
|
|
this.index = transformExpressionsInExpression(this.index, transform, flags);
|
|
}
|
|
clone() {
|
|
return new SafeKeyedReadExpr(this.receiver.clone(), this.index.clone(), this.sourceSpan);
|
|
}
|
|
}
|
|
class SafeInvokeFunctionExpr extends ExpressionBase {
|
|
receiver;
|
|
args;
|
|
kind = ExpressionKind.SafeInvokeFunction;
|
|
constructor(receiver, args) {
|
|
super();
|
|
this.receiver = receiver;
|
|
this.args = args;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.receiver.visitExpression(visitor, context);
|
|
for (const a of this.args) {
|
|
a.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.receiver = transformExpressionsInExpression(this.receiver, transform, flags);
|
|
for (let i = 0; i < this.args.length; i++) {
|
|
this.args[i] = transformExpressionsInExpression(this.args[i], transform, flags);
|
|
}
|
|
}
|
|
clone() {
|
|
return new SafeInvokeFunctionExpr(this.receiver.clone(), this.args.map((a) => a.clone()));
|
|
}
|
|
}
|
|
class SafeTernaryExpr extends ExpressionBase {
|
|
guard;
|
|
expr;
|
|
kind = ExpressionKind.SafeTernaryExpr;
|
|
constructor(guard, expr) {
|
|
super();
|
|
this.guard = guard;
|
|
this.expr = expr;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.guard.visitExpression(visitor, context);
|
|
this.expr.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.guard = transformExpressionsInExpression(this.guard, transform, flags);
|
|
this.expr = transformExpressionsInExpression(this.expr, transform, flags);
|
|
}
|
|
clone() {
|
|
return new SafeTernaryExpr(this.guard.clone(), this.expr.clone());
|
|
}
|
|
}
|
|
class EmptyExpr extends ExpressionBase {
|
|
kind = ExpressionKind.EmptyExpr;
|
|
visitExpression(visitor, context) { }
|
|
isEquivalent(e) {
|
|
return e instanceof EmptyExpr;
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
clone() {
|
|
return new EmptyExpr();
|
|
}
|
|
transformInternalExpressions() { }
|
|
}
|
|
class AssignTemporaryExpr extends ExpressionBase {
|
|
expr;
|
|
xref;
|
|
kind = ExpressionKind.AssignTemporaryExpr;
|
|
name = null;
|
|
constructor(expr, xref) {
|
|
super();
|
|
this.expr = expr;
|
|
this.xref = xref;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.expr.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent() {
|
|
return false;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.expr = transformExpressionsInExpression(this.expr, transform, flags);
|
|
}
|
|
clone() {
|
|
const a = new AssignTemporaryExpr(this.expr.clone(), this.xref);
|
|
a.name = this.name;
|
|
return a;
|
|
}
|
|
}
|
|
class ReadTemporaryExpr extends ExpressionBase {
|
|
xref;
|
|
kind = ExpressionKind.ReadTemporaryExpr;
|
|
name = null;
|
|
constructor(xref) {
|
|
super();
|
|
this.xref = xref;
|
|
}
|
|
visitExpression(visitor, context) { }
|
|
isEquivalent() {
|
|
return this.xref === this.xref;
|
|
}
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
transformInternalExpressions(transform, flags) { }
|
|
clone() {
|
|
const r = new ReadTemporaryExpr(this.xref);
|
|
r.name = this.name;
|
|
return r;
|
|
}
|
|
}
|
|
class SlotLiteralExpr extends ExpressionBase {
|
|
slot;
|
|
kind = ExpressionKind.SlotLiteralExpr;
|
|
constructor(slot) {
|
|
super();
|
|
this.slot = slot;
|
|
}
|
|
visitExpression(visitor, context) { }
|
|
isEquivalent(e) {
|
|
return e instanceof SlotLiteralExpr && e.slot === this.slot;
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
clone() {
|
|
return new SlotLiteralExpr(this.slot);
|
|
}
|
|
transformInternalExpressions() { }
|
|
}
|
|
class ConditionalCaseExpr extends ExpressionBase {
|
|
expr;
|
|
target;
|
|
targetSlot;
|
|
alias;
|
|
kind = ExpressionKind.ConditionalCase;
|
|
/**
|
|
* Create an expression for one branch of a conditional.
|
|
* @param expr The expression to be tested for this case. Might be null, as in an `else` case.
|
|
* @param target The Xref of the view to be displayed if this condition is true.
|
|
*/
|
|
constructor(expr, target, targetSlot, alias = null) {
|
|
super();
|
|
this.expr = expr;
|
|
this.target = target;
|
|
this.targetSlot = targetSlot;
|
|
this.alias = alias;
|
|
}
|
|
visitExpression(visitor, context) {
|
|
if (this.expr !== null) {
|
|
this.expr.visitExpression(visitor, context);
|
|
}
|
|
}
|
|
isEquivalent(e) {
|
|
return e instanceof ConditionalCaseExpr && e.expr === this.expr;
|
|
}
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
clone() {
|
|
return new ConditionalCaseExpr(this.expr, this.target, this.targetSlot);
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
if (this.expr !== null) {
|
|
this.expr = transformExpressionsInExpression(this.expr, transform, flags);
|
|
}
|
|
}
|
|
}
|
|
class ConstCollectedExpr extends ExpressionBase {
|
|
expr;
|
|
kind = ExpressionKind.ConstCollected;
|
|
constructor(expr) {
|
|
super();
|
|
this.expr = expr;
|
|
}
|
|
transformInternalExpressions(transform, flags) {
|
|
this.expr = transform(this.expr, flags);
|
|
}
|
|
visitExpression(visitor, context) {
|
|
this.expr.visitExpression(visitor, context);
|
|
}
|
|
isEquivalent(e) {
|
|
if (!(e instanceof ConstCollectedExpr)) {
|
|
return false;
|
|
}
|
|
return this.expr.isEquivalent(e.expr);
|
|
}
|
|
isConstant() {
|
|
return this.expr.isConstant();
|
|
}
|
|
clone() {
|
|
return new ConstCollectedExpr(this.expr);
|
|
}
|
|
}
|
|
/**
|
|
* Visits all `Expression`s in the AST of `op` with the `visitor` function.
|
|
*/
|
|
function visitExpressionsInOp(op, visitor) {
|
|
transformExpressionsInOp(op, (expr, flags) => {
|
|
visitor(expr, flags);
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
}
|
|
var VisitorContextFlag;
|
|
(function (VisitorContextFlag) {
|
|
VisitorContextFlag[VisitorContextFlag["None"] = 0] = "None";
|
|
VisitorContextFlag[VisitorContextFlag["InChildOperation"] = 1] = "InChildOperation";
|
|
})(VisitorContextFlag || (VisitorContextFlag = {}));
|
|
function transformExpressionsInInterpolation(interpolation, transform, flags) {
|
|
for (let i = 0; i < interpolation.expressions.length; i++) {
|
|
interpolation.expressions[i] = transformExpressionsInExpression(interpolation.expressions[i], transform, flags);
|
|
}
|
|
}
|
|
/**
|
|
* Transform all `Expression`s in the AST of `op` with the `transform` function.
|
|
*
|
|
* All such operations will be replaced with the result of applying `transform`, which may be an
|
|
* identity transformation.
|
|
*/
|
|
function transformExpressionsInOp(op, transform, flags) {
|
|
switch (op.kind) {
|
|
case OpKind.StyleProp:
|
|
case OpKind.StyleMap:
|
|
case OpKind.ClassProp:
|
|
case OpKind.ClassMap:
|
|
case OpKind.AnimationString:
|
|
case OpKind.AnimationBinding:
|
|
case OpKind.Binding:
|
|
if (op.expression instanceof Interpolation) {
|
|
transformExpressionsInInterpolation(op.expression, transform, flags);
|
|
}
|
|
else {
|
|
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
|
|
}
|
|
break;
|
|
case OpKind.Property:
|
|
case OpKind.DomProperty:
|
|
case OpKind.Attribute:
|
|
if (op.expression instanceof Interpolation) {
|
|
transformExpressionsInInterpolation(op.expression, transform, flags);
|
|
}
|
|
else {
|
|
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
|
|
}
|
|
op.sanitizer =
|
|
op.sanitizer && transformExpressionsInExpression(op.sanitizer, transform, flags);
|
|
break;
|
|
case OpKind.TwoWayProperty:
|
|
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
|
|
op.sanitizer =
|
|
op.sanitizer && transformExpressionsInExpression(op.sanitizer, transform, flags);
|
|
break;
|
|
case OpKind.I18nExpression:
|
|
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
|
|
break;
|
|
case OpKind.InterpolateText:
|
|
transformExpressionsInInterpolation(op.interpolation, transform, flags);
|
|
break;
|
|
case OpKind.Statement:
|
|
transformExpressionsInStatement(op.statement, transform, flags);
|
|
break;
|
|
case OpKind.Variable:
|
|
op.initializer = transformExpressionsInExpression(op.initializer, transform, flags);
|
|
break;
|
|
case OpKind.Conditional:
|
|
for (const condition of op.conditions) {
|
|
if (condition.expr === null) {
|
|
// This is a default case.
|
|
continue;
|
|
}
|
|
condition.expr = transformExpressionsInExpression(condition.expr, transform, flags);
|
|
}
|
|
if (op.processed !== null) {
|
|
op.processed = transformExpressionsInExpression(op.processed, transform, flags);
|
|
}
|
|
if (op.contextValue !== null) {
|
|
op.contextValue = transformExpressionsInExpression(op.contextValue, transform, flags);
|
|
}
|
|
break;
|
|
case OpKind.Animation:
|
|
case OpKind.AnimationListener:
|
|
case OpKind.Listener:
|
|
case OpKind.TwoWayListener:
|
|
for (const innerOp of op.handlerOps) {
|
|
transformExpressionsInOp(innerOp, transform, flags | VisitorContextFlag.InChildOperation);
|
|
}
|
|
break;
|
|
case OpKind.ExtractedAttribute:
|
|
op.expression =
|
|
op.expression && transformExpressionsInExpression(op.expression, transform, flags);
|
|
op.trustedValueFn =
|
|
op.trustedValueFn && transformExpressionsInExpression(op.trustedValueFn, transform, flags);
|
|
break;
|
|
case OpKind.RepeaterCreate:
|
|
if (op.trackByOps === null) {
|
|
op.track = transformExpressionsInExpression(op.track, transform, flags);
|
|
}
|
|
else {
|
|
for (const innerOp of op.trackByOps) {
|
|
transformExpressionsInOp(innerOp, transform, flags | VisitorContextFlag.InChildOperation);
|
|
}
|
|
}
|
|
if (op.trackByFn !== null) {
|
|
op.trackByFn = transformExpressionsInExpression(op.trackByFn, transform, flags);
|
|
}
|
|
break;
|
|
case OpKind.Repeater:
|
|
op.collection = transformExpressionsInExpression(op.collection, transform, flags);
|
|
break;
|
|
case OpKind.Defer:
|
|
if (op.loadingConfig !== null) {
|
|
op.loadingConfig = transformExpressionsInExpression(op.loadingConfig, transform, flags);
|
|
}
|
|
if (op.placeholderConfig !== null) {
|
|
op.placeholderConfig = transformExpressionsInExpression(op.placeholderConfig, transform, flags);
|
|
}
|
|
if (op.resolverFn !== null) {
|
|
op.resolverFn = transformExpressionsInExpression(op.resolverFn, transform, flags);
|
|
}
|
|
break;
|
|
case OpKind.I18nMessage:
|
|
for (const [placeholder, expr] of op.params) {
|
|
op.params.set(placeholder, transformExpressionsInExpression(expr, transform, flags));
|
|
}
|
|
for (const [placeholder, expr] of op.postprocessingParams) {
|
|
op.postprocessingParams.set(placeholder, transformExpressionsInExpression(expr, transform, flags));
|
|
}
|
|
break;
|
|
case OpKind.DeferWhen:
|
|
op.expr = transformExpressionsInExpression(op.expr, transform, flags);
|
|
break;
|
|
case OpKind.StoreLet:
|
|
op.value = transformExpressionsInExpression(op.value, transform, flags);
|
|
break;
|
|
case OpKind.Advance:
|
|
case OpKind.Container:
|
|
case OpKind.ContainerEnd:
|
|
case OpKind.ContainerStart:
|
|
case OpKind.DeferOn:
|
|
case OpKind.DisableBindings:
|
|
case OpKind.Element:
|
|
case OpKind.ElementEnd:
|
|
case OpKind.ElementStart:
|
|
case OpKind.EnableBindings:
|
|
case OpKind.I18n:
|
|
case OpKind.I18nApply:
|
|
case OpKind.I18nContext:
|
|
case OpKind.I18nEnd:
|
|
case OpKind.I18nStart:
|
|
case OpKind.IcuEnd:
|
|
case OpKind.IcuStart:
|
|
case OpKind.Namespace:
|
|
case OpKind.Pipe:
|
|
case OpKind.Projection:
|
|
case OpKind.ProjectionDef:
|
|
case OpKind.Template:
|
|
case OpKind.Text:
|
|
case OpKind.I18nAttributes:
|
|
case OpKind.IcuPlaceholder:
|
|
case OpKind.DeclareLet:
|
|
case OpKind.SourceLocation:
|
|
case OpKind.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
// These operations contain no expressions.
|
|
break;
|
|
default:
|
|
throw new Error(`AssertionError: transformExpressionsInOp doesn't handle ${OpKind[op.kind]}`);
|
|
}
|
|
}
|
|
/**
|
|
* Transform all `Expression`s in the AST of `expr` with the `transform` function.
|
|
*
|
|
* All such operations will be replaced with the result of applying `transform`, which may be an
|
|
* identity transformation.
|
|
*/
|
|
function transformExpressionsInExpression(expr, transform, flags) {
|
|
if (expr instanceof ExpressionBase) {
|
|
expr.transformInternalExpressions(transform, flags);
|
|
}
|
|
else if (expr instanceof BinaryOperatorExpr) {
|
|
expr.lhs = transformExpressionsInExpression(expr.lhs, transform, flags);
|
|
expr.rhs = transformExpressionsInExpression(expr.rhs, transform, flags);
|
|
}
|
|
else if (expr instanceof UnaryOperatorExpr) {
|
|
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
|
|
}
|
|
else if (expr instanceof ReadPropExpr) {
|
|
expr.receiver = transformExpressionsInExpression(expr.receiver, transform, flags);
|
|
}
|
|
else if (expr instanceof ReadKeyExpr) {
|
|
expr.receiver = transformExpressionsInExpression(expr.receiver, transform, flags);
|
|
expr.index = transformExpressionsInExpression(expr.index, transform, flags);
|
|
}
|
|
else if (expr instanceof InvokeFunctionExpr) {
|
|
expr.fn = transformExpressionsInExpression(expr.fn, transform, flags);
|
|
for (let i = 0; i < expr.args.length; i++) {
|
|
expr.args[i] = transformExpressionsInExpression(expr.args[i], transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof LiteralArrayExpr) {
|
|
for (let i = 0; i < expr.entries.length; i++) {
|
|
expr.entries[i] = transformExpressionsInExpression(expr.entries[i], transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof LiteralMapExpr) {
|
|
for (let i = 0; i < expr.entries.length; i++) {
|
|
expr.entries[i].value = transformExpressionsInExpression(expr.entries[i].value, transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof ConditionalExpr) {
|
|
expr.condition = transformExpressionsInExpression(expr.condition, transform, flags);
|
|
expr.trueCase = transformExpressionsInExpression(expr.trueCase, transform, flags);
|
|
if (expr.falseCase !== null) {
|
|
expr.falseCase = transformExpressionsInExpression(expr.falseCase, transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof TypeofExpr) {
|
|
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
|
|
}
|
|
else if (expr instanceof VoidExpr) {
|
|
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
|
|
}
|
|
else if (expr instanceof LocalizedString) {
|
|
for (let i = 0; i < expr.expressions.length; i++) {
|
|
expr.expressions[i] = transformExpressionsInExpression(expr.expressions[i], transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof NotExpr) {
|
|
expr.condition = transformExpressionsInExpression(expr.condition, transform, flags);
|
|
}
|
|
else if (expr instanceof TaggedTemplateLiteralExpr) {
|
|
expr.tag = transformExpressionsInExpression(expr.tag, transform, flags);
|
|
expr.template.expressions = expr.template.expressions.map((e) => transformExpressionsInExpression(e, transform, flags));
|
|
}
|
|
else if (expr instanceof ArrowFunctionExpr) {
|
|
if (Array.isArray(expr.body)) {
|
|
for (let i = 0; i < expr.body.length; i++) {
|
|
transformExpressionsInStatement(expr.body[i], transform, flags);
|
|
}
|
|
}
|
|
else {
|
|
expr.body = transformExpressionsInExpression(expr.body, transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof WrappedNodeExpr) ;
|
|
else if (expr instanceof TemplateLiteralExpr) {
|
|
for (let i = 0; i < expr.expressions.length; i++) {
|
|
expr.expressions[i] = transformExpressionsInExpression(expr.expressions[i], transform, flags);
|
|
}
|
|
}
|
|
else if (expr instanceof ParenthesizedExpr) {
|
|
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
|
|
}
|
|
else if (expr instanceof ReadVarExpr ||
|
|
expr instanceof ExternalExpr ||
|
|
expr instanceof LiteralExpr) ;
|
|
else {
|
|
throw new Error(`Unhandled expression kind: ${expr.constructor.name}`);
|
|
}
|
|
return transform(expr, flags);
|
|
}
|
|
/**
|
|
* Transform all `Expression`s in the AST of `stmt` with the `transform` function.
|
|
*
|
|
* All such operations will be replaced with the result of applying `transform`, which may be an
|
|
* identity transformation.
|
|
*/
|
|
function transformExpressionsInStatement(stmt, transform, flags) {
|
|
if (stmt instanceof ExpressionStatement) {
|
|
stmt.expr = transformExpressionsInExpression(stmt.expr, transform, flags);
|
|
}
|
|
else if (stmt instanceof ReturnStatement) {
|
|
stmt.value = transformExpressionsInExpression(stmt.value, transform, flags);
|
|
}
|
|
else if (stmt instanceof DeclareVarStmt) {
|
|
if (stmt.value !== undefined) {
|
|
stmt.value = transformExpressionsInExpression(stmt.value, transform, flags);
|
|
}
|
|
}
|
|
else if (stmt instanceof IfStmt) {
|
|
stmt.condition = transformExpressionsInExpression(stmt.condition, transform, flags);
|
|
for (const caseStatement of stmt.trueCase) {
|
|
transformExpressionsInStatement(caseStatement, transform, flags);
|
|
}
|
|
for (const caseStatement of stmt.falseCase) {
|
|
transformExpressionsInStatement(caseStatement, transform, flags);
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Unhandled statement kind: ${stmt.constructor.name}`);
|
|
}
|
|
}
|
|
/**
|
|
* Checks whether the given expression is a string literal.
|
|
*/
|
|
function isStringLiteral(expr) {
|
|
return expr instanceof LiteralExpr && typeof expr.value === 'string';
|
|
}
|
|
|
|
/**
|
|
* A linked list of `Op` nodes of a given subtype.
|
|
*
|
|
* @param OpT specific subtype of `Op` nodes which this list contains.
|
|
*/
|
|
class OpList {
|
|
static nextListId = 0;
|
|
/**
|
|
* Debug ID of this `OpList` instance.
|
|
*/
|
|
debugListId = OpList.nextListId++;
|
|
// OpList uses static head/tail nodes of a special `ListEnd` type.
|
|
// This avoids the need for special casing of the first and last list
|
|
// elements in all list operations.
|
|
head = {
|
|
kind: OpKind.ListEnd,
|
|
next: null,
|
|
prev: null,
|
|
debugListId: this.debugListId,
|
|
};
|
|
tail = {
|
|
kind: OpKind.ListEnd,
|
|
next: null,
|
|
prev: null,
|
|
debugListId: this.debugListId,
|
|
};
|
|
constructor() {
|
|
// Link `head` and `tail` together at the start (list is empty).
|
|
this.head.next = this.tail;
|
|
this.tail.prev = this.head;
|
|
}
|
|
/**
|
|
* Push a new operation to the tail of the list.
|
|
*/
|
|
push(op) {
|
|
if (Array.isArray(op)) {
|
|
for (const o of op) {
|
|
this.push(o);
|
|
}
|
|
return;
|
|
}
|
|
OpList.assertIsNotEnd(op);
|
|
OpList.assertIsUnowned(op);
|
|
op.debugListId = this.debugListId;
|
|
// The old "previous" node (which might be the head, if the list is empty).
|
|
const oldLast = this.tail.prev;
|
|
// Insert `op` following the old last node.
|
|
op.prev = oldLast;
|
|
oldLast.next = op;
|
|
// Connect `op` with the list tail.
|
|
op.next = this.tail;
|
|
this.tail.prev = op;
|
|
}
|
|
/**
|
|
* Prepend one or more nodes to the start of the list.
|
|
*/
|
|
prepend(ops) {
|
|
if (ops.length === 0) {
|
|
return;
|
|
}
|
|
for (const op of ops) {
|
|
OpList.assertIsNotEnd(op);
|
|
OpList.assertIsUnowned(op);
|
|
op.debugListId = this.debugListId;
|
|
}
|
|
const first = this.head.next;
|
|
let prev = this.head;
|
|
for (const op of ops) {
|
|
prev.next = op;
|
|
op.prev = prev;
|
|
prev = op;
|
|
}
|
|
prev.next = first;
|
|
first.prev = prev;
|
|
}
|
|
/**
|
|
* `OpList` is iterable via the iteration protocol.
|
|
*
|
|
* It's safe to mutate the part of the list that has already been returned by the iterator, up to
|
|
* and including the last operation returned. Mutations beyond that point _may_ be safe, but may
|
|
* also corrupt the iteration position and should be avoided.
|
|
*/
|
|
*[Symbol.iterator]() {
|
|
let current = this.head.next;
|
|
while (current !== this.tail) {
|
|
// Guards against corruption of the iterator state by mutations to the tail of the list during
|
|
// iteration.
|
|
OpList.assertIsOwned(current, this.debugListId);
|
|
const next = current.next;
|
|
yield current;
|
|
current = next;
|
|
}
|
|
}
|
|
*reversed() {
|
|
let current = this.tail.prev;
|
|
while (current !== this.head) {
|
|
OpList.assertIsOwned(current, this.debugListId);
|
|
const prev = current.prev;
|
|
yield current;
|
|
current = prev;
|
|
}
|
|
}
|
|
/**
|
|
* Replace `oldOp` with `newOp` in the list.
|
|
*/
|
|
static replace(oldOp, newOp) {
|
|
OpList.assertIsNotEnd(oldOp);
|
|
OpList.assertIsNotEnd(newOp);
|
|
OpList.assertIsOwned(oldOp);
|
|
OpList.assertIsUnowned(newOp);
|
|
newOp.debugListId = oldOp.debugListId;
|
|
if (oldOp.prev !== null) {
|
|
oldOp.prev.next = newOp;
|
|
newOp.prev = oldOp.prev;
|
|
}
|
|
if (oldOp.next !== null) {
|
|
oldOp.next.prev = newOp;
|
|
newOp.next = oldOp.next;
|
|
}
|
|
oldOp.debugListId = null;
|
|
oldOp.prev = null;
|
|
oldOp.next = null;
|
|
}
|
|
/**
|
|
* Replace `oldOp` with some number of new operations in the list (which may include `oldOp`).
|
|
*/
|
|
static replaceWithMany(oldOp, newOps) {
|
|
if (newOps.length === 0) {
|
|
// Replacing with an empty list -> pure removal.
|
|
OpList.remove(oldOp);
|
|
return;
|
|
}
|
|
OpList.assertIsNotEnd(oldOp);
|
|
OpList.assertIsOwned(oldOp);
|
|
const listId = oldOp.debugListId;
|
|
oldOp.debugListId = null;
|
|
for (const newOp of newOps) {
|
|
OpList.assertIsNotEnd(newOp);
|
|
// `newOp` might be `oldOp`, but at this point it's been marked as unowned.
|
|
OpList.assertIsUnowned(newOp);
|
|
}
|
|
// It should be safe to reuse `oldOp` in the `newOps` list - maybe you want to sandwich an
|
|
// operation between two new ops.
|
|
const { prev: oldPrev, next: oldNext } = oldOp;
|
|
oldOp.prev = null;
|
|
oldOp.next = null;
|
|
let prev = oldPrev;
|
|
for (const newOp of newOps) {
|
|
OpList.assertIsUnowned(newOp);
|
|
newOp.debugListId = listId;
|
|
prev.next = newOp;
|
|
newOp.prev = prev;
|
|
// This _should_ be the case, but set it just in case.
|
|
newOp.next = null;
|
|
prev = newOp;
|
|
}
|
|
// At the end of iteration, `prev` holds the last node in the list.
|
|
const first = newOps[0];
|
|
const last = prev;
|
|
// Replace `oldOp` with the chain `first` -> `last`.
|
|
if (oldPrev !== null) {
|
|
oldPrev.next = first;
|
|
first.prev = oldPrev;
|
|
}
|
|
if (oldNext !== null) {
|
|
oldNext.prev = last;
|
|
last.next = oldNext;
|
|
}
|
|
}
|
|
/**
|
|
* Remove the given node from the list which contains it.
|
|
*/
|
|
static remove(op) {
|
|
OpList.assertIsNotEnd(op);
|
|
OpList.assertIsOwned(op);
|
|
op.prev.next = op.next;
|
|
op.next.prev = op.prev;
|
|
// Break any link between the node and this list to safeguard against its usage in future
|
|
// operations.
|
|
op.debugListId = null;
|
|
op.prev = null;
|
|
op.next = null;
|
|
}
|
|
/**
|
|
* Insert `op` before `target`.
|
|
*/
|
|
static insertBefore(op, target) {
|
|
if (Array.isArray(op)) {
|
|
for (const o of op) {
|
|
OpList.insertBefore(o, target);
|
|
}
|
|
return;
|
|
}
|
|
OpList.assertIsOwned(target);
|
|
if (target.prev === null) {
|
|
throw new Error(`AssertionError: illegal operation on list start`);
|
|
}
|
|
OpList.assertIsNotEnd(op);
|
|
OpList.assertIsUnowned(op);
|
|
op.debugListId = target.debugListId;
|
|
// Just in case.
|
|
op.prev = null;
|
|
target.prev.next = op;
|
|
op.prev = target.prev;
|
|
op.next = target;
|
|
target.prev = op;
|
|
}
|
|
/**
|
|
* Insert `op` after `target`.
|
|
*/
|
|
static insertAfter(op, target) {
|
|
OpList.assertIsOwned(target);
|
|
if (target.next === null) {
|
|
throw new Error(`AssertionError: illegal operation on list end`);
|
|
}
|
|
OpList.assertIsNotEnd(op);
|
|
OpList.assertIsUnowned(op);
|
|
op.debugListId = target.debugListId;
|
|
target.next.prev = op;
|
|
op.next = target.next;
|
|
op.prev = target;
|
|
target.next = op;
|
|
}
|
|
/**
|
|
* Asserts that `op` does not currently belong to a list.
|
|
*/
|
|
static assertIsUnowned(op) {
|
|
if (op.debugListId !== null) {
|
|
throw new Error(`AssertionError: illegal operation on owned node: ${OpKind[op.kind]}`);
|
|
}
|
|
}
|
|
/**
|
|
* Asserts that `op` currently belongs to a list. If `byList` is passed, `op` is asserted to
|
|
* specifically belong to that list.
|
|
*/
|
|
static assertIsOwned(op, byList) {
|
|
if (op.debugListId === null) {
|
|
throw new Error(`AssertionError: illegal operation on unowned node: ${OpKind[op.kind]}`);
|
|
}
|
|
else if (byList !== undefined && op.debugListId !== byList) {
|
|
throw new Error(`AssertionError: node belongs to the wrong list (expected ${byList}, actual ${op.debugListId})`);
|
|
}
|
|
}
|
|
/**
|
|
* Asserts that `op` is not a special `ListEnd` node.
|
|
*/
|
|
static assertIsNotEnd(op) {
|
|
if (op.kind === OpKind.ListEnd) {
|
|
throw new Error(`AssertionError: illegal operation on list head or tail`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SlotHandle {
|
|
slot = null;
|
|
}
|
|
|
|
/**
|
|
* The set of OpKinds that represent the creation of an element or container
|
|
*/
|
|
const elementContainerOpKinds = new Set([
|
|
OpKind.Element,
|
|
OpKind.ElementStart,
|
|
OpKind.Container,
|
|
OpKind.ContainerStart,
|
|
OpKind.Template,
|
|
OpKind.RepeaterCreate,
|
|
OpKind.ConditionalCreate,
|
|
OpKind.ConditionalBranchCreate,
|
|
]);
|
|
/**
|
|
* Checks whether the given operation represents the creation of an element or container.
|
|
*/
|
|
function isElementOrContainerOp(op) {
|
|
return elementContainerOpKinds.has(op.kind);
|
|
}
|
|
/**
|
|
* Create an `ElementStartOp`.
|
|
*/
|
|
function createElementStartOp(tag, xref, namespace, i18nPlaceholder, startSourceSpan, wholeSourceSpan) {
|
|
return {
|
|
kind: OpKind.ElementStart,
|
|
xref,
|
|
tag,
|
|
handle: new SlotHandle(),
|
|
attributes: null,
|
|
localRefs: [],
|
|
nonBindable: false,
|
|
namespace,
|
|
i18nPlaceholder,
|
|
startSourceSpan,
|
|
wholeSourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `TemplateOp`.
|
|
*/
|
|
function createTemplateOp(xref, templateKind, tag, functionNameSuffix, namespace, i18nPlaceholder, startSourceSpan, wholeSourceSpan) {
|
|
return {
|
|
kind: OpKind.Template,
|
|
xref,
|
|
templateKind,
|
|
attributes: null,
|
|
tag,
|
|
handle: new SlotHandle(),
|
|
functionNameSuffix,
|
|
decls: null,
|
|
vars: null,
|
|
localRefs: [],
|
|
nonBindable: false,
|
|
namespace,
|
|
i18nPlaceholder,
|
|
startSourceSpan,
|
|
wholeSourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createConditionalCreateOp(xref, templateKind, tag, functionNameSuffix, namespace, i18nPlaceholder, startSourceSpan, wholeSourceSpan) {
|
|
return {
|
|
kind: OpKind.ConditionalCreate,
|
|
xref,
|
|
templateKind,
|
|
attributes: null,
|
|
tag,
|
|
handle: new SlotHandle(),
|
|
functionNameSuffix,
|
|
decls: null,
|
|
vars: null,
|
|
localRefs: [],
|
|
nonBindable: false,
|
|
namespace,
|
|
i18nPlaceholder,
|
|
startSourceSpan,
|
|
wholeSourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createConditionalBranchCreateOp(xref, templateKind, tag, functionNameSuffix, namespace, i18nPlaceholder, startSourceSpan, wholeSourceSpan) {
|
|
return {
|
|
kind: OpKind.ConditionalBranchCreate,
|
|
xref,
|
|
templateKind,
|
|
attributes: null,
|
|
tag,
|
|
handle: new SlotHandle(),
|
|
functionNameSuffix,
|
|
decls: null,
|
|
vars: null,
|
|
localRefs: [],
|
|
nonBindable: false,
|
|
namespace,
|
|
i18nPlaceholder,
|
|
startSourceSpan,
|
|
wholeSourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createRepeaterCreateOp(primaryView, emptyView, tag, track, varNames, emptyTag, i18nPlaceholder, emptyI18nPlaceholder, startSourceSpan, wholeSourceSpan) {
|
|
return {
|
|
kind: OpKind.RepeaterCreate,
|
|
attributes: null,
|
|
xref: primaryView,
|
|
handle: new SlotHandle(),
|
|
emptyView,
|
|
track,
|
|
trackByFn: null,
|
|
trackByOps: null,
|
|
tag,
|
|
emptyTag,
|
|
emptyAttributes: null,
|
|
functionNameSuffix: 'For',
|
|
namespace: Namespace.HTML,
|
|
nonBindable: false,
|
|
localRefs: [],
|
|
decls: null,
|
|
vars: null,
|
|
varNames,
|
|
usesComponentInstance: false,
|
|
i18nPlaceholder,
|
|
emptyI18nPlaceholder,
|
|
startSourceSpan,
|
|
wholeSourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_VARS,
|
|
numSlotsUsed: emptyView === null ? 2 : 3,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `ElementEndOp`.
|
|
*/
|
|
function createElementEndOp(xref, sourceSpan) {
|
|
return {
|
|
kind: OpKind.ElementEnd,
|
|
xref,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createDisableBindingsOp(xref) {
|
|
return {
|
|
kind: OpKind.DisableBindings,
|
|
xref,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createEnableBindingsOp(xref) {
|
|
return {
|
|
kind: OpKind.EnableBindings,
|
|
xref,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `TextOp`.
|
|
*/
|
|
function createTextOp(xref, initialValue, icuPlaceholder, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Text,
|
|
xref,
|
|
handle: new SlotHandle(),
|
|
initialValue,
|
|
icuPlaceholder,
|
|
sourceSpan,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `AnimationOp`.
|
|
*/
|
|
function createAnimationStringOp(name, target, animationKind, expression, securityContext, sourceSpan) {
|
|
return {
|
|
kind: OpKind.AnimationString,
|
|
name,
|
|
target,
|
|
animationKind,
|
|
expression,
|
|
i18nMessage: null,
|
|
securityContext,
|
|
sanitizer: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `AnimationOp`.
|
|
*/
|
|
function createAnimationOp(name, target, animationKind, callbackOps, securityContext, sourceSpan) {
|
|
const handlerOps = new OpList();
|
|
handlerOps.push(callbackOps);
|
|
return {
|
|
kind: OpKind.Animation,
|
|
name,
|
|
target,
|
|
animationKind,
|
|
handlerOps,
|
|
handlerFnName: null,
|
|
i18nMessage: null,
|
|
securityContext,
|
|
sanitizer: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `ListenerOp`. Host bindings reuse all the listener logic.
|
|
*/
|
|
function createListenerOp(target, targetSlot, name, tag, handlerOps, legacyAnimationPhase, eventTarget, hostListener, sourceSpan) {
|
|
const handlerList = new OpList();
|
|
handlerList.push(handlerOps);
|
|
return {
|
|
kind: OpKind.Listener,
|
|
target,
|
|
targetSlot,
|
|
tag,
|
|
hostListener,
|
|
name,
|
|
handlerOps: handlerList,
|
|
handlerFnName: null,
|
|
consumesDollarEvent: false,
|
|
isLegacyAnimationListener: legacyAnimationPhase !== null,
|
|
legacyAnimationPhase: legacyAnimationPhase,
|
|
eventTarget,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `ListenerOp`. Host bindings reuse all the listener logic.
|
|
*/
|
|
function createAnimationListenerOp(target, targetSlot, name, tag, handlerOps, animationKind, eventTarget, hostListener, sourceSpan) {
|
|
const handlerList = new OpList();
|
|
handlerList.push(handlerOps);
|
|
return {
|
|
kind: OpKind.AnimationListener,
|
|
target,
|
|
targetSlot,
|
|
tag,
|
|
hostListener,
|
|
name,
|
|
animationKind,
|
|
handlerOps: handlerList,
|
|
handlerFnName: null,
|
|
consumesDollarEvent: false,
|
|
eventTarget,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create a `TwoWayListenerOp`.
|
|
*/
|
|
function createTwoWayListenerOp(target, targetSlot, name, tag, handlerOps, sourceSpan) {
|
|
const handlerList = new OpList();
|
|
handlerList.push(handlerOps);
|
|
return {
|
|
kind: OpKind.TwoWayListener,
|
|
target,
|
|
targetSlot,
|
|
tag,
|
|
name,
|
|
handlerOps: handlerList,
|
|
handlerFnName: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createPipeOp(xref, slot, name) {
|
|
return {
|
|
kind: OpKind.Pipe,
|
|
xref,
|
|
handle: slot,
|
|
name,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
};
|
|
}
|
|
function createNamespaceOp(namespace) {
|
|
return {
|
|
kind: OpKind.Namespace,
|
|
active: namespace,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createProjectionDefOp(def) {
|
|
return {
|
|
kind: OpKind.ProjectionDef,
|
|
def,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createProjectionOp(xref, selector, i18nPlaceholder, fallbackView, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Projection,
|
|
xref,
|
|
handle: new SlotHandle(),
|
|
selector,
|
|
i18nPlaceholder,
|
|
fallbackView,
|
|
projectionSlotIndex: 0,
|
|
attributes: null,
|
|
localRefs: [],
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
numSlotsUsed: fallbackView === null ? 1 : 2,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `ExtractedAttributeOp`.
|
|
*/
|
|
function createExtractedAttributeOp(target, bindingKind, namespace, name, expression, i18nContext, i18nMessage, securityContext) {
|
|
return {
|
|
kind: OpKind.ExtractedAttribute,
|
|
target,
|
|
bindingKind,
|
|
namespace,
|
|
name,
|
|
expression,
|
|
i18nContext,
|
|
i18nMessage,
|
|
securityContext,
|
|
trustedValueFn: null,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createDeferOp(xref, main, mainSlot, ownResolverFn, resolverFn, sourceSpan) {
|
|
return {
|
|
kind: OpKind.Defer,
|
|
xref,
|
|
handle: new SlotHandle(),
|
|
mainView: main,
|
|
mainSlot,
|
|
loadingView: null,
|
|
loadingSlot: null,
|
|
loadingConfig: null,
|
|
loadingMinimumTime: null,
|
|
loadingAfterTime: null,
|
|
placeholderView: null,
|
|
placeholderSlot: null,
|
|
placeholderConfig: null,
|
|
placeholderMinimumTime: null,
|
|
errorView: null,
|
|
errorSlot: null,
|
|
ownResolverFn,
|
|
resolverFn,
|
|
flags: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
numSlotsUsed: 2,
|
|
};
|
|
}
|
|
function createDeferOnOp(defer, trigger, modifier, sourceSpan) {
|
|
return {
|
|
kind: OpKind.DeferOn,
|
|
defer,
|
|
trigger,
|
|
modifier,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Creates a `DeclareLetOp`.
|
|
*/
|
|
function createDeclareLetOp(xref, declaredName, sourceSpan) {
|
|
return {
|
|
kind: OpKind.DeclareLet,
|
|
xref,
|
|
declaredName,
|
|
sourceSpan,
|
|
handle: new SlotHandle(),
|
|
...TRAIT_CONSUMES_SLOT,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `ExtractedMessageOp`.
|
|
*/
|
|
function createI18nMessageOp(xref, i18nContext, i18nBlock, message, messagePlaceholder, params, postprocessingParams, needsPostprocessing) {
|
|
return {
|
|
kind: OpKind.I18nMessage,
|
|
xref,
|
|
i18nContext,
|
|
i18nBlock,
|
|
message,
|
|
messagePlaceholder,
|
|
params,
|
|
postprocessingParams,
|
|
needsPostprocessing,
|
|
subMessages: [],
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `I18nStartOp`.
|
|
*/
|
|
function createI18nStartOp(xref, message, root, sourceSpan) {
|
|
return {
|
|
kind: OpKind.I18nStart,
|
|
xref,
|
|
handle: new SlotHandle(),
|
|
root: root ?? xref,
|
|
message,
|
|
messageIndex: null,
|
|
subTemplateIndex: null,
|
|
context: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
};
|
|
}
|
|
/**
|
|
* Create an `I18nEndOp`.
|
|
*/
|
|
function createI18nEndOp(xref, sourceSpan) {
|
|
return {
|
|
kind: OpKind.I18nEnd,
|
|
xref,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Creates an ICU start op.
|
|
*/
|
|
function createIcuStartOp(xref, message, messagePlaceholder, sourceSpan) {
|
|
return {
|
|
kind: OpKind.IcuStart,
|
|
xref,
|
|
message,
|
|
messagePlaceholder,
|
|
context: null,
|
|
sourceSpan,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Creates an ICU end op.
|
|
*/
|
|
function createIcuEndOp(xref) {
|
|
return {
|
|
kind: OpKind.IcuEnd,
|
|
xref,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
/**
|
|
* Creates an ICU placeholder op.
|
|
*/
|
|
function createIcuPlaceholderOp(xref, name, strings) {
|
|
return {
|
|
kind: OpKind.IcuPlaceholder,
|
|
xref,
|
|
name,
|
|
strings,
|
|
expressionPlaceholders: [],
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createI18nContextOp(contextKind, xref, i18nBlock, message, sourceSpan) {
|
|
if (i18nBlock === null && contextKind !== I18nContextKind.Attr) {
|
|
throw new Error('AssertionError: i18nBlock must be provided for non-attribute contexts.');
|
|
}
|
|
return {
|
|
kind: OpKind.I18nContext,
|
|
contextKind,
|
|
xref,
|
|
i18nBlock,
|
|
message,
|
|
sourceSpan,
|
|
params: new Map(),
|
|
postprocessingParams: new Map(),
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
function createI18nAttributesOp(xref, handle, target) {
|
|
return {
|
|
kind: OpKind.I18nAttributes,
|
|
xref,
|
|
handle,
|
|
target,
|
|
i18nAttributesConfig: null,
|
|
...NEW_OP,
|
|
...TRAIT_CONSUMES_SLOT,
|
|
};
|
|
}
|
|
/** Create a `SourceLocationOp`. */
|
|
function createSourceLocationOp(templatePath, locations) {
|
|
return {
|
|
kind: OpKind.SourceLocation,
|
|
templatePath,
|
|
locations,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
|
|
function createDomPropertyOp(name, expression, bindingKind, i18nContext, securityContext, sourceSpan) {
|
|
return {
|
|
kind: OpKind.DomProperty,
|
|
name,
|
|
expression,
|
|
bindingKind,
|
|
i18nContext,
|
|
securityContext,
|
|
sanitizer: null,
|
|
sourceSpan,
|
|
...TRAIT_CONSUMES_VARS,
|
|
...NEW_OP,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* When referenced in the template's context parameters, this indicates a reference to the entire
|
|
* context object, rather than a specific parameter.
|
|
*/
|
|
const CTX_REF = 'CTX_REF_MARKER';
|
|
|
|
var CompilationJobKind;
|
|
(function (CompilationJobKind) {
|
|
CompilationJobKind[CompilationJobKind["Tmpl"] = 0] = "Tmpl";
|
|
CompilationJobKind[CompilationJobKind["Host"] = 1] = "Host";
|
|
CompilationJobKind[CompilationJobKind["Both"] = 2] = "Both";
|
|
})(CompilationJobKind || (CompilationJobKind = {}));
|
|
/** Possible modes in which a component's template can be compiled. */
|
|
var TemplateCompilationMode;
|
|
(function (TemplateCompilationMode) {
|
|
/** Supports the full instruction set, including directives. */
|
|
TemplateCompilationMode[TemplateCompilationMode["Full"] = 0] = "Full";
|
|
/** Uses a narrower instruction set that doesn't support directives and allows optimizations. */
|
|
TemplateCompilationMode[TemplateCompilationMode["DomOnly"] = 1] = "DomOnly";
|
|
})(TemplateCompilationMode || (TemplateCompilationMode = {}));
|
|
/**
|
|
* An entire ongoing compilation, which will result in one or more template functions when complete.
|
|
* Contains one or more corresponding compilation units.
|
|
*/
|
|
class CompilationJob {
|
|
componentName;
|
|
pool;
|
|
compatibility;
|
|
mode;
|
|
constructor(componentName, pool, compatibility, mode) {
|
|
this.componentName = componentName;
|
|
this.pool = pool;
|
|
this.compatibility = compatibility;
|
|
this.mode = mode;
|
|
}
|
|
kind = CompilationJobKind.Both;
|
|
/**
|
|
* Generate a new unique `ir.XrefId` in this job.
|
|
*/
|
|
allocateXrefId() {
|
|
return this.nextXrefId++;
|
|
}
|
|
/**
|
|
* Tracks the next `ir.XrefId` which can be assigned as template structures are ingested.
|
|
*/
|
|
nextXrefId = 0;
|
|
}
|
|
/**
|
|
* Compilation-in-progress of a whole component's template, including the main template and any
|
|
* embedded views or host bindings.
|
|
*/
|
|
class ComponentCompilationJob extends CompilationJob {
|
|
relativeContextFilePath;
|
|
i18nUseExternalIds;
|
|
deferMeta;
|
|
allDeferrableDepsFn;
|
|
relativeTemplatePath;
|
|
enableDebugLocations;
|
|
constructor(componentName, pool, compatibility, mode, relativeContextFilePath, i18nUseExternalIds, deferMeta, allDeferrableDepsFn, relativeTemplatePath, enableDebugLocations) {
|
|
super(componentName, pool, compatibility, mode);
|
|
this.relativeContextFilePath = relativeContextFilePath;
|
|
this.i18nUseExternalIds = i18nUseExternalIds;
|
|
this.deferMeta = deferMeta;
|
|
this.allDeferrableDepsFn = allDeferrableDepsFn;
|
|
this.relativeTemplatePath = relativeTemplatePath;
|
|
this.enableDebugLocations = enableDebugLocations;
|
|
this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null);
|
|
this.views.set(this.root.xref, this.root);
|
|
}
|
|
kind = CompilationJobKind.Tmpl;
|
|
fnSuffix = 'Template';
|
|
/**
|
|
* The root view, representing the component's template.
|
|
*/
|
|
root;
|
|
views = new Map();
|
|
/**
|
|
* Causes ngContentSelectors to be emitted, for content projection slots in the view. Possibly a
|
|
* reference into the constant pool.
|
|
*/
|
|
contentSelectors = null;
|
|
/**
|
|
* Add a `ViewCompilation` for a new embedded view to this compilation.
|
|
*/
|
|
allocateView(parent) {
|
|
const view = new ViewCompilationUnit(this, this.allocateXrefId(), parent);
|
|
this.views.set(view.xref, view);
|
|
return view;
|
|
}
|
|
get units() {
|
|
return this.views.values();
|
|
}
|
|
/**
|
|
* Add a constant `o.Expression` to the compilation and return its index in the `consts` array.
|
|
*/
|
|
addConst(newConst, initializers) {
|
|
for (let idx = 0; idx < this.consts.length; idx++) {
|
|
if (this.consts[idx].isEquivalent(newConst)) {
|
|
return idx;
|
|
}
|
|
}
|
|
const idx = this.consts.length;
|
|
this.consts.push(newConst);
|
|
if (initializers) {
|
|
this.constsInitializers.push(...initializers);
|
|
}
|
|
return idx;
|
|
}
|
|
/**
|
|
* Constant expressions used by operations within this component's compilation.
|
|
*
|
|
* This will eventually become the `consts` array in the component definition.
|
|
*/
|
|
consts = [];
|
|
/**
|
|
* Initialization statements needed to set up the consts.
|
|
*/
|
|
constsInitializers = [];
|
|
}
|
|
/**
|
|
* A compilation unit is compiled into a template function. Some example units are views and host
|
|
* bindings.
|
|
*/
|
|
class CompilationUnit {
|
|
xref;
|
|
constructor(xref) {
|
|
this.xref = xref;
|
|
}
|
|
/**
|
|
* List of creation operations for this view.
|
|
*
|
|
* Creation operations may internally contain other operations, including update operations.
|
|
*/
|
|
create = new OpList();
|
|
/**
|
|
* List of update operations for this view.
|
|
*/
|
|
update = new OpList();
|
|
/**
|
|
* Name of the function which will be generated for this unit.
|
|
*
|
|
* May be `null` if not yet determined.
|
|
*/
|
|
fnName = null;
|
|
/**
|
|
* Number of variable slots used within this view, or `null` if variables have not yet been
|
|
* counted.
|
|
*/
|
|
vars = null;
|
|
/**
|
|
* Iterate over all `ir.Op`s within this view.
|
|
*
|
|
* Some operations may have child operations, which this iterator will visit.
|
|
*/
|
|
*ops() {
|
|
for (const op of this.create) {
|
|
yield op;
|
|
if (op.kind === OpKind.Listener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener ||
|
|
op.kind === OpKind.TwoWayListener) {
|
|
for (const listenerOp of op.handlerOps) {
|
|
yield listenerOp;
|
|
}
|
|
}
|
|
else if (op.kind === OpKind.RepeaterCreate && op.trackByOps !== null) {
|
|
for (const trackOp of op.trackByOps) {
|
|
yield trackOp;
|
|
}
|
|
}
|
|
}
|
|
for (const op of this.update) {
|
|
yield op;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Compilation-in-progress of an individual view within a template.
|
|
*/
|
|
class ViewCompilationUnit extends CompilationUnit {
|
|
job;
|
|
parent;
|
|
constructor(job, xref, parent) {
|
|
super(xref);
|
|
this.job = job;
|
|
this.parent = parent;
|
|
}
|
|
/**
|
|
* Map of declared variables available within this view to the property on the context object
|
|
* which they alias.
|
|
*/
|
|
contextVariables = new Map();
|
|
/**
|
|
* Set of aliases available within this view. An alias is a variable whose provided expression is
|
|
* inlined at every location it is used. It may also depend on context variables, by name.
|
|
*/
|
|
aliases = new Set();
|
|
/**
|
|
* Number of declaration slots used within this view, or `null` if slots have not yet been
|
|
* allocated.
|
|
*/
|
|
decls = null;
|
|
}
|
|
/**
|
|
* Compilation-in-progress of a host binding, which contains a single unit for that host binding.
|
|
*/
|
|
class HostBindingCompilationJob extends CompilationJob {
|
|
constructor(componentName, pool, compatibility, mode) {
|
|
super(componentName, pool, compatibility, mode);
|
|
this.root = new HostBindingCompilationUnit(this);
|
|
}
|
|
kind = CompilationJobKind.Host;
|
|
fnSuffix = 'HostBindings';
|
|
root;
|
|
get units() {
|
|
return [this.root];
|
|
}
|
|
}
|
|
class HostBindingCompilationUnit extends CompilationUnit {
|
|
job;
|
|
constructor(job) {
|
|
super(0);
|
|
this.job = job;
|
|
}
|
|
/**
|
|
* Much like an element can have attributes, so can a host binding function.
|
|
*/
|
|
attributes = null;
|
|
}
|
|
|
|
/**
|
|
* Find any function calls to `$any`, excluding `this.$any`, and delete them, since they have no
|
|
* runtime effects.
|
|
*/
|
|
function deleteAnyCasts(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
transformExpressionsInOp(op, removeAnys, VisitorContextFlag.None);
|
|
}
|
|
}
|
|
}
|
|
function removeAnys(e) {
|
|
if (e instanceof InvokeFunctionExpr &&
|
|
e.fn instanceof LexicalReadExpr &&
|
|
e.fn.name === '$any') {
|
|
if (e.args.length !== 1) {
|
|
throw new Error('The $any builtin function expects exactly one argument.');
|
|
}
|
|
return e.args[0];
|
|
}
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* Adds apply operations after i18n expressions.
|
|
*/
|
|
function applyI18nExpressions(job) {
|
|
const i18nContexts = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.I18nContext) {
|
|
i18nContexts.set(op.xref, op);
|
|
}
|
|
}
|
|
}
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
// Only add apply after expressions that are not followed by more expressions.
|
|
if (op.kind === OpKind.I18nExpression && needsApplication(i18nContexts, op)) {
|
|
// TODO: what should be the source span for the apply op?
|
|
OpList.insertAfter(createI18nApplyOp(op.i18nOwner, op.handle, null), op);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Checks whether the given expression op needs to be followed with an apply op.
|
|
*/
|
|
function needsApplication(i18nContexts, op) {
|
|
// If the next op is not another expression, we need to apply.
|
|
if (op.next?.kind !== OpKind.I18nExpression) {
|
|
return true;
|
|
}
|
|
const context = i18nContexts.get(op.context);
|
|
const nextContext = i18nContexts.get(op.next.context);
|
|
if (context === undefined) {
|
|
throw new Error("AssertionError: expected an I18nContextOp to exist for the I18nExpressionOp's context");
|
|
}
|
|
if (nextContext === undefined) {
|
|
throw new Error("AssertionError: expected an I18nContextOp to exist for the next I18nExpressionOp's context");
|
|
}
|
|
// If the next op is an expression targeting a different i18n block (or different element, in the
|
|
// case of i18n attributes), we need to apply.
|
|
// First, handle the case of i18n blocks.
|
|
if (context.i18nBlock !== null) {
|
|
// This is a block context. Compare the blocks.
|
|
if (context.i18nBlock !== nextContext.i18nBlock) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
// Second, handle the case of i18n attributes.
|
|
if (op.i18nOwner !== op.next.i18nOwner) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Updates i18n expression ops to target the last slot in their owning i18n block, and moves them
|
|
* after the last update instruction that depends on that slot.
|
|
*/
|
|
function assignI18nSlotDependencies(job) {
|
|
for (const unit of job.units) {
|
|
// The first update op.
|
|
let updateOp = unit.update.head;
|
|
// I18n expressions currently being moved during the iteration.
|
|
let i18nExpressionsInProgress = [];
|
|
// Non-null while we are iterating through an i18nStart/i18nEnd pair
|
|
let state = null;
|
|
for (const createOp of unit.create) {
|
|
if (createOp.kind === OpKind.I18nStart) {
|
|
state = {
|
|
blockXref: createOp.xref,
|
|
lastSlotConsumer: createOp.xref,
|
|
};
|
|
}
|
|
else if (createOp.kind === OpKind.I18nEnd) {
|
|
for (const op of i18nExpressionsInProgress) {
|
|
op.target = state.lastSlotConsumer;
|
|
OpList.insertBefore(op, updateOp);
|
|
}
|
|
i18nExpressionsInProgress.length = 0;
|
|
state = null;
|
|
}
|
|
if (hasConsumesSlotTrait(createOp)) {
|
|
if (state !== null) {
|
|
state.lastSlotConsumer = createOp.xref;
|
|
}
|
|
while (true) {
|
|
if (updateOp.next === null) {
|
|
break;
|
|
}
|
|
if (state !== null &&
|
|
updateOp.kind === OpKind.I18nExpression &&
|
|
updateOp.usage === I18nExpressionFor.I18nText &&
|
|
updateOp.i18nOwner === state.blockXref) {
|
|
const opToRemove = updateOp;
|
|
updateOp = updateOp.next;
|
|
OpList.remove(opToRemove);
|
|
i18nExpressionsInProgress.push(opToRemove);
|
|
continue;
|
|
}
|
|
let hasDifferentTarget = false;
|
|
if (hasDependsOnSlotContextTrait(updateOp) && updateOp.target !== createOp.xref) {
|
|
hasDifferentTarget = true;
|
|
}
|
|
else if (
|
|
// Some expressions may consume slots as well (e.g. `storeLet`).
|
|
updateOp.kind === OpKind.Statement ||
|
|
updateOp.kind === OpKind.Variable) {
|
|
visitExpressionsInOp(updateOp, (expr) => {
|
|
if (!hasDifferentTarget &&
|
|
hasDependsOnSlotContextTrait(expr) &&
|
|
expr.target !== createOp.xref) {
|
|
hasDifferentTarget = true;
|
|
}
|
|
});
|
|
}
|
|
if (hasDifferentTarget) {
|
|
break;
|
|
}
|
|
updateOp = updateOp.next;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Locates all of the elements defined in a creation block and outputs an op
|
|
* that will expose their definition location in the DOM.
|
|
*/
|
|
function attachSourceLocations(job) {
|
|
if (!job.enableDebugLocations || job.relativeTemplatePath === null) {
|
|
return;
|
|
}
|
|
for (const unit of job.units) {
|
|
const locations = [];
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.ElementStart || op.kind === OpKind.Element) {
|
|
const start = op.startSourceSpan.start;
|
|
locations.push({
|
|
targetSlot: op.handle,
|
|
offset: start.offset,
|
|
line: start.line,
|
|
column: start.col,
|
|
});
|
|
}
|
|
}
|
|
if (locations.length > 0) {
|
|
unit.create.push(createSourceLocationOp(job.relativeTemplatePath, locations));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a map of all elements in the given view by their xref id.
|
|
*/
|
|
function createOpXrefMap(unit) {
|
|
const map = new Map();
|
|
for (const op of unit.create) {
|
|
if (!hasConsumesSlotTrait(op)) {
|
|
continue;
|
|
}
|
|
map.set(op.xref, op);
|
|
// TODO(dylhunn): `@for` loops with `@empty` blocks need to be special-cased here,
|
|
// because the slot consumer trait currently only supports one slot per consumer and we
|
|
// need two. This should be revisited when making the refactors mentioned in:
|
|
// https://github.com/angular/angular/pull/53620#discussion_r1430918822
|
|
if (op.kind === OpKind.RepeaterCreate && op.emptyView !== null) {
|
|
map.set(op.emptyView, op);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Find all extractable attribute and binding ops, and create ExtractedAttributeOps for them.
|
|
* In cases where no instruction needs to be generated for the attribute or binding, it is removed.
|
|
*/
|
|
function extractAttributes(job) {
|
|
for (const unit of job.units) {
|
|
const elements = createOpXrefMap(unit);
|
|
for (const op of unit.ops()) {
|
|
switch (op.kind) {
|
|
case OpKind.Attribute:
|
|
extractAttributeOp(unit, op, elements);
|
|
break;
|
|
case OpKind.Property:
|
|
if (op.bindingKind !== BindingKind.LegacyAnimation &&
|
|
op.bindingKind !== BindingKind.Animation) {
|
|
let bindingKind;
|
|
if (op.i18nMessage !== null && op.templateKind === null) {
|
|
// If the binding has an i18n context, it is an i18n attribute, and should have that
|
|
// kind in the consts array.
|
|
bindingKind = BindingKind.I18n;
|
|
}
|
|
else if (op.isStructuralTemplateAttribute) {
|
|
bindingKind = BindingKind.Template;
|
|
}
|
|
else {
|
|
bindingKind = BindingKind.Property;
|
|
}
|
|
OpList.insertBefore(
|
|
// Deliberately null i18nMessage value
|
|
createExtractedAttributeOp(op.target, bindingKind, null, op.name,
|
|
/* expression */ null,
|
|
/* i18nContext */ null,
|
|
/* i18nMessage */ null, op.securityContext), lookupElement$3(elements, op.target));
|
|
}
|
|
break;
|
|
case OpKind.TwoWayProperty:
|
|
OpList.insertBefore(createExtractedAttributeOp(op.target, BindingKind.TwoWayProperty, null, op.name,
|
|
/* expression */ null,
|
|
/* i18nContext */ null,
|
|
/* i18nMessage */ null, op.securityContext), lookupElement$3(elements, op.target));
|
|
break;
|
|
case OpKind.StyleProp:
|
|
case OpKind.ClassProp:
|
|
// TODO: Can style or class bindings be i18n attributes?
|
|
// The old compiler treated empty style bindings as regular bindings for the purpose of
|
|
// directive matching. That behavior is incorrect, but we emulate it in compatibility
|
|
// mode.
|
|
if (unit.job.compatibility === CompatibilityMode.TemplateDefinitionBuilder &&
|
|
op.expression instanceof EmptyExpr) {
|
|
OpList.insertBefore(createExtractedAttributeOp(op.target, BindingKind.Property, null, op.name,
|
|
/* expression */ null,
|
|
/* i18nContext */ null,
|
|
/* i18nMessage */ null, SecurityContext.STYLE), lookupElement$3(elements, op.target));
|
|
}
|
|
break;
|
|
case OpKind.Listener:
|
|
if (!op.isLegacyAnimationListener) {
|
|
const extractedAttributeOp = createExtractedAttributeOp(op.target, BindingKind.Property, null, op.name,
|
|
/* expression */ null,
|
|
/* i18nContext */ null,
|
|
/* i18nMessage */ null, SecurityContext.NONE);
|
|
if (job.kind === CompilationJobKind.Host) {
|
|
if (job.compatibility) {
|
|
// TemplateDefinitionBuilder does not extract listener bindings to the const array
|
|
// (which is honestly pretty inconsistent).
|
|
break;
|
|
}
|
|
// This attribute will apply to the enclosing host binding compilation unit, so order
|
|
// doesn't matter.
|
|
unit.create.push(extractedAttributeOp);
|
|
}
|
|
else {
|
|
OpList.insertBefore(extractedAttributeOp, lookupElement$3(elements, op.target));
|
|
}
|
|
}
|
|
break;
|
|
case OpKind.TwoWayListener:
|
|
// Two-way listeners aren't supported in host bindings.
|
|
if (job.kind !== CompilationJobKind.Host) {
|
|
const extractedAttributeOp = createExtractedAttributeOp(op.target, BindingKind.Property, null, op.name,
|
|
/* expression */ null,
|
|
/* i18nContext */ null,
|
|
/* i18nMessage */ null, SecurityContext.NONE);
|
|
OpList.insertBefore(extractedAttributeOp, lookupElement$3(elements, op.target));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Looks up an element in the given map by xref ID.
|
|
*/
|
|
function lookupElement$3(elements, xref) {
|
|
const el = elements.get(xref);
|
|
if (el === undefined) {
|
|
throw new Error('All attributes should have an element-like target.');
|
|
}
|
|
return el;
|
|
}
|
|
/**
|
|
* Extracts an attribute binding.
|
|
*/
|
|
function extractAttributeOp(unit, op, elements) {
|
|
if (op.expression instanceof Interpolation) {
|
|
return;
|
|
}
|
|
let extractable = op.isTextAttribute || op.expression.isConstant();
|
|
if (unit.job.compatibility === CompatibilityMode.TemplateDefinitionBuilder) {
|
|
// TemplateDefinitionBuilder only extracts text attributes. It does not extract attriibute
|
|
// bindings, even if they are constants.
|
|
extractable &&= op.isTextAttribute;
|
|
}
|
|
if (extractable) {
|
|
const extractedAttributeOp = createExtractedAttributeOp(op.target, op.isStructuralTemplateAttribute ? BindingKind.Template : BindingKind.Attribute, op.namespace, op.name, op.expression, op.i18nContext, op.i18nMessage, op.securityContext);
|
|
if (unit.job.kind === CompilationJobKind.Host) {
|
|
// This attribute will apply to the enclosing host binding compilation unit, so order doesn't
|
|
// matter.
|
|
unit.create.push(extractedAttributeOp);
|
|
}
|
|
else {
|
|
const ownerOp = lookupElement$3(elements, op.target);
|
|
OpList.insertBefore(extractedAttributeOp, ownerOp);
|
|
}
|
|
OpList.remove(op);
|
|
}
|
|
}
|
|
|
|
const ARIA_PREFIX = 'aria-';
|
|
/**
|
|
* Returns whether `name` is an ARIA attribute name.
|
|
*
|
|
* This is a heuristic based on whether name begins with and is longer than `aria-`.
|
|
*/
|
|
function isAriaAttribute(name) {
|
|
return name.startsWith(ARIA_PREFIX) && name.length > ARIA_PREFIX.length;
|
|
}
|
|
|
|
/**
|
|
* Looks up an element in the given map by xref ID.
|
|
*/
|
|
function lookupElement$2(elements, xref) {
|
|
const el = elements.get(xref);
|
|
if (el === undefined) {
|
|
throw new Error('All attributes should have an element-like target.');
|
|
}
|
|
return el;
|
|
}
|
|
function specializeBindings(job) {
|
|
const elements = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (!isElementOrContainerOp(op)) {
|
|
continue;
|
|
}
|
|
elements.set(op.xref, op);
|
|
}
|
|
}
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
if (op.kind !== OpKind.Binding) {
|
|
continue;
|
|
}
|
|
switch (op.bindingKind) {
|
|
case BindingKind.Attribute:
|
|
if (op.name === 'ngNonBindable') {
|
|
OpList.remove(op);
|
|
const target = lookupElement$2(elements, op.target);
|
|
target.nonBindable = true;
|
|
}
|
|
else if (op.name.startsWith('animate.')) {
|
|
OpList.replace(op, createAnimationBindingOp(op.name, op.target, op.name === 'animate.enter' ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, op.expression, op.securityContext, op.sourceSpan, 0 /* ir.AnimationBindingKind.STRING */));
|
|
}
|
|
else {
|
|
const [namespace, name] = splitNsName(op.name);
|
|
OpList.replace(op, createAttributeOp(op.target, namespace, name, op.expression, op.securityContext, op.isTextAttribute, op.isStructuralTemplateAttribute, op.templateKind, op.i18nMessage, op.sourceSpan));
|
|
}
|
|
break;
|
|
case BindingKind.Animation:
|
|
OpList.replace(op, createAnimationBindingOp(op.name, op.target, op.name === 'animate.enter' ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, op.expression, op.securityContext, op.sourceSpan, 1 /* ir.AnimationBindingKind.VALUE */));
|
|
break;
|
|
case BindingKind.Property:
|
|
case BindingKind.LegacyAnimation:
|
|
// Convert a property binding targeting an ARIA attribute (e.g. [aria-label]) into an
|
|
// attribute binding when we know it can't also target an input. Note that a `Host` job is
|
|
// always `DomOnly`, so this condition must be checked first.
|
|
if (job.mode === TemplateCompilationMode.DomOnly && isAriaAttribute(op.name)) {
|
|
OpList.replace(op, createAttributeOp(op.target,
|
|
/* namespace= */ null, op.name, op.expression, op.securityContext,
|
|
/* isTextAttribute= */ false, op.isStructuralTemplateAttribute, op.templateKind, op.i18nMessage, op.sourceSpan));
|
|
}
|
|
else if (job.kind === CompilationJobKind.Host) {
|
|
OpList.replace(op, createDomPropertyOp(op.name, op.expression, op.bindingKind, op.i18nContext, op.securityContext, op.sourceSpan));
|
|
}
|
|
else {
|
|
OpList.replace(op, createPropertyOp(op.target, op.name, op.expression, op.bindingKind, op.securityContext, op.isStructuralTemplateAttribute, op.templateKind, op.i18nContext, op.i18nMessage, op.sourceSpan));
|
|
}
|
|
break;
|
|
case BindingKind.TwoWayProperty:
|
|
if (!(op.expression instanceof Expression)) {
|
|
// We shouldn't be able to hit this code path since interpolations in two-way bindings
|
|
// result in a parser error. We assert here so that downstream we can assume that
|
|
// the value is always an expression.
|
|
throw new Error(`Expected value of two-way property binding "${op.name}" to be an expression`);
|
|
}
|
|
OpList.replace(op, createTwoWayPropertyOp(op.target, op.name, op.expression, op.securityContext, op.isStructuralTemplateAttribute, op.templateKind, op.i18nContext, op.i18nMessage, op.sourceSpan));
|
|
break;
|
|
case BindingKind.I18n:
|
|
case BindingKind.ClassName:
|
|
case BindingKind.StyleProperty:
|
|
throw new Error(`Unhandled binding of kind ${BindingKind[op.bindingKind]}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const CHAIN_COMPATIBILITY = new Map([
|
|
[Identifiers.ariaProperty, Identifiers.ariaProperty],
|
|
[Identifiers.attribute, Identifiers.attribute],
|
|
[Identifiers.classProp, Identifiers.classProp],
|
|
[Identifiers.element, Identifiers.element],
|
|
[Identifiers.elementContainer, Identifiers.elementContainer],
|
|
[Identifiers.elementContainerEnd, Identifiers.elementContainerEnd],
|
|
[Identifiers.elementContainerStart, Identifiers.elementContainerStart],
|
|
[Identifiers.elementEnd, Identifiers.elementEnd],
|
|
[Identifiers.elementStart, Identifiers.elementStart],
|
|
[Identifiers.domProperty, Identifiers.domProperty],
|
|
[Identifiers.i18nExp, Identifiers.i18nExp],
|
|
[Identifiers.listener, Identifiers.listener],
|
|
[Identifiers.listener, Identifiers.listener],
|
|
[Identifiers.property, Identifiers.property],
|
|
[Identifiers.styleProp, Identifiers.styleProp],
|
|
[Identifiers.syntheticHostListener, Identifiers.syntheticHostListener],
|
|
[Identifiers.syntheticHostProperty, Identifiers.syntheticHostProperty],
|
|
[Identifiers.templateCreate, Identifiers.templateCreate],
|
|
[Identifiers.twoWayProperty, Identifiers.twoWayProperty],
|
|
[Identifiers.twoWayListener, Identifiers.twoWayListener],
|
|
[Identifiers.declareLet, Identifiers.declareLet],
|
|
[Identifiers.conditionalCreate, Identifiers.conditionalBranchCreate],
|
|
[Identifiers.conditionalBranchCreate, Identifiers.conditionalBranchCreate],
|
|
[Identifiers.domElement, Identifiers.domElement],
|
|
[Identifiers.domElementStart, Identifiers.domElementStart],
|
|
[Identifiers.domElementEnd, Identifiers.domElementEnd],
|
|
[Identifiers.domElementContainer, Identifiers.domElementContainer],
|
|
[Identifiers.domElementContainerStart, Identifiers.domElementContainerStart],
|
|
[Identifiers.domElementContainerEnd, Identifiers.domElementContainerEnd],
|
|
[Identifiers.domListener, Identifiers.domListener],
|
|
[Identifiers.domTemplate, Identifiers.domTemplate],
|
|
[Identifiers.animationEnter, Identifiers.animationEnter],
|
|
[Identifiers.animationLeave, Identifiers.animationLeave],
|
|
[Identifiers.animationEnterListener, Identifiers.animationEnterListener],
|
|
[Identifiers.animationLeaveListener, Identifiers.animationLeaveListener],
|
|
]);
|
|
/**
|
|
* Chaining results in repeated call expressions, causing a deep AST of receiver expressions. To prevent running out of
|
|
* stack depth the maximum number of chained instructions is limited to this threshold, which has been selected
|
|
* arbitrarily.
|
|
*/
|
|
const MAX_CHAIN_LENGTH = 256;
|
|
/**
|
|
* Post-process a reified view compilation and convert sequential calls to chainable instructions
|
|
* into chain calls.
|
|
*
|
|
* For example, two `elementStart` operations in sequence:
|
|
*
|
|
* ```ts
|
|
* elementStart(0, 'div');
|
|
* elementStart(1, 'span');
|
|
* ```
|
|
*
|
|
* Can be called as a chain instead:
|
|
*
|
|
* ```ts
|
|
* elementStart(0, 'div')(1, 'span');
|
|
* ```
|
|
*/
|
|
function chain(job) {
|
|
for (const unit of job.units) {
|
|
chainOperationsInList(unit.create);
|
|
chainOperationsInList(unit.update);
|
|
}
|
|
}
|
|
function chainOperationsInList(opList) {
|
|
let chain = null;
|
|
for (const op of opList) {
|
|
if (op.kind !== OpKind.Statement || !(op.statement instanceof ExpressionStatement)) {
|
|
// This type of statement isn't chainable.
|
|
chain = null;
|
|
continue;
|
|
}
|
|
if (!(op.statement.expr instanceof InvokeFunctionExpr) ||
|
|
!(op.statement.expr.fn instanceof ExternalExpr)) {
|
|
// This is a statement, but not an instruction-type call, so not chainable.
|
|
chain = null;
|
|
continue;
|
|
}
|
|
const instruction = op.statement.expr.fn.value;
|
|
if (!CHAIN_COMPATIBILITY.has(instruction)) {
|
|
// This instruction isn't chainable.
|
|
chain = null;
|
|
continue;
|
|
}
|
|
// This instruction can be chained. It can either be added on to the previous chain (if
|
|
// compatible) or it can be the start of a new chain.
|
|
if (chain !== null &&
|
|
CHAIN_COMPATIBILITY.get(chain.instruction) === instruction &&
|
|
chain.length < MAX_CHAIN_LENGTH) {
|
|
// This instruction can be added onto the previous chain.
|
|
const expression = chain.expression.callFn(op.statement.expr.args, op.statement.expr.sourceSpan, op.statement.expr.pure);
|
|
chain.expression = expression;
|
|
chain.op.statement = expression.toStmt();
|
|
chain.length++;
|
|
OpList.remove(op);
|
|
}
|
|
else {
|
|
// Leave this instruction alone for now, but consider it the start of a new chain.
|
|
chain = {
|
|
op,
|
|
instruction,
|
|
expression: op.statement.expr,
|
|
length: 1,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attribute or style interpolations of the form `[attr.foo]="{{foo}}""` should be "collapsed"
|
|
* into a plain instruction, instead of an interpolated one.
|
|
*
|
|
* (We cannot do this for singleton property interpolations,
|
|
* because they need to stringify their expressions)
|
|
*
|
|
* The reification step is also capable of performing this transformation, but doing it early in the
|
|
* pipeline allows other phases to accurately know what instruction will be emitted.
|
|
*/
|
|
function collapseSingletonInterpolations(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
const eligibleOpKind = op.kind === OpKind.Attribute ||
|
|
op.kind === OpKind.StyleProp ||
|
|
op.kind == OpKind.StyleMap ||
|
|
op.kind === OpKind.ClassMap;
|
|
if (eligibleOpKind &&
|
|
op.expression instanceof Interpolation &&
|
|
op.expression.strings.length === 2 &&
|
|
op.expression.strings.every((s) => s === '')) {
|
|
op.expression = op.expression.expressions[0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collapse the various conditions of conditional ops (if, switch) into a single test expression.
|
|
*/
|
|
function generateConditionalExpressions(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
if (op.kind !== OpKind.Conditional) {
|
|
continue;
|
|
}
|
|
let test;
|
|
// Any case with a `null` condition is `default`. If one exists, default to it instead.
|
|
const defaultCase = op.conditions.findIndex((cond) => cond.expr === null);
|
|
if (defaultCase >= 0) {
|
|
const slot = op.conditions.splice(defaultCase, 1)[0].targetSlot;
|
|
test = new SlotLiteralExpr(slot);
|
|
}
|
|
else {
|
|
// By default, a switch evaluates to `-1`, causing no template to be displayed.
|
|
test = literal(-1);
|
|
}
|
|
// Switch expressions assign their main test to a temporary, to avoid re-executing it.
|
|
let tmp = op.test == null ? null : new AssignTemporaryExpr(op.test, job.allocateXrefId());
|
|
let caseExpressionTemporaryXref = null;
|
|
// For each remaining condition, test whether the temporary satifies the check. (If no temp is
|
|
// present, just check each expression directly.)
|
|
for (let i = op.conditions.length - 1; i >= 0; i--) {
|
|
let conditionalCase = op.conditions[i];
|
|
if (conditionalCase.expr === null) {
|
|
continue;
|
|
}
|
|
if (tmp !== null) {
|
|
const useTmp = i === 0 ? tmp : new ReadTemporaryExpr(tmp.xref);
|
|
conditionalCase.expr = new BinaryOperatorExpr(BinaryOperator.Identical, useTmp, conditionalCase.expr);
|
|
}
|
|
else if (conditionalCase.alias !== null) {
|
|
// Since we can only pass one variable into the conditional instruction,
|
|
// reuse the same variable to store the result of the expressions.
|
|
caseExpressionTemporaryXref ??= job.allocateXrefId();
|
|
conditionalCase.expr = new AssignTemporaryExpr(conditionalCase.expr, caseExpressionTemporaryXref);
|
|
op.contextValue = new ReadTemporaryExpr(caseExpressionTemporaryXref);
|
|
}
|
|
test = new ConditionalExpr(conditionalCase.expr, new SlotLiteralExpr(conditionalCase.targetSlot), test);
|
|
}
|
|
// Save the resulting aggregate Joost-expression.
|
|
op.processed = test;
|
|
// Clear the original conditions array, since we no longer need it, and don't want it to
|
|
// affect subsequent phases (e.g. pipe creation).
|
|
op.conditions = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
const BINARY_OPERATORS = new Map([
|
|
['&&', BinaryOperator.And],
|
|
['>', BinaryOperator.Bigger],
|
|
['>=', BinaryOperator.BiggerEquals],
|
|
['|', BinaryOperator.BitwiseOr],
|
|
['&', BinaryOperator.BitwiseAnd],
|
|
['/', BinaryOperator.Divide],
|
|
['=', BinaryOperator.Assign],
|
|
['==', BinaryOperator.Equals],
|
|
['===', BinaryOperator.Identical],
|
|
['<', BinaryOperator.Lower],
|
|
['<=', BinaryOperator.LowerEquals],
|
|
['-', BinaryOperator.Minus],
|
|
['%', BinaryOperator.Modulo],
|
|
['**', BinaryOperator.Exponentiation],
|
|
['*', BinaryOperator.Multiply],
|
|
['!=', BinaryOperator.NotEquals],
|
|
['!==', BinaryOperator.NotIdentical],
|
|
['??', BinaryOperator.NullishCoalesce],
|
|
['||', BinaryOperator.Or],
|
|
['+', BinaryOperator.Plus],
|
|
['in', BinaryOperator.In],
|
|
['+=', BinaryOperator.AdditionAssignment],
|
|
['-=', BinaryOperator.SubtractionAssignment],
|
|
['*=', BinaryOperator.MultiplicationAssignment],
|
|
['/=', BinaryOperator.DivisionAssignment],
|
|
['%=', BinaryOperator.RemainderAssignment],
|
|
['**=', BinaryOperator.ExponentiationAssignment],
|
|
['&&=', BinaryOperator.AndAssignment],
|
|
['||=', BinaryOperator.OrAssignment],
|
|
['??=', BinaryOperator.NullishCoalesceAssignment],
|
|
]);
|
|
function namespaceForKey(namespacePrefixKey) {
|
|
const NAMESPACES = new Map([
|
|
['svg', Namespace.SVG],
|
|
['math', Namespace.Math],
|
|
]);
|
|
if (namespacePrefixKey === null) {
|
|
return Namespace.HTML;
|
|
}
|
|
return NAMESPACES.get(namespacePrefixKey) ?? Namespace.HTML;
|
|
}
|
|
function keyForNamespace(namespace) {
|
|
const NAMESPACES = new Map([
|
|
['svg', Namespace.SVG],
|
|
['math', Namespace.Math],
|
|
]);
|
|
for (const [k, n] of NAMESPACES.entries()) {
|
|
if (n === namespace) {
|
|
return k;
|
|
}
|
|
}
|
|
return null; // No namespace prefix for HTML
|
|
}
|
|
function prefixWithNamespace(strippedTag, namespace) {
|
|
if (namespace === Namespace.HTML) {
|
|
return strippedTag;
|
|
}
|
|
return `:${keyForNamespace(namespace)}:${strippedTag}`;
|
|
}
|
|
function literalOrArrayLiteral(value) {
|
|
if (Array.isArray(value)) {
|
|
return literalArr(value.map(literalOrArrayLiteral));
|
|
}
|
|
return literal(value);
|
|
}
|
|
|
|
/**
|
|
* Converts the semantic attributes of element-like operations (elements, templates) into constant
|
|
* array expressions, and lifts them into the overall component `consts`.
|
|
*/
|
|
function collectElementConsts(job) {
|
|
// Collect all extracted attributes.
|
|
const allElementAttributes = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.ExtractedAttribute) {
|
|
const attributes = allElementAttributes.get(op.target) || new ElementAttributes(job.compatibility);
|
|
allElementAttributes.set(op.target, attributes);
|
|
attributes.add(op.bindingKind, op.name, op.expression, op.namespace, op.trustedValueFn);
|
|
OpList.remove(op);
|
|
}
|
|
}
|
|
}
|
|
// Serialize the extracted attributes into the const array.
|
|
if (job instanceof ComponentCompilationJob) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
// TODO: Simplify and combine these cases.
|
|
if (op.kind == OpKind.Projection) {
|
|
const attributes = allElementAttributes.get(op.xref);
|
|
if (attributes !== undefined) {
|
|
const attrArray = serializeAttributes(attributes);
|
|
if (attrArray.entries.length > 0) {
|
|
op.attributes = attrArray;
|
|
}
|
|
}
|
|
}
|
|
else if (isElementOrContainerOp(op)) {
|
|
op.attributes = getConstIndex(job, allElementAttributes, op.xref);
|
|
// TODO(dylhunn): `@for` loops with `@empty` blocks need to be special-cased here,
|
|
// because the slot consumer trait currently only supports one slot per consumer and we
|
|
// need two. This should be revisited when making the refactors mentioned in:
|
|
// https://github.com/angular/angular/pull/53620#discussion_r1430918822
|
|
if (op.kind === OpKind.RepeaterCreate && op.emptyView !== null) {
|
|
op.emptyAttributes = getConstIndex(job, allElementAttributes, op.emptyView);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (job instanceof HostBindingCompilationJob) {
|
|
// TODO: If the host binding case further diverges, we may want to split it into its own
|
|
// phase.
|
|
for (const [xref, attributes] of allElementAttributes.entries()) {
|
|
if (xref !== job.root.xref) {
|
|
throw new Error(`An attribute would be const collected into the host binding's template function, but is not associated with the root xref.`);
|
|
}
|
|
const attrArray = serializeAttributes(attributes);
|
|
if (attrArray.entries.length > 0) {
|
|
job.root.attributes = attrArray;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function getConstIndex(job, allElementAttributes, xref) {
|
|
const attributes = allElementAttributes.get(xref);
|
|
if (attributes !== undefined) {
|
|
const attrArray = serializeAttributes(attributes);
|
|
if (attrArray.entries.length > 0) {
|
|
return job.addConst(attrArray);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Shared instance of an empty array to avoid unnecessary array allocations.
|
|
*/
|
|
const FLYWEIGHT_ARRAY = Object.freeze([]);
|
|
/**
|
|
* Container for all of the various kinds of attributes which are applied on an element.
|
|
*/
|
|
class ElementAttributes {
|
|
compatibility;
|
|
known = new Map();
|
|
byKind = new Map();
|
|
propertyBindings = null;
|
|
projectAs = null;
|
|
get attributes() {
|
|
return this.byKind.get(BindingKind.Attribute) ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
get classes() {
|
|
return this.byKind.get(BindingKind.ClassName) ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
get styles() {
|
|
return this.byKind.get(BindingKind.StyleProperty) ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
get bindings() {
|
|
return this.propertyBindings ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
get template() {
|
|
return this.byKind.get(BindingKind.Template) ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
get i18n() {
|
|
return this.byKind.get(BindingKind.I18n) ?? FLYWEIGHT_ARRAY;
|
|
}
|
|
constructor(compatibility) {
|
|
this.compatibility = compatibility;
|
|
}
|
|
isKnown(kind, name) {
|
|
const nameToValue = this.known.get(kind) ?? new Set();
|
|
this.known.set(kind, nameToValue);
|
|
if (nameToValue.has(name)) {
|
|
return true;
|
|
}
|
|
nameToValue.add(name);
|
|
return false;
|
|
}
|
|
add(kind, name, value, namespace, trustedValueFn) {
|
|
// TemplateDefinitionBuilder puts duplicate attribute, class, and style values into the consts
|
|
// array. This seems inefficient, we can probably keep just the first one or the last value
|
|
// (whichever actually gets applied when multiple values are listed for the same attribute).
|
|
const allowDuplicates = this.compatibility === CompatibilityMode.TemplateDefinitionBuilder &&
|
|
(kind === BindingKind.Attribute ||
|
|
kind === BindingKind.ClassName ||
|
|
kind === BindingKind.StyleProperty);
|
|
if (!allowDuplicates && this.isKnown(kind, name)) {
|
|
return;
|
|
}
|
|
// TODO: Can this be its own phase
|
|
if (name === 'ngProjectAs') {
|
|
if (value === null ||
|
|
!(value instanceof LiteralExpr) ||
|
|
value.value == null ||
|
|
typeof value.value?.toString() !== 'string') {
|
|
throw Error('ngProjectAs must have a string literal value');
|
|
}
|
|
this.projectAs = value.value.toString();
|
|
// TODO: TemplateDefinitionBuilder allows `ngProjectAs` to also be assigned as a literal
|
|
// attribute. Is this sane?
|
|
}
|
|
const array = this.arrayFor(kind);
|
|
array.push(...getAttributeNameLiterals(namespace, name));
|
|
if (kind === BindingKind.Attribute || kind === BindingKind.StyleProperty) {
|
|
if (value === null) {
|
|
throw Error('Attribute, i18n attribute, & style element attributes must have a value');
|
|
}
|
|
if (trustedValueFn !== null) {
|
|
if (!isStringLiteral(value)) {
|
|
throw Error('AssertionError: extracted attribute value should be string literal');
|
|
}
|
|
array.push(taggedTemplate(trustedValueFn, new TemplateLiteralExpr([new TemplateLiteralElementExpr(value.value)], []), undefined, value.sourceSpan));
|
|
}
|
|
else {
|
|
array.push(value);
|
|
}
|
|
}
|
|
}
|
|
arrayFor(kind) {
|
|
if (kind === BindingKind.Property || kind === BindingKind.TwoWayProperty) {
|
|
this.propertyBindings ??= [];
|
|
return this.propertyBindings;
|
|
}
|
|
else {
|
|
if (!this.byKind.has(kind)) {
|
|
this.byKind.set(kind, []);
|
|
}
|
|
return this.byKind.get(kind);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Gets an array of literal expressions representing the attribute's namespaced name.
|
|
*/
|
|
function getAttributeNameLiterals(namespace, name) {
|
|
const nameLiteral = literal(name);
|
|
if (namespace) {
|
|
return [literal(0 /* core.AttributeMarker.NamespaceURI */), literal(namespace), nameLiteral];
|
|
}
|
|
return [nameLiteral];
|
|
}
|
|
/**
|
|
* Serializes an ElementAttributes object into an array expression.
|
|
*/
|
|
function serializeAttributes({ attributes, bindings, classes, i18n, projectAs, styles, template, }) {
|
|
const attrArray = [...attributes];
|
|
if (projectAs !== null) {
|
|
// Parse the attribute value into a CssSelectorList. Note that we only take the
|
|
// first selector, because we don't support multiple selectors in ngProjectAs.
|
|
const parsedR3Selector = parseSelectorToR3Selector(projectAs)[0];
|
|
attrArray.push(literal(5 /* core.AttributeMarker.ProjectAs */), literalOrArrayLiteral(parsedR3Selector));
|
|
}
|
|
if (classes.length > 0) {
|
|
attrArray.push(literal(1 /* core.AttributeMarker.Classes */), ...classes);
|
|
}
|
|
if (styles.length > 0) {
|
|
attrArray.push(literal(2 /* core.AttributeMarker.Styles */), ...styles);
|
|
}
|
|
if (bindings.length > 0) {
|
|
attrArray.push(literal(3 /* core.AttributeMarker.Bindings */), ...bindings);
|
|
}
|
|
if (template.length > 0) {
|
|
attrArray.push(literal(4 /* core.AttributeMarker.Template */), ...template);
|
|
}
|
|
if (i18n.length > 0) {
|
|
attrArray.push(literal(6 /* core.AttributeMarker.I18n */), ...i18n);
|
|
}
|
|
return literalArr(attrArray);
|
|
}
|
|
|
|
/**
|
|
* Looks up an element in the given map by xref ID.
|
|
*/
|
|
function lookupElement$1(elements, xref) {
|
|
const el = elements.get(xref);
|
|
if (el === undefined) {
|
|
throw new Error('All attributes should have an element-like target.');
|
|
}
|
|
return el;
|
|
}
|
|
function convertAnimations(job) {
|
|
const elements = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (!isElementOrContainerOp(op)) {
|
|
continue;
|
|
}
|
|
elements.set(op.xref, op);
|
|
}
|
|
}
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
if (op.kind === OpKind.AnimationBinding) {
|
|
const createAnimationOp = getAnimationOp(op);
|
|
if (job.kind === CompilationJobKind.Host) {
|
|
unit.create.push(createAnimationOp);
|
|
}
|
|
else {
|
|
OpList.insertAfter(createAnimationOp, lookupElement$1(elements, op.target));
|
|
}
|
|
OpList.remove(op);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function getAnimationOp(op) {
|
|
if (op.animationBindingKind === 0 /* ir.AnimationBindingKind.STRING */) {
|
|
// this is a simple string case
|
|
return createAnimationStringOp(op.name, op.target, op.name === 'animate.enter' ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, op.expression, op.securityContext, op.sourceSpan);
|
|
}
|
|
else {
|
|
const expression = op.expression;
|
|
return createAnimationOp(op.name, op.target, op.name === 'animate.enter' ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, [createStatementOp(new ReturnStatement(expression, expression.sourceSpan))], op.securityContext, op.sourceSpan);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Some binding instructions in the update block may actually correspond to i18n bindings. In that
|
|
* case, they should be replaced with i18nExp instructions for the dynamic portions.
|
|
*/
|
|
function convertI18nBindings(job) {
|
|
const i18nAttributesByElem = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.I18nAttributes) {
|
|
i18nAttributesByElem.set(op.target, op);
|
|
}
|
|
}
|
|
for (const op of unit.update) {
|
|
switch (op.kind) {
|
|
case OpKind.Property:
|
|
case OpKind.Attribute:
|
|
if (op.i18nContext === null) {
|
|
continue;
|
|
}
|
|
if (!(op.expression instanceof Interpolation)) {
|
|
continue;
|
|
}
|
|
const i18nAttributesForElem = i18nAttributesByElem.get(op.target);
|
|
if (i18nAttributesForElem === undefined) {
|
|
throw new Error('AssertionError: An i18n attribute binding instruction requires the owning element to have an I18nAttributes create instruction');
|
|
}
|
|
if (i18nAttributesForElem.target !== op.target) {
|
|
throw new Error('AssertionError: Expected i18nAttributes target element to match binding target element');
|
|
}
|
|
const ops = [];
|
|
for (let i = 0; i < op.expression.expressions.length; i++) {
|
|
const expr = op.expression.expressions[i];
|
|
if (op.expression.i18nPlaceholders.length !== op.expression.expressions.length) {
|
|
throw new Error(`AssertionError: An i18n attribute binding instruction requires the same number of expressions and placeholders, but found ${op.expression.i18nPlaceholders.length} placeholders and ${op.expression.expressions.length} expressions`);
|
|
}
|
|
ops.push(createI18nExpressionOp(op.i18nContext, i18nAttributesForElem.target, i18nAttributesForElem.xref, i18nAttributesForElem.handle, expr, null, op.expression.i18nPlaceholders[i], I18nParamResolutionTime.Creation, I18nExpressionFor.I18nAttribute, op.name, op.sourceSpan));
|
|
}
|
|
OpList.replaceWithMany(op, ops);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create one helper context op per i18n block (including generate descending blocks).
|
|
*
|
|
* Also, if an ICU exists inside an i18n block that also contains other localizable content (such as
|
|
* string), create an additional helper context op for the ICU.
|
|
*
|
|
* These context ops are later used for generating i18n messages. (Although we generate at least one
|
|
* context op per nested view, we will collect them up the tree later, to generate a top-level
|
|
* message.)
|
|
*/
|
|
function createI18nContexts(job) {
|
|
// Create i18n context ops for i18n attrs.
|
|
const attrContextByMessage = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
switch (op.kind) {
|
|
case OpKind.Binding:
|
|
case OpKind.Property:
|
|
case OpKind.Attribute:
|
|
case OpKind.ExtractedAttribute:
|
|
if (op.i18nMessage === null) {
|
|
continue;
|
|
}
|
|
if (!attrContextByMessage.has(op.i18nMessage)) {
|
|
const i18nContext = createI18nContextOp(I18nContextKind.Attr, job.allocateXrefId(), null, op.i18nMessage, null);
|
|
unit.create.push(i18nContext);
|
|
attrContextByMessage.set(op.i18nMessage, i18nContext.xref);
|
|
}
|
|
op.i18nContext = attrContextByMessage.get(op.i18nMessage);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Create i18n context ops for root i18n blocks.
|
|
const blockContextByI18nBlock = new Map();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
switch (op.kind) {
|
|
case OpKind.I18nStart:
|
|
if (op.xref === op.root) {
|
|
const contextOp = createI18nContextOp(I18nContextKind.RootI18n, job.allocateXrefId(), op.xref, op.message, null);
|
|
unit.create.push(contextOp);
|
|
op.context = contextOp.xref;
|
|
blockContextByI18nBlock.set(op.xref, contextOp);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Assign i18n contexts for child i18n blocks. These don't need their own conext, instead they
|
|
// should inherit from their root i18n block.
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.I18nStart && op.xref !== op.root) {
|
|
const rootContext = blockContextByI18nBlock.get(op.root);
|
|
if (rootContext === undefined) {
|
|
throw Error('AssertionError: Root i18n block i18n context should have been created.');
|
|
}
|
|
op.context = rootContext.xref;
|
|
blockContextByI18nBlock.set(op.xref, rootContext);
|
|
}
|
|
}
|
|
}
|
|
// Create or assign i18n contexts for ICUs.
|
|
let currentI18nOp = null;
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
switch (op.kind) {
|
|
case OpKind.I18nStart:
|
|
currentI18nOp = op;
|
|
break;
|
|
case OpKind.I18nEnd:
|
|
currentI18nOp = null;
|
|
break;
|
|
case OpKind.IcuStart:
|
|
if (currentI18nOp === null) {
|
|
throw Error('AssertionError: Unexpected ICU outside of an i18n block.');
|
|
}
|
|
if (op.message.id !== currentI18nOp.message.id) {
|
|
// This ICU is a sub-message inside its parent i18n block message. We need to give it
|
|
// its own context.
|
|
const contextOp = createI18nContextOp(I18nContextKind.Icu, job.allocateXrefId(), currentI18nOp.root, op.message, null);
|
|
unit.create.push(contextOp);
|
|
op.context = contextOp.xref;
|
|
}
|
|
else {
|
|
// This ICU is the only translatable content in its parent i18n block. We need to
|
|
// convert the parent's context into an ICU context.
|
|
op.context = currentI18nOp.context;
|
|
blockContextByI18nBlock.get(currentI18nOp.xref).contextKind = I18nContextKind.Icu;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deduplicate text bindings, e.g. <div class="cls1" class="cls2">
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
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.Never:
|
|
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, op.modifier === "hydrate" /* ir.DeferOpModifierKind.HYDRATE */
|
|
? deferOp.mainView
|
|
: deferOp.placeholderView);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let Scope$1 = class Scope {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
else if (e instanceof ParenthesizedExpr) {
|
|
return needsTemporaryInSafeAccess(e.expr);
|
|
}
|
|
// 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 ParenthesizedExpr(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, 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(`AssertionError: 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) {
|
|
let consumer = null;
|
|
if (hasDependsOnSlotContextTrait(op)) {
|
|
consumer = op;
|
|
}
|
|
else {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (consumer === null && hasDependsOnSlotContextTrait(expr)) {
|
|
consumer = expr;
|
|
}
|
|
});
|
|
}
|
|
if (consumer === null) {
|
|
continue;
|
|
}
|
|
if (!slotMap.has(consumer.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 ${consumer.target}`);
|
|
}
|
|
const slot = slotMap.get(consumer.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, consumer.sourceSpan), op);
|
|
slotContext = slot;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces the `storeLet` ops with variables that can be
|
|
* used to reference the value within the same view.
|
|
*/
|
|
function generateLocalLetReferences(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
if (op.kind !== OpKind.StoreLet) {
|
|
continue;
|
|
}
|
|
const variable = {
|
|
kind: SemanticVariableKind.Identifier,
|
|
name: null,
|
|
identifier: op.declaredName,
|
|
local: true,
|
|
};
|
|
OpList.replace(op, createVariableOp(job.allocateXrefId(), variable, new StoreLetExpr(op.target, op.value, op.sourceSpan), VariableFlags.None));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
case OpKind.Template:
|
|
// Descend into child embedded views.
|
|
recursivelyProcessView(view.job.views.get(op.xref), scope);
|
|
break;
|
|
case OpKind.Projection:
|
|
if (op.fallbackView !== null) {
|
|
recursivelyProcessView(view.job.views.get(op.fallbackView), 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);
|
|
}
|
|
if (op.trackByOps !== null) {
|
|
op.trackByOps.prepend(generateVariablesInScopeForView(view, scope, false));
|
|
}
|
|
break;
|
|
case OpKind.Animation:
|
|
case OpKind.AnimationListener:
|
|
case OpKind.Listener:
|
|
case OpKind.TwoWayListener:
|
|
// Prepend variables to listener handler functions.
|
|
op.handlerOps.prepend(generateVariablesInScopeForView(view, scope, true));
|
|
break;
|
|
}
|
|
}
|
|
view.update.prepend(generateVariablesInScopeForView(view, scope, false));
|
|
}
|
|
/**
|
|
* 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: [],
|
|
letDeclarations: [],
|
|
parent,
|
|
};
|
|
for (const identifier of view.contextVariables.keys()) {
|
|
scope.contextVariables.set(identifier, {
|
|
kind: SemanticVariableKind.Identifier,
|
|
name: null,
|
|
identifier,
|
|
local: false,
|
|
});
|
|
}
|
|
for (const op of view.create) {
|
|
switch (op.kind) {
|
|
case OpKind.ElementStart:
|
|
case OpKind.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
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,
|
|
local: false,
|
|
},
|
|
});
|
|
}
|
|
break;
|
|
case OpKind.DeclareLet:
|
|
scope.letDeclarations.push({
|
|
targetId: op.xref,
|
|
targetSlot: op.handle,
|
|
variable: {
|
|
kind: SemanticVariableKind.Identifier,
|
|
name: null,
|
|
identifier: op.declaredName,
|
|
local: false,
|
|
},
|
|
});
|
|
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, isCallback) {
|
|
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.view !== view.xref || isCallback) {
|
|
for (const decl of scope.letDeclarations) {
|
|
newOps.push(createVariableOp(view.job.allocateXrefId(), decl.variable, new ContextLetReferenceExpr(decl.targetId, decl.targetSlot), VariableFlags.None));
|
|
}
|
|
}
|
|
if (scope.parent !== null) {
|
|
// Recursively add variables from the parent scope.
|
|
newOps.push(...generateVariablesInScopeForView(view, scope.parent, false));
|
|
}
|
|
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(op.name)) {
|
|
op.name = hyphenate$1(op.name);
|
|
}
|
|
const { property, suffix } = parseProperty(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(op.name.substring(CLASS_DOT.length)).property;
|
|
}
|
|
else if (op.name.startsWith(CLASS_BANG)) {
|
|
op.bindingKind = BindingKind.ClassName;
|
|
op.name = parseProperty(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(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(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 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);
|
|
}
|
|
|
|
class NodeWithI18n {
|
|
sourceSpan;
|
|
i18n;
|
|
constructor(sourceSpan, i18n) {
|
|
this.sourceSpan = sourceSpan;
|
|
this.i18n = i18n;
|
|
}
|
|
}
|
|
class Text extends NodeWithI18n {
|
|
value;
|
|
tokens;
|
|
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 {
|
|
switchValue;
|
|
type;
|
|
cases;
|
|
switchValueSourceSpan;
|
|
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 {
|
|
value;
|
|
expression;
|
|
sourceSpan;
|
|
valueSourceSpan;
|
|
expSourceSpan;
|
|
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 {
|
|
name;
|
|
value;
|
|
keySpan;
|
|
valueSpan;
|
|
valueTokens;
|
|
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 {
|
|
name;
|
|
attrs;
|
|
directives;
|
|
children;
|
|
isSelfClosing;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
isVoid;
|
|
constructor(name, attrs, directives, children, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan = null, isVoid, i18n) {
|
|
super(sourceSpan, i18n);
|
|
this.name = name;
|
|
this.attrs = attrs;
|
|
this.directives = directives;
|
|
this.children = children;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
this.isVoid = isVoid;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitElement(this, context);
|
|
}
|
|
}
|
|
class Comment {
|
|
value;
|
|
sourceSpan;
|
|
constructor(value, sourceSpan) {
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitComment(this, context);
|
|
}
|
|
}
|
|
class Block extends NodeWithI18n {
|
|
name;
|
|
parameters;
|
|
children;
|
|
nameSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
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 Component extends NodeWithI18n {
|
|
componentName;
|
|
tagName;
|
|
fullName;
|
|
attrs;
|
|
directives;
|
|
children;
|
|
isSelfClosing;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
constructor(componentName, tagName, fullName, attrs, directives, children, isSelfClosing, sourceSpan, startSourceSpan, endSourceSpan = null, i18n) {
|
|
super(sourceSpan, i18n);
|
|
this.componentName = componentName;
|
|
this.tagName = tagName;
|
|
this.fullName = fullName;
|
|
this.attrs = attrs;
|
|
this.directives = directives;
|
|
this.children = children;
|
|
this.isSelfClosing = isSelfClosing;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitComponent(this, context);
|
|
}
|
|
}
|
|
class Directive {
|
|
name;
|
|
attrs;
|
|
sourceSpan;
|
|
startSourceSpan;
|
|
endSourceSpan;
|
|
constructor(name, attrs, sourceSpan, startSourceSpan, endSourceSpan = null) {
|
|
this.name = name;
|
|
this.attrs = attrs;
|
|
this.sourceSpan = sourceSpan;
|
|
this.startSourceSpan = startSourceSpan;
|
|
this.endSourceSpan = endSourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitDirective(this, context);
|
|
}
|
|
}
|
|
class BlockParameter {
|
|
expression;
|
|
sourceSpan;
|
|
constructor(expression, sourceSpan) {
|
|
this.expression = expression;
|
|
this.sourceSpan = sourceSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitBlockParameter(this, context);
|
|
}
|
|
}
|
|
class LetDeclaration {
|
|
name;
|
|
value;
|
|
sourceSpan;
|
|
nameSpan;
|
|
valueSpan;
|
|
constructor(name, value, sourceSpan, nameSpan, valueSpan) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.sourceSpan = sourceSpan;
|
|
this.nameSpan = nameSpan;
|
|
this.valueSpan = valueSpan;
|
|
}
|
|
visit(visitor, context) {
|
|
return visitor.visitLetDeclaration(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.directives);
|
|
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) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
this.visitChildren(context, (visit) => {
|
|
visit(component.attrs);
|
|
visit(component.children);
|
|
});
|
|
}
|
|
visitDirective(directive, context) {
|
|
this.visitChildren(context, (visit) => {
|
|
visit(directive.attrs);
|
|
});
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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 TokenizeResult {
|
|
tokens;
|
|
errors;
|
|
nonNormalizedIcuExpressions;
|
|
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 "&#<decimal>;" or "&#x<hex>;" 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 = {}));
|
|
const SUPPORTED_BLOCKS = [
|
|
'@if',
|
|
'@else', // Covers `@else if` as well
|
|
'@for',
|
|
'@switch',
|
|
'@case',
|
|
'@default',
|
|
'@empty',
|
|
'@defer',
|
|
'@placeholder',
|
|
'@loading',
|
|
'@error',
|
|
];
|
|
// See https://www.w3.org/TR/html51/syntax.html#writing-html-documents
|
|
class _Tokenizer {
|
|
_getTagDefinition;
|
|
_cursor;
|
|
_tokenizeIcu;
|
|
_interpolationConfig;
|
|
_leadingTriviaCodePoints;
|
|
_currentTokenStart = null;
|
|
_currentTokenType = null;
|
|
_expansionCaseStack = [];
|
|
_openDirectiveCount = 0;
|
|
_inInterpolation = false;
|
|
_preserveLineEndings;
|
|
_i18nNormalizeLineEndingsInICUs;
|
|
_tokenizeBlocks;
|
|
_tokenizeLet;
|
|
_selectorlessEnabled;
|
|
tokens = [];
|
|
errors = [];
|
|
nonNormalizedIcuExpressions = [];
|
|
/**
|
|
* @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._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;
|
|
this._tokenizeLet = options.tokenizeLet ?? true;
|
|
this._selectorlessEnabled = options.selectorlessEnabled ?? false;
|
|
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._tokenizeLet &&
|
|
// Use `peek` instead of `attempCharCode` since we
|
|
// don't want to advance in case it's not `@let`.
|
|
this._cursor.peek() === $AT &&
|
|
!this._inInterpolation &&
|
|
this._isLetStart()) {
|
|
this._consumeLetDeclaration(start);
|
|
}
|
|
else if (this._tokenizeBlocks && this._isBlockStart()) {
|
|
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(41 /* 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._requireCharCode($AT);
|
|
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);
|
|
}
|
|
}
|
|
_consumeLetDeclaration(start) {
|
|
this._requireStr('@let');
|
|
this._beginToken(29 /* TokenType.LET_START */, start);
|
|
// Require at least one white space after the `@let`.
|
|
if (isWhitespace(this._cursor.peek())) {
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
}
|
|
else {
|
|
const token = this._endToken([this._cursor.getChars(start)]);
|
|
token.type = 32 /* TokenType.INCOMPLETE_LET */;
|
|
return;
|
|
}
|
|
const startToken = this._endToken([this._getLetDeclarationName()]);
|
|
// Skip over white space before the equals character.
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
// Expect an equals sign.
|
|
if (!this._attemptCharCode($EQ)) {
|
|
startToken.type = 32 /* TokenType.INCOMPLETE_LET */;
|
|
return;
|
|
}
|
|
// Skip spaces after the equals.
|
|
this._attemptCharCodeUntilFn((code) => isNotWhitespace(code) && !isNewLine(code));
|
|
this._consumeLetDeclarationValue();
|
|
// Terminate the `@let` with a semicolon.
|
|
const endChar = this._cursor.peek();
|
|
if (endChar === $SEMICOLON) {
|
|
this._beginToken(31 /* TokenType.LET_END */);
|
|
this._endToken([]);
|
|
this._cursor.advance();
|
|
}
|
|
else {
|
|
startToken.type = 32 /* TokenType.INCOMPLETE_LET */;
|
|
startToken.sourceSpan = this._cursor.getSpan(start);
|
|
}
|
|
}
|
|
_getLetDeclarationName() {
|
|
const nameCursor = this._cursor.clone();
|
|
let allowDigit = false;
|
|
this._attemptCharCodeUntilFn((code) => {
|
|
if (isAsciiLetter(code) ||
|
|
code === $$ ||
|
|
code === $_ ||
|
|
// `@let` names can't start with a digit, but digits are valid anywhere else in the name.
|
|
(allowDigit && isDigit(code))) {
|
|
allowDigit = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return this._cursor.getChars(nameCursor).trim();
|
|
}
|
|
_consumeLetDeclarationValue() {
|
|
const start = this._cursor.clone();
|
|
this._beginToken(30 /* TokenType.LET_VALUE */, start);
|
|
while (this._cursor.peek() !== $EOF) {
|
|
const char = this._cursor.peek();
|
|
// `@let` declarations terminate with a semicolon.
|
|
if (char === $SEMICOLON) {
|
|
break;
|
|
}
|
|
// If we hit a quote, skip over its content since we don't care what's inside.
|
|
if (isQuote(char)) {
|
|
this._cursor.advance();
|
|
this._attemptCharCodeUntilFn((inner) => {
|
|
if (inner === $BACKSLASH) {
|
|
this._cursor.advance();
|
|
return false;
|
|
}
|
|
return inner === char;
|
|
});
|
|
}
|
|
this._cursor.advance();
|
|
}
|
|
this._endToken([this._cursor.getChars(start)]);
|
|
}
|
|
/**
|
|
* @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 ParseError(this._cursor.getSpan(end), 'Programming error - attempted to end a token when there was no start to the token');
|
|
}
|
|
if (this._currentTokenType === null) {
|
|
throw new ParseError(this._cursor.getSpan(this._currentTokenStart), 'Programming error - attempted to end a token which has no token type');
|
|
}
|
|
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 ParseError(span, msg);
|
|
this._currentTokenStart = null;
|
|
this._currentTokenType = null;
|
|
return error;
|
|
}
|
|
handleError(e) {
|
|
if (e instanceof CursorError) {
|
|
e = this._createError(e.msg, this._cursor.getSpan(e.cursor));
|
|
}
|
|
if (e instanceof ParseError) {
|
|
this.errors.push(e);
|
|
}
|
|
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;
|
|
}
|
|
_peekStr(chars) {
|
|
const len = chars.length;
|
|
if (this._cursor.charsLeft() < len) {
|
|
return false;
|
|
}
|
|
const cursor = this._cursor.clone();
|
|
for (let i = 0; i < len; i++) {
|
|
if (cursor.peek() !== chars.charCodeAt(i)) {
|
|
return false;
|
|
}
|
|
cursor.advance();
|
|
}
|
|
return true;
|
|
}
|
|
_isBlockStart() {
|
|
return (this._cursor.peek() === $AT &&
|
|
SUPPORTED_BLOCKS.some((blockName) => this._peekStr(blockName)));
|
|
}
|
|
_isLetStart() {
|
|
return this._cursor.peek() === $AT && this._peekStr('@let');
|
|
}
|
|
_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.fromCodePoint(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.hasOwnProperty(name) && 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(endPredicate) {
|
|
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(endPredicate, prefix === '' ? 0 : 1);
|
|
const name = this._cursor.getChars(nameStart);
|
|
return [prefix, name];
|
|
}
|
|
_consumeTagOpen(start) {
|
|
let tagName;
|
|
let prefix;
|
|
let closingTagName;
|
|
let openToken;
|
|
try {
|
|
if (this._selectorlessEnabled && isSelectorlessNameStart(this._cursor.peek())) {
|
|
openToken = this._consumeComponentOpenStart(start);
|
|
[closingTagName, prefix, tagName] = openToken.parts;
|
|
if (prefix) {
|
|
closingTagName += `:${prefix}`;
|
|
}
|
|
if (tagName) {
|
|
closingTagName += `:${tagName}`;
|
|
}
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
}
|
|
else {
|
|
if (!isAsciiLetter(this._cursor.peek())) {
|
|
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start));
|
|
}
|
|
openToken = this._consumeTagOpenStart(start);
|
|
prefix = openToken.parts[0];
|
|
tagName = closingTagName = openToken.parts[1];
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
}
|
|
while (!isAttributeTerminator(this._cursor.peek())) {
|
|
if (this._selectorlessEnabled && this._cursor.peek() === $AT) {
|
|
const start = this._cursor.clone();
|
|
const nameStart = start.clone();
|
|
nameStart.advance();
|
|
if (isSelectorlessNameStart(nameStart.peek())) {
|
|
this._consumeDirective(start, nameStart);
|
|
}
|
|
}
|
|
else {
|
|
this._consumeAttribute();
|
|
}
|
|
}
|
|
if (openToken.type === 33 /* TokenType.COMPONENT_OPEN_START */) {
|
|
this._consumeComponentOpenEnd();
|
|
}
|
|
else {
|
|
this._consumeTagOpenEnd();
|
|
}
|
|
}
|
|
catch (e) {
|
|
if (e instanceof ParseError) {
|
|
if (openToken) {
|
|
// We errored before we could close the opening tag, so it is incomplete.
|
|
openToken.type =
|
|
openToken.type === 33 /* TokenType.COMPONENT_OPEN_START */
|
|
? 37 /* TokenType.INCOMPLETE_COMPONENT_OPEN */
|
|
: 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(openToken, closingTagName, false);
|
|
}
|
|
else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) {
|
|
this._consumeRawTextWithTagClose(openToken, closingTagName, true);
|
|
}
|
|
}
|
|
_consumeRawTextWithTagClose(openToken, 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(openToken.type === 33 /* TokenType.COMPONENT_OPEN_START */
|
|
? 36 /* TokenType.COMPONENT_CLOSE */
|
|
: 3 /* TokenType.TAG_CLOSE */);
|
|
this._requireCharCodeUntilFn((code) => code === $GT, 3);
|
|
this._cursor.advance(); // Consume the `>`
|
|
this._endToken(openToken.parts);
|
|
}
|
|
_consumeTagOpenStart(start) {
|
|
this._beginToken(0 /* TokenType.TAG_OPEN_START */, start);
|
|
const parts = this._consumePrefixAndName(isNameEnd);
|
|
return this._endToken(parts);
|
|
}
|
|
_consumeComponentOpenStart(start) {
|
|
this._beginToken(33 /* TokenType.COMPONENT_OPEN_START */, start);
|
|
const parts = this._consumeComponentName();
|
|
return this._endToken(parts);
|
|
}
|
|
_consumeComponentName() {
|
|
const nameStart = this._cursor.clone();
|
|
while (isSelectorlessNameChar(this._cursor.peek())) {
|
|
this._cursor.advance();
|
|
}
|
|
const name = this._cursor.getChars(nameStart);
|
|
let prefix = '';
|
|
let tagName = '';
|
|
if (this._cursor.peek() === $COLON) {
|
|
this._cursor.advance();
|
|
[prefix, tagName] = this._consumePrefixAndName(isNameEnd);
|
|
}
|
|
return [name, prefix, tagName];
|
|
}
|
|
_consumeAttribute() {
|
|
this._consumeAttributeName();
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
if (this._attemptCharCode($EQ)) {
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
this._consumeAttributeValue();
|
|
}
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
}
|
|
_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 */);
|
|
let nameEndPredicate;
|
|
if (this._openDirectiveCount > 0) {
|
|
// If we're parsing attributes inside of directive syntax, we have to terminate the name
|
|
// on the first non-matching closing paren. For example, if we have `@Dir(someAttr)`,
|
|
// `@Dir` and `(` will have already been captured as `DIRECTIVE_NAME` and `DIRECTIVE_OPEN`
|
|
// respectively, but the `)` will get captured as a part of the name for `someAttr`
|
|
// because normally that would be an event binding.
|
|
let openParens = 0;
|
|
nameEndPredicate = (code) => {
|
|
if (this._openDirectiveCount > 0) {
|
|
if (code === $LPAREN) {
|
|
openParens++;
|
|
}
|
|
else if (code === $RPAREN) {
|
|
if (openParens === 0) {
|
|
return true;
|
|
}
|
|
openParens--;
|
|
}
|
|
}
|
|
return isNameEnd(code);
|
|
};
|
|
}
|
|
else if (attrNameStart === $LBRACKET) {
|
|
let openBrackets = 0;
|
|
// Be more permissive for which characters are allowed inside square-bracketed attributes,
|
|
// because they usually end up being bound as attribute values. Some third-party packages
|
|
// like Tailwind take advantage of this.
|
|
nameEndPredicate = (code) => {
|
|
if (code === $LBRACKET) {
|
|
openBrackets++;
|
|
}
|
|
else if (code === $RBRACKET) {
|
|
openBrackets--;
|
|
}
|
|
// Only check for name-ending characters if the brackets are balanced or mismatched.
|
|
// Also interrupt the matching on new lines.
|
|
return openBrackets <= 0 ? isNameEnd(code) : isNewLine(code);
|
|
};
|
|
}
|
|
else {
|
|
nameEndPredicate = isNameEnd;
|
|
}
|
|
const prefixAndName = this._consumePrefixAndName(nameEndPredicate);
|
|
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([]);
|
|
}
|
|
_consumeComponentOpenEnd() {
|
|
const tokenType = this._attemptCharCode($SLASH)
|
|
? 35 /* TokenType.COMPONENT_OPEN_END_VOID */
|
|
: 34 /* TokenType.COMPONENT_OPEN_END */;
|
|
this._beginToken(tokenType);
|
|
this._requireCharCode($GT);
|
|
this._endToken([]);
|
|
}
|
|
_consumeTagClose(start) {
|
|
if (this._selectorlessEnabled) {
|
|
const clone = start.clone();
|
|
while (clone.peek() !== $GT && !isSelectorlessNameStart(clone.peek())) {
|
|
clone.advance();
|
|
}
|
|
if (isSelectorlessNameStart(clone.peek())) {
|
|
this._beginToken(36 /* TokenType.COMPONENT_CLOSE */, start);
|
|
const parts = this._consumeComponentName();
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
this._requireCharCode($GT);
|
|
this._endToken(parts);
|
|
return;
|
|
}
|
|
}
|
|
this._beginToken(3 /* TokenType.TAG_CLOSE */, start);
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
const prefixAndName = this._consumePrefixAndName(isNameEnd);
|
|
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);
|
|
}
|
|
_consumeDirective(start, nameStart) {
|
|
this._requireCharCode($AT);
|
|
// Skip over the @ since it's not part of the name.
|
|
this._cursor.advance();
|
|
// Capture the rest of the name.
|
|
while (isSelectorlessNameChar(this._cursor.peek())) {
|
|
this._cursor.advance();
|
|
}
|
|
// Capture the opening token.
|
|
this._beginToken(38 /* TokenType.DIRECTIVE_NAME */, start);
|
|
const name = this._cursor.getChars(nameStart);
|
|
this._endToken([name]);
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
// Optionally there might be attributes bound to the specific directive.
|
|
// Stop parsing if there's no opening character for them.
|
|
if (this._cursor.peek() !== $LPAREN) {
|
|
return;
|
|
}
|
|
this._openDirectiveCount++;
|
|
this._beginToken(39 /* TokenType.DIRECTIVE_OPEN */);
|
|
this._cursor.advance();
|
|
this._endToken([]);
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
// Capture all the attributes until we hit a closing paren.
|
|
while (!isAttributeTerminator(this._cursor.peek()) && this._cursor.peek() !== $RPAREN) {
|
|
this._consumeAttribute();
|
|
}
|
|
// Trim any trailing whitespace.
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
this._openDirectiveCount--;
|
|
if (this._cursor.peek() !== $RPAREN) {
|
|
// Stop parsing, instead of throwing, if we've hit the end of the tag.
|
|
// This can be handled better later when turning the tokens into AST.
|
|
if (this._cursor.peek() === $GT || this._cursor.peek() === $SLASH) {
|
|
return;
|
|
}
|
|
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start));
|
|
}
|
|
// Capture the closing token.
|
|
this._beginToken(40 /* TokenType.DIRECTIVE_CLOSE */);
|
|
this._cursor.advance();
|
|
this._endToken([]);
|
|
this._attemptCharCodeUntilFn(isNotWhitespace);
|
|
}
|
|
_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._isBlockStart() || this._isLetStart() || 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 isSelectorlessNameStart(code) {
|
|
return code === $_ || (code >= $A && code <= $Z);
|
|
}
|
|
function isSelectorlessNameChar(code) {
|
|
return isAsciiLetter(code) || isDigit(code) || code === $_;
|
|
}
|
|
function isAttributeTerminator(code) {
|
|
return code === $SLASH || code === $GT || code === $LT || code === $EOF;
|
|
}
|
|
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 {
|
|
state;
|
|
file;
|
|
input;
|
|
end;
|
|
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 {
|
|
internalState;
|
|
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 extends Error {
|
|
msg;
|
|
cursor;
|
|
constructor(msg, cursor) {
|
|
super(msg);
|
|
this.msg = msg;
|
|
this.cursor = cursor;
|
|
// Extending `Error` does not always work when code is transpiled. See:
|
|
// https://stackoverflow.com/questions/41102060/typescript-extending-error-class
|
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
}
|
|
}
|
|
|
|
class TreeError extends ParseError {
|
|
elementName;
|
|
static create(elementName, span, msg) {
|
|
return new TreeError(elementName, span, msg);
|
|
}
|
|
constructor(elementName, span, msg) {
|
|
super(span, msg);
|
|
this.elementName = elementName;
|
|
}
|
|
}
|
|
class ParseTreeResult {
|
|
rootNodes;
|
|
errors;
|
|
constructor(rootNodes, errors) {
|
|
this.rootNodes = rootNodes;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
let Parser$1 = class Parser {
|
|
getTagDefinition;
|
|
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, ...parser.errors]);
|
|
}
|
|
};
|
|
class _TreeBuilder {
|
|
tokens;
|
|
tagDefinitionResolver;
|
|
_index = -1;
|
|
// `_peek` will be initialized by the call to `_advance()` in the constructor.
|
|
_peek;
|
|
_containerStack = [];
|
|
rootNodes = [];
|
|
errors = [];
|
|
constructor(tokens, tagDefinitionResolver) {
|
|
this.tokens = tokens;
|
|
this.tagDefinitionResolver = tagDefinitionResolver;
|
|
this._advance();
|
|
}
|
|
build() {
|
|
while (this._peek.type !== 41 /* TokenType.EOF */) {
|
|
if (this._peek.type === 0 /* TokenType.TAG_OPEN_START */ ||
|
|
this._peek.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) {
|
|
this._consumeElementStartTag(this._advance());
|
|
}
|
|
else if (this._peek.type === 3 /* TokenType.TAG_CLOSE */) {
|
|
this._consumeElementEndTag(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 if (this._peek.type === 29 /* TokenType.LET_START */) {
|
|
this._closeVoidElement();
|
|
this._consumeLet(this._advance());
|
|
}
|
|
else if (this._peek.type === 32 /* TokenType.INCOMPLETE_LET */) {
|
|
this._closeVoidElement();
|
|
this._consumeIncompleteLet(this._advance());
|
|
}
|
|
else if (this._peek.type === 33 /* TokenType.COMPONENT_OPEN_START */ ||
|
|
this._peek.type === 37 /* TokenType.INCOMPLETE_COMPONENT_OPEN */) {
|
|
this._consumeComponentStartTag(this._advance());
|
|
}
|
|
else if (this._peek.type === 36 /* TokenType.COMPONENT_CLOSE */) {
|
|
this._consumeComponentEndTag(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: 41 /* TokenType.EOF */, parts: [], sourceSpan: end.sourceSpan });
|
|
// parse everything in between { and }
|
|
const expansionCaseParser = new _TreeBuilder(exp, this.tagDefinitionResolver);
|
|
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 === 41 /* 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)?.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 !== null && this._getTagDefinition(el)?.isVoid) {
|
|
this._containerStack.pop();
|
|
}
|
|
}
|
|
_consumeElementStartTag(startTagToken) {
|
|
const attrs = [];
|
|
const directives = [];
|
|
this._consumeAttributesAndDirectives(attrs, directives);
|
|
const fullName = this._getElementFullName(startTagToken, this._getClosestElementLikeParent());
|
|
const tagDef = this._getTagDefinition(fullName);
|
|
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;
|
|
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, directives, [], selfClosing, span, startSpan, undefined, tagDef?.isVoid ?? false);
|
|
const parent = this._getContainer();
|
|
const isClosedByChild = parent !== null && !!this._getTagDefinition(parent)?.isClosedByChild(el.name);
|
|
this._pushContainer(el, isClosedByChild);
|
|
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.`));
|
|
}
|
|
}
|
|
_consumeComponentStartTag(startToken) {
|
|
const componentName = startToken.parts[0];
|
|
const attrs = [];
|
|
const directives = [];
|
|
this._consumeAttributesAndDirectives(attrs, directives);
|
|
const closestElement = this._getClosestElementLikeParent();
|
|
const tagName = this._getComponentTagName(startToken, closestElement);
|
|
const fullName = this._getComponentFullName(startToken, closestElement);
|
|
const selfClosing = this._peek.type === 35 /* TokenType.COMPONENT_OPEN_END_VOID */;
|
|
this._advance();
|
|
const end = this._peek.sourceSpan.fullStart;
|
|
const span = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart);
|
|
const startSpan = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart);
|
|
const node = new Component(componentName, tagName, fullName, attrs, directives, [], selfClosing, span, startSpan, undefined);
|
|
const parent = this._getContainer();
|
|
const isClosedByChild = parent !== null &&
|
|
node.tagName !== null &&
|
|
!!this._getTagDefinition(parent)?.isClosedByChild(node.tagName);
|
|
this._pushContainer(node, isClosedByChild);
|
|
if (selfClosing) {
|
|
this._popContainer(fullName, Component, span);
|
|
}
|
|
else if (startToken.type === 37 /* TokenType.INCOMPLETE_COMPONENT_OPEN */) {
|
|
this._popContainer(fullName, Component, null);
|
|
this.errors.push(TreeError.create(fullName, span, `Opening tag "${fullName}" not terminated.`));
|
|
}
|
|
}
|
|
_consumeAttributesAndDirectives(attributesResult, directivesResult) {
|
|
while (this._peek.type === 14 /* TokenType.ATTR_NAME */ ||
|
|
this._peek.type === 38 /* TokenType.DIRECTIVE_NAME */) {
|
|
if (this._peek.type === 38 /* TokenType.DIRECTIVE_NAME */) {
|
|
directivesResult.push(this._consumeDirective(this._peek));
|
|
}
|
|
else {
|
|
attributesResult.push(this._consumeAttr(this._advance()));
|
|
}
|
|
}
|
|
}
|
|
_consumeComponentEndTag(endToken) {
|
|
const fullName = this._getComponentFullName(endToken, this._getClosestElementLikeParent());
|
|
if (!this._popContainer(fullName, Component, endToken.sourceSpan)) {
|
|
const container = this._containerStack[this._containerStack.length - 1];
|
|
let suffix;
|
|
if (container instanceof Component && container.componentName === endToken.parts[0]) {
|
|
suffix = `, did you mean "${container.fullName}"?`;
|
|
}
|
|
else {
|
|
suffix = '. It may happen when the tag has already been closed by another tag.';
|
|
}
|
|
const errMsg = `Unexpected closing tag "${fullName}"${suffix}`;
|
|
this.errors.push(TreeError.create(fullName, endToken.sourceSpan, errMsg));
|
|
}
|
|
}
|
|
_getTagDefinition(nodeOrName) {
|
|
if (typeof nodeOrName === 'string') {
|
|
return this.tagDefinitionResolver(nodeOrName);
|
|
}
|
|
else if (nodeOrName instanceof Element) {
|
|
return this.tagDefinitionResolver(nodeOrName.name);
|
|
}
|
|
else if (nodeOrName instanceof Component && nodeOrName.tagName !== null) {
|
|
return this.tagDefinitionResolver(nodeOrName.tagName);
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
_pushContainer(node, isClosedByChild) {
|
|
if (isClosedByChild) {
|
|
this._containerStack.pop();
|
|
}
|
|
this._addToParent(node);
|
|
this._containerStack.push(node);
|
|
}
|
|
_consumeElementEndTag(endTagToken) {
|
|
const fullName = this._getElementFullName(endTagToken, this._getClosestElementLikeParent());
|
|
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];
|
|
const nodeName = node instanceof Component ? node.fullName : node.name;
|
|
if ((nodeName === 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 || !this._getTagDefinition(node)?.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);
|
|
}
|
|
_consumeDirective(nameToken) {
|
|
const attributes = [];
|
|
let startSourceSpanEnd = nameToken.sourceSpan.end;
|
|
let endSourceSpan = null;
|
|
this._advance();
|
|
if (this._peek.type === 39 /* TokenType.DIRECTIVE_OPEN */) {
|
|
// Capture the opening token in the start span.
|
|
startSourceSpanEnd = this._peek.sourceSpan.end;
|
|
this._advance();
|
|
// Cast here is necessary, because TS doesn't know that `_advance` changed `_peek`.
|
|
while (this._peek.type === 14 /* TokenType.ATTR_NAME */) {
|
|
attributes.push(this._consumeAttr(this._advance()));
|
|
}
|
|
if (this._peek.type === 40 /* TokenType.DIRECTIVE_CLOSE */) {
|
|
endSourceSpan = this._peek.sourceSpan;
|
|
this._advance();
|
|
}
|
|
else {
|
|
this.errors.push(TreeError.create(null, nameToken.sourceSpan, 'Unterminated directive definition'));
|
|
}
|
|
}
|
|
const startSourceSpan = new ParseSourceSpan(nameToken.sourceSpan.start, startSourceSpanEnd, nameToken.sourceSpan.fullStart);
|
|
const sourceSpan = new ParseSourceSpan(startSourceSpan.start, endSourceSpan === null ? nameToken.sourceSpan.end : endSourceSpan.end, startSourceSpan.fullStart);
|
|
return new Directive(nameToken.parts[0], attributes, sourceSpan, startSourceSpan, endSourceSpan);
|
|
}
|
|
_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.`));
|
|
}
|
|
_consumeLet(startToken) {
|
|
const name = startToken.parts[0];
|
|
let valueToken;
|
|
let endToken;
|
|
if (this._peek.type !== 30 /* TokenType.LET_VALUE */) {
|
|
this.errors.push(TreeError.create(startToken.parts[0], startToken.sourceSpan, `Invalid @let declaration "${name}". Declaration must have a value.`));
|
|
return;
|
|
}
|
|
else {
|
|
valueToken = this._advance();
|
|
}
|
|
// Type cast is necessary here since TS narrowed the type of `peek` above.
|
|
if (this._peek.type !== 31 /* TokenType.LET_END */) {
|
|
this.errors.push(TreeError.create(startToken.parts[0], startToken.sourceSpan, `Unterminated @let declaration "${name}". Declaration must be terminated with a semicolon.`));
|
|
return;
|
|
}
|
|
else {
|
|
endToken = this._advance();
|
|
}
|
|
const end = endToken.sourceSpan.fullStart;
|
|
const span = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart);
|
|
// The start token usually captures the `@let`. Construct a name span by
|
|
// offsetting the start by the length of any text before the name.
|
|
const startOffset = startToken.sourceSpan.toString().lastIndexOf(name);
|
|
const nameStart = startToken.sourceSpan.start.moveBy(startOffset);
|
|
const nameSpan = new ParseSourceSpan(nameStart, startToken.sourceSpan.end);
|
|
const node = new LetDeclaration(name, valueToken.parts[0], span, nameSpan, valueToken.sourceSpan);
|
|
this._addToParent(node);
|
|
}
|
|
_consumeIncompleteLet(token) {
|
|
// Incomplete `@let` declaration may end up with an empty name.
|
|
const name = token.parts[0] ?? '';
|
|
const nameString = name ? ` "${name}"` : '';
|
|
// If there's at least a name, we can salvage an AST node that can be used for completions.
|
|
if (name.length > 0) {
|
|
const startOffset = token.sourceSpan.toString().lastIndexOf(name);
|
|
const nameStart = token.sourceSpan.start.moveBy(startOffset);
|
|
const nameSpan = new ParseSourceSpan(nameStart, token.sourceSpan.end);
|
|
const valueSpan = new ParseSourceSpan(token.sourceSpan.start, token.sourceSpan.start.moveBy(0));
|
|
const node = new LetDeclaration(name, '', token.sourceSpan, nameSpan, valueSpan);
|
|
this._addToParent(node);
|
|
}
|
|
this.errors.push(TreeError.create(token.parts[0], token.sourceSpan, `Incomplete @let declaration${nameString}. ` +
|
|
`@let declarations must be written as \`@let <name> = <value>;\``));
|
|
}
|
|
_getContainer() {
|
|
return this._containerStack.length > 0
|
|
? this._containerStack[this._containerStack.length - 1]
|
|
: null;
|
|
}
|
|
_getClosestElementLikeParent() {
|
|
for (let i = this._containerStack.length - 1; i > -1; i--) {
|
|
const current = this._containerStack[i];
|
|
if (current instanceof Element || current instanceof Component) {
|
|
return current;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
_addToParent(node) {
|
|
const parent = this._getContainer();
|
|
if (parent === null) {
|
|
this.rootNodes.push(node);
|
|
}
|
|
else {
|
|
parent.children.push(node);
|
|
}
|
|
}
|
|
_getElementFullName(token, parent) {
|
|
const prefix = this._getPrefix(token, parent);
|
|
return mergeNsAndName(prefix, token.parts[1]);
|
|
}
|
|
_getComponentFullName(token, parent) {
|
|
const componentName = token.parts[0];
|
|
const tagName = this._getComponentTagName(token, parent);
|
|
if (tagName === null) {
|
|
return componentName;
|
|
}
|
|
return tagName.startsWith(':') ? componentName + tagName : `${componentName}:${tagName}`;
|
|
}
|
|
_getComponentTagName(token, parent) {
|
|
const prefix = this._getPrefix(token, parent);
|
|
const tagName = token.parts[2];
|
|
if (!prefix && !tagName) {
|
|
return null;
|
|
}
|
|
else if (!prefix && tagName) {
|
|
return tagName;
|
|
}
|
|
else {
|
|
// TODO(crisbeto): re-evaluate this fallback. Maybe base it off the class name?
|
|
return mergeNsAndName(prefix, tagName || 'ng-component');
|
|
}
|
|
}
|
|
_getPrefix(token, parent) {
|
|
let prefix;
|
|
let tagName;
|
|
if (token.type === 33 /* TokenType.COMPONENT_OPEN_START */ ||
|
|
token.type === 37 /* TokenType.INCOMPLETE_COMPONENT_OPEN */ ||
|
|
token.type === 36 /* TokenType.COMPONENT_CLOSE */) {
|
|
prefix = token.parts[1];
|
|
tagName = token.parts[2];
|
|
}
|
|
else {
|
|
prefix = token.parts[0];
|
|
tagName = token.parts[1];
|
|
}
|
|
prefix = prefix || this._getTagDefinition(tagName)?.implicitNamespacePrefix || '';
|
|
if (!prefix && parent) {
|
|
const parentName = parent instanceof Element ? parent.name : parent.tagName;
|
|
if (parentName !== null) {
|
|
const parentTagName = splitNsName(parentName)[1];
|
|
const parentTagDefinition = this._getTagDefinition(parentTagName);
|
|
if (parentTagDefinition !== null && !parentTagDefinition.preventNamespaceInheritance) {
|
|
prefix = getNsPrefix(parentName);
|
|
}
|
|
}
|
|
}
|
|
return prefix;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces';
|
|
const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']);
|
|
// Equivalent to \s with \u00a0 (non-breaking space) excluded.
|
|
// Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
|
|
const WS_CHARS = ' \f\n\r\t\v\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff';
|
|
const NO_WS_REGEXP = new RegExp(`[^${WS_CHARS}]`);
|
|
const WS_REPLACE_REGEXP = new RegExp(`[${WS_CHARS}]{2,}`, 'g');
|
|
function hasPreserveWhitespacesAttr(attrs) {
|
|
return attrs.some((attr) => attr.name === PRESERVE_WS_ATTR_NAME);
|
|
}
|
|
/**
|
|
* &ngsp; is a placeholder for non-removable space
|
|
* &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
|
|
* and later on replaced by a space.
|
|
*/
|
|
function replaceNgsp(value) {
|
|
// lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE
|
|
return value.replace(new RegExp(NGSP_UNICODE, 'g'), ' ');
|
|
}
|
|
/**
|
|
* This visitor can walk HTML parse tree and remove / trim text nodes using the following rules:
|
|
* - consider spaces, tabs and new lines as whitespace characters;
|
|
* - drop text nodes consisting of whitespace characters only;
|
|
* - for all other text nodes replace consecutive whitespace characters with one space;
|
|
* - convert &ngsp; pseudo-entity to a single space;
|
|
*
|
|
* Removal and trimming of whitespaces have positive performance impact (less code to generate
|
|
* while compiling templates, faster view creation). At the same time it can be "destructive"
|
|
* in some cases (whitespaces can influence layout). Because of the potential of breaking layout
|
|
* this visitor is not activated by default in Angular 5 and people need to explicitly opt-in for
|
|
* whitespace removal. The default option for whitespace removal will be revisited in Angular 6
|
|
* and might be changed to "on" by default.
|
|
*
|
|
* If `originalNodeMap` is provided, the transformed nodes will be mapped back to their original
|
|
* inputs. Any output nodes not in the map were not transformed. This supports correlating and
|
|
* porting information between the trimmed nodes and original nodes (such as `i18n` properties)
|
|
* such that trimming whitespace does not does not drop required information from the node.
|
|
*/
|
|
class WhitespaceVisitor {
|
|
preserveSignificantWhitespace;
|
|
originalNodeMap;
|
|
requireContext;
|
|
// How many ICU expansions which are currently being visited. ICUs can be nested, so this
|
|
// tracks the current depth of nesting. If this depth is greater than 0, then this visitor is
|
|
// currently processing content inside an ICU expansion.
|
|
icuExpansionDepth = 0;
|
|
constructor(preserveSignificantWhitespace, originalNodeMap, requireContext = true) {
|
|
this.preserveSignificantWhitespace = preserveSignificantWhitespace;
|
|
this.originalNodeMap = originalNodeMap;
|
|
this.requireContext = requireContext;
|
|
}
|
|
visitElement(element, context) {
|
|
if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) {
|
|
// don't descent into elements where we need to preserve whitespaces
|
|
// but still visit all attributes to eliminate one used as a market to preserve WS
|
|
const newElement = new Element(element.name, visitAllWithSiblings(this, element.attrs), visitAllWithSiblings(this, element.directives), element.children, element.isSelfClosing, element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.isVoid, element.i18n);
|
|
this.originalNodeMap?.set(newElement, element);
|
|
return newElement;
|
|
}
|
|
const newElement = new Element(element.name, element.attrs, element.directives, visitAllWithSiblings(this, element.children), element.isSelfClosing, element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.isVoid, element.i18n);
|
|
this.originalNodeMap?.set(newElement, element);
|
|
return newElement;
|
|
}
|
|
visitAttribute(attribute, context) {
|
|
return attribute.name !== PRESERVE_WS_ATTR_NAME ? attribute : null;
|
|
}
|
|
visitText(text, context) {
|
|
const isNotBlank = text.value.match(NO_WS_REGEXP);
|
|
const hasExpansionSibling = context && (context.prev instanceof Expansion || context.next instanceof Expansion);
|
|
// Do not trim whitespace within ICU expansions when preserving significant whitespace.
|
|
// Historically, ICU whitespace was never trimmed and this is really a bug. However fixing it
|
|
// would change message IDs which we can't easily do. Instead we only trim ICU whitespace within
|
|
// ICU expansions when not preserving significant whitespace, which is the new behavior where it
|
|
// most matters.
|
|
const inIcuExpansion = this.icuExpansionDepth > 0;
|
|
if (inIcuExpansion && this.preserveSignificantWhitespace)
|
|
return text;
|
|
if (isNotBlank || hasExpansionSibling) {
|
|
// Process the whitespace in the tokens of this Text node
|
|
const tokens = text.tokens.map((token) => token.type === 5 /* TokenType.TEXT */ ? createWhitespaceProcessedTextToken(token) : token);
|
|
// Fully trim message when significant whitespace is not preserved.
|
|
if (!this.preserveSignificantWhitespace && tokens.length > 0) {
|
|
// The first token should only call `.trimStart()` and the last token
|
|
// should only call `.trimEnd()`, but there might be only one token which
|
|
// needs to call both.
|
|
const firstToken = tokens[0];
|
|
tokens.splice(0, 1, trimLeadingWhitespace(firstToken, context));
|
|
const lastToken = tokens[tokens.length - 1]; // Could be the same as the first token.
|
|
tokens.splice(tokens.length - 1, 1, trimTrailingWhitespace(lastToken, context));
|
|
}
|
|
// Process the whitespace of the value of this Text node. Also trim the leading/trailing
|
|
// whitespace when we don't need to preserve significant whitespace.
|
|
const processed = processWhitespace(text.value);
|
|
const value = this.preserveSignificantWhitespace
|
|
? processed
|
|
: trimLeadingAndTrailingWhitespace(processed, context);
|
|
const result = new Text(value, text.sourceSpan, tokens, text.i18n);
|
|
this.originalNodeMap?.set(result, text);
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
visitComment(comment, context) {
|
|
return comment;
|
|
}
|
|
visitExpansion(expansion, context) {
|
|
this.icuExpansionDepth++;
|
|
let newExpansion;
|
|
try {
|
|
newExpansion = new Expansion(expansion.switchValue, expansion.type, visitAllWithSiblings(this, expansion.cases), expansion.sourceSpan, expansion.switchValueSourceSpan, expansion.i18n);
|
|
}
|
|
finally {
|
|
this.icuExpansionDepth--;
|
|
}
|
|
this.originalNodeMap?.set(newExpansion, expansion);
|
|
return newExpansion;
|
|
}
|
|
visitExpansionCase(expansionCase, context) {
|
|
const newExpansionCase = new ExpansionCase(expansionCase.value, visitAllWithSiblings(this, expansionCase.expression), expansionCase.sourceSpan, expansionCase.valueSourceSpan, expansionCase.expSourceSpan);
|
|
this.originalNodeMap?.set(newExpansionCase, expansionCase);
|
|
return newExpansionCase;
|
|
}
|
|
visitBlock(block, context) {
|
|
const newBlock = new Block(block.name, block.parameters, visitAllWithSiblings(this, block.children), block.sourceSpan, block.nameSpan, block.startSourceSpan, block.endSourceSpan);
|
|
this.originalNodeMap?.set(newBlock, block);
|
|
return newBlock;
|
|
}
|
|
visitBlockParameter(parameter, context) {
|
|
return parameter;
|
|
}
|
|
visitLetDeclaration(decl, context) {
|
|
return decl;
|
|
}
|
|
visitComponent(node, context) {
|
|
if ((node.tagName && SKIP_WS_TRIM_TAGS.has(node.tagName)) ||
|
|
hasPreserveWhitespacesAttr(node.attrs)) {
|
|
// don't descent into elements where we need to preserve whitespaces
|
|
// but still visit all attributes to eliminate one used as a market to preserve WS
|
|
const newElement = new Component(node.componentName, node.tagName, node.fullName, visitAllWithSiblings(this, node.attrs), visitAllWithSiblings(this, node.directives), node.children, node.isSelfClosing, node.sourceSpan, node.startSourceSpan, node.endSourceSpan, node.i18n);
|
|
this.originalNodeMap?.set(newElement, node);
|
|
return newElement;
|
|
}
|
|
const newElement = new Component(node.componentName, node.tagName, node.fullName, node.attrs, node.directives, visitAllWithSiblings(this, node.children), node.isSelfClosing, node.sourceSpan, node.startSourceSpan, node.endSourceSpan, node.i18n);
|
|
this.originalNodeMap?.set(newElement, node);
|
|
return newElement;
|
|
}
|
|
visitDirective(directive, context) {
|
|
return directive;
|
|
}
|
|
visit(_node, context) {
|
|
// `visitAllWithSiblings` provides context necessary for ICU messages to be handled correctly.
|
|
// Prefer that over calling `html.visitAll` directly on this visitor.
|
|
if (this.requireContext && !context) {
|
|
throw new Error(`WhitespaceVisitor requires context. Visit via \`visitAllWithSiblings\` to get this context.`);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
function trimLeadingWhitespace(token, context) {
|
|
if (token.type !== 5 /* TokenType.TEXT */)
|
|
return token;
|
|
const isFirstTokenInTag = !context?.prev;
|
|
if (!isFirstTokenInTag)
|
|
return token;
|
|
return transformTextToken(token, (text) => text.trimStart());
|
|
}
|
|
function trimTrailingWhitespace(token, context) {
|
|
if (token.type !== 5 /* TokenType.TEXT */)
|
|
return token;
|
|
const isLastTokenInTag = !context?.next;
|
|
if (!isLastTokenInTag)
|
|
return token;
|
|
return transformTextToken(token, (text) => text.trimEnd());
|
|
}
|
|
function trimLeadingAndTrailingWhitespace(text, context) {
|
|
const isFirstTokenInTag = !context?.prev;
|
|
const isLastTokenInTag = !context?.next;
|
|
const maybeTrimmedStart = isFirstTokenInTag ? text.trimStart() : text;
|
|
const maybeTrimmed = isLastTokenInTag ? maybeTrimmedStart.trimEnd() : maybeTrimmedStart;
|
|
return maybeTrimmed;
|
|
}
|
|
function createWhitespaceProcessedTextToken({ type, parts, sourceSpan }) {
|
|
return { type, parts: [processWhitespace(parts[0])], sourceSpan };
|
|
}
|
|
function transformTextToken({ type, parts, sourceSpan }, transform) {
|
|
// `TextToken` only ever has one part as defined in its type, so we just transform the first element.
|
|
return { type, parts: [transform(parts[0])], sourceSpan };
|
|
}
|
|
function processWhitespace(text) {
|
|
return replaceNgsp(text).replace(WS_REPLACE_REGEXP, ' ');
|
|
}
|
|
function visitAllWithSiblings(visitor, nodes) {
|
|
const result = [];
|
|
nodes.forEach((ast, i) => {
|
|
const context = { prev: nodes[i - 1], next: nodes[i + 1] };
|
|
const astResult = ast.visit(visitor, context);
|
|
if (astResult) {
|
|
result.push(astResult);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
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 = {}));
|
|
var StringTokenKind;
|
|
(function (StringTokenKind) {
|
|
StringTokenKind[StringTokenKind["Plain"] = 0] = "Plain";
|
|
StringTokenKind[StringTokenKind["TemplateLiteralPart"] = 1] = "TemplateLiteralPart";
|
|
StringTokenKind[StringTokenKind["TemplateLiteralEnd"] = 2] = "TemplateLiteralEnd";
|
|
})(StringTokenKind || (StringTokenKind = {}));
|
|
const KEYWORDS = [
|
|
'var',
|
|
'let',
|
|
'as',
|
|
'null',
|
|
'undefined',
|
|
'true',
|
|
'false',
|
|
'if',
|
|
'else',
|
|
'this',
|
|
'typeof',
|
|
'void',
|
|
'in',
|
|
];
|
|
class Lexer {
|
|
tokenize(text) {
|
|
return new _Scanner(text).scan();
|
|
}
|
|
}
|
|
class Token {
|
|
index;
|
|
end;
|
|
type;
|
|
numValue;
|
|
strValue;
|
|
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';
|
|
}
|
|
isKeywordTypeof() {
|
|
return this.type === TokenType.Keyword && this.strValue === 'typeof';
|
|
}
|
|
isKeywordVoid() {
|
|
return this.type === TokenType.Keyword && this.strValue === 'void';
|
|
}
|
|
isKeywordIn() {
|
|
return this.type === TokenType.Keyword && this.strValue === 'in';
|
|
}
|
|
isError() {
|
|
return this.type === TokenType.Error;
|
|
}
|
|
toNumber() {
|
|
return this.type === TokenType.Number ? this.numValue : -1;
|
|
}
|
|
isTemplateLiteralPart() {
|
|
// Note: Explicit type is needed for Closure.
|
|
return this.isString() && this.kind === StringTokenKind.TemplateLiteralPart;
|
|
}
|
|
isTemplateLiteralEnd() {
|
|
// Note: Explicit type is needed for Closure.
|
|
return this.isString() && this.kind === StringTokenKind.TemplateLiteralEnd;
|
|
}
|
|
isTemplateLiteralInterpolationStart() {
|
|
return this.isOperator('${');
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
class StringToken extends Token {
|
|
kind;
|
|
constructor(index, end, strValue, kind) {
|
|
super(index, end, TokenType.String, 0, strValue);
|
|
this.kind = kind;
|
|
}
|
|
}
|
|
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 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 {
|
|
input;
|
|
tokens = [];
|
|
length;
|
|
peek = 0;
|
|
index = -1;
|
|
braceStack = [];
|
|
constructor(input) {
|
|
this.input = input;
|
|
this.length = input.length;
|
|
this.advance();
|
|
}
|
|
scan() {
|
|
let token = this.scanToken();
|
|
while (token !== null) {
|
|
this.tokens.push(token);
|
|
token = this.scanToken();
|
|
}
|
|
return this.tokens;
|
|
}
|
|
advance() {
|
|
this.peek = ++this.index >= this.length ? $EOF : this.input.charCodeAt(this.index);
|
|
}
|
|
scanToken() {
|
|
const input = this.input;
|
|
const length = this.length;
|
|
let peek = this.peek;
|
|
let 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 $LBRACKET:
|
|
case $RBRACKET:
|
|
case $COMMA:
|
|
case $COLON:
|
|
case $SEMICOLON:
|
|
return this.scanCharacter(start, peek);
|
|
case $LBRACE:
|
|
return this.scanOpenBrace(start, peek);
|
|
case $RBRACE:
|
|
return this.scanCloseBrace(start, peek);
|
|
case $SQ:
|
|
case $DQ:
|
|
return this.scanString();
|
|
case $BT:
|
|
this.advance();
|
|
return this.scanTemplateLiteralPart(start);
|
|
case $HASH:
|
|
return this.scanPrivateIdentifier();
|
|
case $PLUS:
|
|
return this.scanComplexOperator(start, '+', $EQ, '=');
|
|
case $MINUS:
|
|
return this.scanComplexOperator(start, '-', $EQ, '=');
|
|
case $SLASH:
|
|
return this.scanComplexOperator(start, '/', $EQ, '=');
|
|
case $PERCENT:
|
|
return this.scanComplexOperator(start, '%', $EQ, '=');
|
|
case $CARET:
|
|
return this.scanOperator(start, '^');
|
|
case $STAR:
|
|
return this.scanStar(start);
|
|
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, '&', $EQ, '=');
|
|
case $BAR:
|
|
return this.scanComplexOperator(start, '|', $BAR, '|', $EQ, '=');
|
|
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);
|
|
}
|
|
scanOpenBrace(start, code) {
|
|
this.braceStack.push('expression');
|
|
this.advance();
|
|
return newCharacterToken(start, this.index, code);
|
|
}
|
|
scanCloseBrace(start, code) {
|
|
this.advance();
|
|
const currentBrace = this.braceStack.pop();
|
|
if (currentBrace === 'interpolation') {
|
|
this.tokens.push(newCharacterToken(start, this.index, $RBRACE));
|
|
return this.scanTemplateLiteralPart(this.index);
|
|
}
|
|
return newCharacterToken(start, this.index, code);
|
|
}
|
|
/**
|
|
* 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)) ;
|
|
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) {
|
|
const result = this.scanStringBackslash(buffer, marker);
|
|
if (typeof result !== 'string') {
|
|
return result; // Error
|
|
}
|
|
buffer = result;
|
|
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 new StringToken(start, this.index, buffer + last, StringTokenKind.Plain);
|
|
}
|
|
scanQuestion(start) {
|
|
this.advance();
|
|
let operator = '?';
|
|
// `a ?? b` or `a ??= b`.
|
|
if (this.peek === $QUESTION) {
|
|
operator += '?';
|
|
this.advance();
|
|
// @ts-expect-error
|
|
if (this.peek === $EQ) {
|
|
operator += '=';
|
|
this.advance();
|
|
}
|
|
}
|
|
else if (this.peek === $PERIOD) {
|
|
// `a?.b`
|
|
operator += '.';
|
|
this.advance();
|
|
}
|
|
return newOperatorToken(start, this.index, operator);
|
|
}
|
|
scanTemplateLiteralPart(start) {
|
|
let buffer = '';
|
|
let marker = this.index;
|
|
while (this.peek !== $BT) {
|
|
if (this.peek === $BACKSLASH) {
|
|
const result = this.scanStringBackslash(buffer, marker);
|
|
if (typeof result !== 'string') {
|
|
return result; // Error
|
|
}
|
|
buffer = result;
|
|
marker = this.index;
|
|
}
|
|
else if (this.peek === $$) {
|
|
const dollar = this.index;
|
|
this.advance();
|
|
// @ts-expect-error
|
|
if (this.peek === $LBRACE) {
|
|
this.braceStack.push('interpolation');
|
|
this.tokens.push(new StringToken(start, dollar, buffer + this.input.substring(marker, dollar), StringTokenKind.TemplateLiteralPart));
|
|
this.advance();
|
|
return newOperatorToken(dollar, this.index, this.input.substring(dollar, this.index));
|
|
}
|
|
}
|
|
else if (this.peek === $EOF) {
|
|
return this.error('Unterminated template literal', 0);
|
|
}
|
|
else {
|
|
this.advance();
|
|
}
|
|
}
|
|
const last = this.input.substring(marker, this.index);
|
|
this.advance();
|
|
return new StringToken(start, this.index, buffer + last, StringTokenKind.TemplateLiteralEnd);
|
|
}
|
|
error(message, offset) {
|
|
const position = this.index + offset;
|
|
return newErrorToken(position, this.index, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
|
|
}
|
|
scanStringBackslash(buffer, marker) {
|
|
buffer += this.input.substring(marker, this.index);
|
|
let unescapedCode;
|
|
this.advance();
|
|
if (this.peek === $u) {
|
|
// 4 character hex code for unicode character.
|
|
const hex = this.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);
|
|
return buffer;
|
|
}
|
|
scanStar(start) {
|
|
this.advance();
|
|
// `*`, `**`, `**=` or `*=`
|
|
let operator = '*';
|
|
if (this.peek === $STAR) {
|
|
operator += '*';
|
|
this.advance();
|
|
// @ts-expect-error
|
|
if (this.peek === $EQ) {
|
|
operator += '=';
|
|
this.advance();
|
|
}
|
|
}
|
|
else if (this.peek === $EQ) {
|
|
operator += '=';
|
|
this.advance();
|
|
}
|
|
return newOperatorToken(start, this.index, operator);
|
|
}
|
|
}
|
|
function isIdentifierStart(code) {
|
|
return (($a <= code && code <= $z) ||
|
|
($A <= code && code <= $Z) ||
|
|
code == $_ ||
|
|
code == $$);
|
|
}
|
|
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 {
|
|
strings;
|
|
expressions;
|
|
offsets;
|
|
constructor(strings, expressions, offsets) {
|
|
this.strings = strings;
|
|
this.expressions = expressions;
|
|
this.offsets = offsets;
|
|
}
|
|
}
|
|
class TemplateBindingParseResult {
|
|
templateBindings;
|
|
warnings;
|
|
errors;
|
|
constructor(templateBindings, warnings, errors) {
|
|
this.templateBindings = templateBindings;
|
|
this.warnings = warnings;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
function getLocation(span) {
|
|
return span.start.toString() || '(unknown)';
|
|
}
|
|
class Parser {
|
|
_lexer;
|
|
_supportsDirectPipeReferences;
|
|
constructor(_lexer, _supportsDirectPipeReferences = false) {
|
|
this._lexer = _lexer;
|
|
this._supportsDirectPipeReferences = _supportsDirectPipeReferences;
|
|
}
|
|
parseAction(input, parseSourceSpan, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
|
const errors = [];
|
|
this._checkNoInterpolation(errors, input, parseSourceSpan, interpolationConfig);
|
|
const { stripped: sourceToLex } = this._stripComments(input);
|
|
const tokens = this._lexer.tokenize(sourceToLex);
|
|
const ast = new _ParseAST(input, parseSourceSpan, absoluteOffset, tokens, 1 /* ParseFlags.Action */, errors, 0, this._supportsDirectPipeReferences).parseChain();
|
|
return new ASTWithSource(ast, input, getLocation(parseSourceSpan), absoluteOffset, errors);
|
|
}
|
|
parseBinding(input, parseSourceSpan, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
|
const errors = [];
|
|
const ast = this._parseBindingAst(input, parseSourceSpan, absoluteOffset, interpolationConfig, errors);
|
|
return new ASTWithSource(ast, input, getLocation(parseSourceSpan), absoluteOffset, errors);
|
|
}
|
|
checkSimpleExpression(ast) {
|
|
const checker = new SimpleExpressionChecker();
|
|
ast.visit(checker);
|
|
return checker.errors;
|
|
}
|
|
// Host bindings parsed here
|
|
parseSimpleBinding(input, parseSourceSpan, absoluteOffset, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
|
const errors = [];
|
|
const ast = this._parseBindingAst(input, parseSourceSpan, absoluteOffset, interpolationConfig, errors);
|
|
const simplExpressionErrors = this.checkSimpleExpression(ast);
|
|
if (simplExpressionErrors.length > 0) {
|
|
errors.push(getParseError(`Host binding expression cannot contain ${simplExpressionErrors.join(' ')}`, input, '', parseSourceSpan));
|
|
}
|
|
return new ASTWithSource(ast, input, getLocation(parseSourceSpan), absoluteOffset, errors);
|
|
}
|
|
_parseBindingAst(input, parseSourceSpan, absoluteOffset, interpolationConfig, errors) {
|
|
this._checkNoInterpolation(errors, input, parseSourceSpan, interpolationConfig);
|
|
const { stripped: sourceToLex } = this._stripComments(input);
|
|
const tokens = this._lexer.tokenize(sourceToLex);
|
|
return new _ParseAST(input, parseSourceSpan, absoluteOffset, tokens, 0 /* ParseFlags.None */, errors, 0, this._supportsDirectPipeReferences).parseChain();
|
|
}
|
|
/**
|
|
* Parse microsyntax template expression and return a list of bindings or
|
|
* parsing errors in case the given expression is invalid.
|
|
*
|
|
* For example,
|
|
* ```html
|
|
* <div *ngFor="let item of items">
|
|
* ^ ^ 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:
|
|
* ```html
|
|
* <ng-template ngFor let-item [ngForOf]="items">
|
|
* ```
|
|
*
|
|
* @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, parseSourceSpan, absoluteKeyOffset, absoluteValueOffset) {
|
|
const tokens = this._lexer.tokenize(templateValue);
|
|
const errors = [];
|
|
const parser = new _ParseAST(templateValue, parseSourceSpan, absoluteValueOffset, tokens, 0 /* ParseFlags.None */, errors, 0 /* relative offset */, this._supportsDirectPipeReferences);
|
|
return parser.parseTemplateBindings({
|
|
source: templateKey,
|
|
span: new AbsoluteSourceSpan(absoluteKeyOffset, absoluteKeyOffset + templateKey.length),
|
|
});
|
|
}
|
|
parseInterpolation(input, parseSourceSpan, absoluteOffset, interpolatedTokens, interpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
|
const errors = [];
|
|
const { strings, expressions, offsets } = this.splitInterpolation(input, parseSourceSpan, errors, interpolatedTokens, interpolationConfig);
|
|
if (expressions.length === 0)
|
|
return null;
|
|
const expressionNodes = [];
|
|
for (let i = 0; i < expressions.length; ++i) {
|
|
// If we have a token for the specific expression, it's preferrable to use it because it
|
|
// allows us to produce more accurate error messages. The expressions are always at the odd
|
|
// indexes inside the tokens.
|
|
const expressionSpan = interpolatedTokens?.[i * 2 + 1]?.sourceSpan;
|
|
const expressionText = expressions[i].text;
|
|
const { stripped: sourceToLex, hasComments } = this._stripComments(expressionText);
|
|
const tokens = this._lexer.tokenize(sourceToLex);
|
|
if (hasComments && sourceToLex.trim().length === 0 && tokens.length === 0) {
|
|
// Empty expressions error are handled futher down, here we only take care of the comment case
|
|
errors.push(getParseError('Interpolation expression cannot only contain a comment', input, `at column ${expressions[i].start} in`, parseSourceSpan));
|
|
continue;
|
|
}
|
|
const ast = new _ParseAST(expressionSpan ? expressionText : input, expressionSpan || parseSourceSpan, absoluteOffset, tokens, 0 /* ParseFlags.None */, errors, offsets[i], this._supportsDirectPipeReferences).parseChain();
|
|
expressionNodes.push(ast);
|
|
}
|
|
return this.createInterpolationAst(strings.map((s) => s.text), expressionNodes, input, getLocation(parseSourceSpan), absoluteOffset, errors);
|
|
}
|
|
/**
|
|
* 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, parseSourceSpan, absoluteOffset) {
|
|
const { stripped: sourceToLex } = this._stripComments(expression);
|
|
const tokens = this._lexer.tokenize(sourceToLex);
|
|
const errors = [];
|
|
const ast = new _ParseAST(expression, parseSourceSpan, absoluteOffset, tokens, 0 /* ParseFlags.None */, errors, 0, this._supportsDirectPipeReferences).parseChain();
|
|
const strings = ['', '']; // The prefix and suffix strings are both empty
|
|
return this.createInterpolationAst(strings, [ast], expression, getLocation(parseSourceSpan), absoluteOffset, errors);
|
|
}
|
|
createInterpolationAst(strings, expressions, input, location, absoluteOffset, errors) {
|
|
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, 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
|
|
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
|
|
*/
|
|
splitInterpolation(input, parseSourceSpan, errors, 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) {
|
|
errors.push(getParseError('Blank expressions are not allowed in interpolated strings', input, `at column ${i} in`, parseSourceSpan));
|
|
}
|
|
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, sourceSpanOrLocation, absoluteOffset) {
|
|
const span = new ParseSpan(0, input == null ? 0 : input.length);
|
|
return new ASTWithSource(new LiteralPrimitive(span, span.toAbsolute(absoluteOffset), input), input, typeof sourceSpanOrLocation === 'string'
|
|
? sourceSpanOrLocation
|
|
: getLocation(sourceSpanOrLocation), absoluteOffset, []);
|
|
}
|
|
_stripComments(input) {
|
|
const i = this._commentStart(input);
|
|
return i != null
|
|
? { stripped: input.substring(0, i), hasComments: true }
|
|
: { stripped: input, hasComments: false };
|
|
}
|
|
_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(errors, input, parseSourceSpan, { 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) {
|
|
errors.push(getParseError(`Got interpolation (${start}${end}) where expression was expected`, input, `at column ${startIndex} in`, parseSourceSpan));
|
|
}
|
|
}
|
|
/**
|
|
* 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 {
|
|
input;
|
|
parseSourceSpan;
|
|
absoluteOffset;
|
|
tokens;
|
|
parseFlags;
|
|
errors;
|
|
offset;
|
|
supportsDirectPipeReferences;
|
|
rparensExpected = 0;
|
|
rbracketsExpected = 0;
|
|
rbracesExpected = 0;
|
|
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.
|
|
sourceSpanCache = new Map();
|
|
index = 0;
|
|
constructor(input, parseSourceSpan, absoluteOffset, tokens, parseFlags, errors, offset, supportsDirectPipeReferences) {
|
|
this.input = input;
|
|
this.parseSourceSpan = parseSourceSpan;
|
|
this.absoluteOffset = absoluteOffset;
|
|
this.tokens = tokens;
|
|
this.parseFlags = parseFlags;
|
|
this.errors = errors;
|
|
this.offset = offset;
|
|
this.supportsDirectPipeReferences = supportsDirectPipeReferences;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
isAssignmentOperator(token) {
|
|
return token.type === TokenType.Operator && Binary.isAssignmentOperation(token.strValue);
|
|
}
|
|
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.
|
|
}
|
|
let type;
|
|
if (this.supportsDirectPipeReferences) {
|
|
const charCode = nameId.charCodeAt(0);
|
|
type =
|
|
charCode === $_ || (charCode >= $A && charCode <= $Z)
|
|
? BindingPipeType.ReferencedDirectly
|
|
: BindingPipeType.ReferencedByName;
|
|
}
|
|
else {
|
|
type = BindingPipeType.ReferencedByName;
|
|
}
|
|
result = new BindingPipe(this.span(start), this.sourceSpan(start, fullSpanEnd), result, nameId, args, type, 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() {
|
|
// '<', '>', '<=', '>=', 'in'
|
|
const start = this.inputIndex;
|
|
let result = this.parseAdditive();
|
|
while (this.next.type == TokenType.Operator || this.next.isKeywordIn) {
|
|
const operator = this.next.strValue;
|
|
switch (operator) {
|
|
case '<':
|
|
case '>':
|
|
case '<=':
|
|
case '>=':
|
|
case 'in':
|
|
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.parseExponentiation();
|
|
while (this.next.type == TokenType.Operator) {
|
|
const operator = this.next.strValue;
|
|
switch (operator) {
|
|
case '*':
|
|
case '%':
|
|
case '/':
|
|
this.advance();
|
|
const right = this.parseExponentiation();
|
|
result = new Binary(this.span(start), this.sourceSpan(start), operator, result, right);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
parseExponentiation() {
|
|
// '**'
|
|
const start = this.inputIndex;
|
|
let result = this.parsePrefix();
|
|
while (this.next.type == TokenType.Operator && this.next.strValue === '**') {
|
|
// This aligns with Javascript semantics which require any unary operator preceeding the
|
|
// exponentiation operation to be explicitly grouped as either applying to the base or result
|
|
// of the exponentiation operation.
|
|
if (result instanceof Unary ||
|
|
result instanceof PrefixNot ||
|
|
result instanceof TypeofExpression ||
|
|
result instanceof VoidExpression) {
|
|
this.error('Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence');
|
|
}
|
|
this.advance();
|
|
const right = this.parseExponentiation();
|
|
result = new Binary(this.span(start), this.sourceSpan(start), '**', result, right);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
else if (this.next.isKeywordTypeof()) {
|
|
this.advance();
|
|
const start = this.inputIndex;
|
|
let result = this.parsePrefix();
|
|
return new TypeofExpression(this.span(start), this.sourceSpan(start), result);
|
|
}
|
|
else if (this.next.isKeywordVoid()) {
|
|
this.advance();
|
|
const start = this.inputIndex;
|
|
let result = this.parsePrefix();
|
|
return new VoidExpression(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 if (this.next.isTemplateLiteralEnd()) {
|
|
result = this.parseNoInterpolationTaggedTemplateLiteral(result, start);
|
|
}
|
|
else if (this.next.isTemplateLiteralPart()) {
|
|
result = this.parseTaggedTemplateLiteral(result, start);
|
|
}
|
|
else {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
parsePrimary() {
|
|
const start = this.inputIndex;
|
|
if (this.consumeOptionalCharacter($LPAREN)) {
|
|
this.rparensExpected++;
|
|
const result = this.parsePipe();
|
|
if (!this.consumeOptionalCharacter($RPAREN)) {
|
|
this.error('Missing closing parentheses');
|
|
// Calling into `error` above will attempt to recover up until the next closing paren.
|
|
// If that's the case, consume it so we can partially recover the expression.
|
|
this.consumeOptionalCharacter($RPAREN);
|
|
}
|
|
this.rparensExpected--;
|
|
return new ParenthesizedExpression(this.span(start), this.sourceSpan(start), 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.isKeywordIn()) {
|
|
this.advance();
|
|
return new LiteralPrimitive(this.span(start), this.sourceSpan(start), 'in');
|
|
}
|
|
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.isTemplateLiteralEnd()) {
|
|
return this.parseNoInterpolationTemplateLiteral();
|
|
}
|
|
else if (this.next.isTemplateLiteralPart()) {
|
|
return this.parseTemplateLiteral();
|
|
}
|
|
else if (this.next.isString() && this.next.kind === StringTokenKind.Plain) {
|
|
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();
|
|
const literalMapKey = { key, quoted };
|
|
keys.push(literalMapKey);
|
|
// 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 {
|
|
literalMapKey.isShorthandInitialized = true;
|
|
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);
|
|
if (isSafe) {
|
|
if (this.isAssignmentOperator(this.next)) {
|
|
this.advance();
|
|
this.error("The '?.' operator cannot be used in the assignment");
|
|
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
|
|
}
|
|
else {
|
|
return new SafePropertyRead(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id);
|
|
}
|
|
}
|
|
else {
|
|
if (this.isAssignmentOperator(this.next)) {
|
|
const operation = this.next.strValue;
|
|
if (!(this.parseFlags & 1 /* ParseFlags.Action */)) {
|
|
this.advance();
|
|
this.error('Bindings cannot contain assignments');
|
|
return new EmptyExpr$1(this.span(start), this.sourceSpan(start));
|
|
}
|
|
const receiver = new PropertyRead(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id);
|
|
this.advance();
|
|
const value = this.parseConditional();
|
|
return new Binary(this.span(start), this.sourceSpan(start), operation, receiver, value);
|
|
}
|
|
else {
|
|
return new PropertyRead(this.span(start), this.sourceSpan(start), nameSpan, readReceiver, id);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
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,
|
|
* ```html
|
|
* <div *ngFor="let item of items; index as i; trackBy: func">
|
|
* ```
|
|
* 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.isAssignmentOperator(this.next)) {
|
|
const operation = this.next.strValue;
|
|
if (isSafe) {
|
|
this.advance();
|
|
this.error("The '?.' operator cannot be used in the assignment");
|
|
}
|
|
else {
|
|
const binaryReceiver = new KeyedRead(this.span(start), this.sourceSpan(start), receiver, key);
|
|
this.advance();
|
|
const value = this.parseConditional();
|
|
return new Binary(this.span(start), this.sourceSpan(start), operation, binaryReceiver, 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, getLocation(this.parseSourceSpan), 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);
|
|
}
|
|
parseNoInterpolationTaggedTemplateLiteral(tag, start) {
|
|
const template = this.parseNoInterpolationTemplateLiteral();
|
|
return new TaggedTemplateLiteral(this.span(start), this.sourceSpan(start), tag, template);
|
|
}
|
|
parseNoInterpolationTemplateLiteral() {
|
|
const text = this.next.strValue;
|
|
const start = this.inputIndex;
|
|
this.advance();
|
|
const span = this.span(start);
|
|
const sourceSpan = this.sourceSpan(start);
|
|
return new TemplateLiteral(span, sourceSpan, [new TemplateLiteralElement(span, sourceSpan, text)], []);
|
|
}
|
|
parseTaggedTemplateLiteral(tag, start) {
|
|
const template = this.parseTemplateLiteral();
|
|
return new TaggedTemplateLiteral(this.span(start), this.sourceSpan(start), tag, template);
|
|
}
|
|
parseTemplateLiteral() {
|
|
const elements = [];
|
|
const expressions = [];
|
|
const start = this.inputIndex;
|
|
while (this.next !== EOF) {
|
|
const token = this.next;
|
|
if (token.isTemplateLiteralPart() || token.isTemplateLiteralEnd()) {
|
|
const partStart = this.inputIndex;
|
|
this.advance();
|
|
elements.push(new TemplateLiteralElement(this.span(partStart), this.sourceSpan(partStart), token.strValue));
|
|
if (token.isTemplateLiteralEnd()) {
|
|
break;
|
|
}
|
|
}
|
|
else if (token.isTemplateLiteralInterpolationStart()) {
|
|
this.advance();
|
|
this.rbracesExpected++;
|
|
const expression = this.parsePipe();
|
|
if (expression instanceof EmptyExpr$1) {
|
|
this.error('Template literal interpolation cannot be empty');
|
|
}
|
|
else {
|
|
expressions.push(expression);
|
|
}
|
|
this.rbracesExpected--;
|
|
}
|
|
else {
|
|
this.advance();
|
|
}
|
|
}
|
|
return new TemplateLiteral(this.span(start), this.sourceSpan(start), elements, expressions);
|
|
}
|
|
/**
|
|
* 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 = this.index) {
|
|
this.errors.push(getParseError(message, this.input, this.getErrorLocationText(index), this.parseSourceSpan));
|
|
this.skip();
|
|
}
|
|
getErrorLocationText(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 '(' <expr> ')' 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.
|
|
* - Assignment 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) || !this.isAssignmentOperator(n))) {
|
|
if (this.next.isError()) {
|
|
this.errors.push(getParseError(this.next.toString(), this.input, this.getErrorLocationText(this.next.index), this.parseSourceSpan));
|
|
}
|
|
this.advance();
|
|
n = this.next;
|
|
}
|
|
}
|
|
}
|
|
function getParseError(message, input, locationText, parseSourceSpan) {
|
|
if (locationText.length > 0) {
|
|
locationText = ` ${locationText} `;
|
|
}
|
|
const location = getLocation(parseSourceSpan);
|
|
const error = `Parser Error: ${message}${locationText}[${input}] in ${location}`;
|
|
return new ParseError(parseSourceSpan, error);
|
|
}
|
|
class SimpleExpressionChecker extends RecursiveAstVisitor {
|
|
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;
|
|
}
|
|
|
|
/** Serializes the given AST into a normalized string format. */
|
|
function serialize(expression) {
|
|
return expression.visit(new SerializeExpressionVisitor());
|
|
}
|
|
class SerializeExpressionVisitor {
|
|
visitUnary(ast, context) {
|
|
return `${ast.operator}${ast.expr.visit(this, context)}`;
|
|
}
|
|
visitBinary(ast, context) {
|
|
return `${ast.left.visit(this, context)} ${ast.operation} ${ast.right.visit(this, context)}`;
|
|
}
|
|
visitChain(ast, context) {
|
|
return ast.expressions.map((e) => e.visit(this, context)).join('; ');
|
|
}
|
|
visitConditional(ast, context) {
|
|
return `${ast.condition.visit(this, context)} ? ${ast.trueExp.visit(this, context)} : ${ast.falseExp.visit(this, context)}`;
|
|
}
|
|
visitThisReceiver() {
|
|
return 'this';
|
|
}
|
|
visitImplicitReceiver() {
|
|
return '';
|
|
}
|
|
visitInterpolation(ast, context) {
|
|
return interleave(ast.strings, ast.expressions.map((e) => e.visit(this, context))).join('');
|
|
}
|
|
visitKeyedRead(ast, context) {
|
|
return `${ast.receiver.visit(this, context)}[${ast.key.visit(this, context)}]`;
|
|
}
|
|
visitLiteralArray(ast, context) {
|
|
return `[${ast.expressions.map((e) => e.visit(this, context)).join(', ')}]`;
|
|
}
|
|
visitLiteralMap(ast, context) {
|
|
return `{${zip(ast.keys.map((literal) => (literal.quoted ? `'${literal.key}'` : literal.key)), ast.values.map((value) => value.visit(this, context)))
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join(', ')}}`;
|
|
}
|
|
visitLiteralPrimitive(ast) {
|
|
if (ast.value === null)
|
|
return 'null';
|
|
switch (typeof ast.value) {
|
|
case 'number':
|
|
case 'boolean':
|
|
return ast.value.toString();
|
|
case 'undefined':
|
|
return 'undefined';
|
|
case 'string':
|
|
return `'${ast.value.replace(/'/g, `\\'`)}'`;
|
|
default:
|
|
throw new Error(`Unsupported primitive type: ${ast.value}`);
|
|
}
|
|
}
|
|
visitPipe(ast, context) {
|
|
return `${ast.exp.visit(this, context)} | ${ast.name}`;
|
|
}
|
|
visitPrefixNot(ast, context) {
|
|
return `!${ast.expression.visit(this, context)}`;
|
|
}
|
|
visitNonNullAssert(ast, context) {
|
|
return `${ast.expression.visit(this, context)}!`;
|
|
}
|
|
visitPropertyRead(ast, context) {
|
|
if (ast.receiver instanceof ImplicitReceiver) {
|
|
return ast.name;
|
|
}
|
|
else {
|
|
return `${ast.receiver.visit(this, context)}.${ast.name}`;
|
|
}
|
|
}
|
|
visitSafePropertyRead(ast, context) {
|
|
return `${ast.receiver.visit(this, context)}?.${ast.name}`;
|
|
}
|
|
visitSafeKeyedRead(ast, context) {
|
|
return `${ast.receiver.visit(this, context)}?.[${ast.key.visit(this, context)}]`;
|
|
}
|
|
visitCall(ast, context) {
|
|
return `${ast.receiver.visit(this, context)}(${ast.args
|
|
.map((e) => e.visit(this, context))
|
|
.join(', ')})`;
|
|
}
|
|
visitSafeCall(ast, context) {
|
|
return `${ast.receiver.visit(this, context)}?.(${ast.args
|
|
.map((e) => e.visit(this, context))
|
|
.join(', ')})`;
|
|
}
|
|
visitTypeofExpression(ast, context) {
|
|
return `typeof ${ast.expression.visit(this, context)}`;
|
|
}
|
|
visitVoidExpression(ast, context) {
|
|
return `void ${ast.expression.visit(this, context)}`;
|
|
}
|
|
visitASTWithSource(ast, context) {
|
|
return ast.ast.visit(this, context);
|
|
}
|
|
visitTemplateLiteral(ast, context) {
|
|
let result = '';
|
|
for (let i = 0; i < ast.elements.length; i++) {
|
|
result += ast.elements[i].visit(this, context);
|
|
const expression = i < ast.expressions.length ? ast.expressions[i] : null;
|
|
if (expression !== null) {
|
|
result += '${' + expression.visit(this, context) + '}';
|
|
}
|
|
}
|
|
return '`' + result + '`';
|
|
}
|
|
visitTemplateLiteralElement(ast, context) {
|
|
return ast.text;
|
|
}
|
|
visitTaggedTemplateLiteral(ast, context) {
|
|
return ast.tag.visit(this, context) + ast.template.visit(this, context);
|
|
}
|
|
visitParenthesizedExpression(ast, context) {
|
|
return '(' + ast.expression.visit(this, context) + ')';
|
|
}
|
|
}
|
|
/** Zips the two input arrays into a single array of pairs of elements at the same index. */
|
|
function zip(left, right) {
|
|
if (left.length !== right.length)
|
|
throw new Error('Array lengths must match');
|
|
return left.map((l, i) => [l, right[i]]);
|
|
}
|
|
/**
|
|
* Interleaves the two arrays, starting with the first item on the left, then the first item
|
|
* on the right, second item from the left, and so on. When the first array's items are exhausted,
|
|
* the remaining items from the other array are included with no interleaving.
|
|
*/
|
|
function interleave(left, right) {
|
|
const result = [];
|
|
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
if (index < left.length)
|
|
result.push(left[index]);
|
|
if (index < right.length)
|
|
result.push(right[index]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// =================================================================================================
|
|
// =================================================================================================
|
|
// =========== 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 LIST OF SECURITY SENSITIVE PROPERTIES WITHOUT A SECURITY REVIEW!
|
|
// Reach out to mprobst for details.
|
|
//
|
|
// =================================================================================================
|
|
/** Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'. */
|
|
let _SECURITY_SCHEMA;
|
|
function SECURITY_SCHEMA() {
|
|
if (!_SECURITY_SCHEMA) {
|
|
_SECURITY_SCHEMA = {};
|
|
// Case is insignificant below, all element and attribute names are lower-cased for lookup.
|
|
registerContext(SecurityContext.HTML, ['iframe|srcdoc', '*|innerHTML', '*|outerHTML']);
|
|
registerContext(SecurityContext.STYLE, ['*|style']);
|
|
// NB: no SCRIPT contexts here, they are never allowed due to the parser stripping them.
|
|
registerContext(SecurityContext.URL, [
|
|
'*|formAction',
|
|
'area|href',
|
|
'area|ping',
|
|
'audio|src',
|
|
'a|href',
|
|
'a|ping',
|
|
'blockquote|cite',
|
|
'body|background',
|
|
'del|cite',
|
|
'form|action',
|
|
'img|src',
|
|
'input|src',
|
|
'ins|cite',
|
|
'q|cite',
|
|
'source|src',
|
|
'track|src',
|
|
'video|poster',
|
|
'video|src',
|
|
]);
|
|
registerContext(SecurityContext.RESOURCE_URL, [
|
|
'applet|code',
|
|
'applet|codebase',
|
|
'base|href',
|
|
'embed|src',
|
|
'frame|src',
|
|
'head|profile',
|
|
'html|manifest',
|
|
'iframe|src',
|
|
'link|href',
|
|
'media|src',
|
|
'object|codebase',
|
|
'object|data',
|
|
'script|src',
|
|
]);
|
|
}
|
|
return _SECURITY_SCHEMA;
|
|
}
|
|
function registerContext(ctx, specs) {
|
|
for (const spec of specs)
|
|
_SECURITY_SCHEMA[spec.toLowerCase()] = ctx;
|
|
}
|
|
/**
|
|
* The set of security-sensitive attributes of an `<iframe>` that *must* be
|
|
* applied as a static attribute only. This ensures that all security-sensitive
|
|
* attributes are taken into account while creating an instance of an `<iframe>`
|
|
* at runtime.
|
|
*
|
|
* Note: avoid using this set directly, use the `isIframeSecuritySensitiveAttr` function
|
|
* in the code instead.
|
|
*/
|
|
const IFRAME_SECURITY_SENSITIVE_ATTRS = new Set([
|
|
'sandbox',
|
|
'allow',
|
|
'allowfullscreen',
|
|
'referrerpolicy',
|
|
'csp',
|
|
'fetchpriority',
|
|
]);
|
|
/**
|
|
* Checks whether a given attribute name might represent a security-sensitive
|
|
* attribute of an <iframe>.
|
|
*/
|
|
function isIframeSecuritySensitiveAttr(attrName) {
|
|
// The `setAttribute` DOM API is case-insensitive, so we lowercase the value
|
|
// before checking it against a known security-sensitive attributes.
|
|
return IFRAME_SECURITY_SENSITIVE_ATTRS.has(attrName.toLowerCase());
|
|
}
|
|
|
|
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,%ariaInvalid,%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,!inert,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,search,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',
|
|
'selectedcontent^[HTMLElement]|',
|
|
'slot^[HTMLElement]|name',
|
|
'source^[HTMLElement]|#height,media,sizes,src,srcset,type,#width',
|
|
'span^[HTMLElement]|',
|
|
'style^[HTMLElement]|!disabled,media,type',
|
|
'search^[HTMLELement]|',
|
|
'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:|',
|
|
':math:^[HTMLElement]|!autofocus,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforeinput,*beforematch,*beforetoggle,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contentvisibilityautostatechange,*contextlost,*contextmenu,*contextrestored,*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,*scrollend,*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',
|
|
':math:math^:math:|',
|
|
':math:maction^:math:|',
|
|
':math:menclose^:math:|',
|
|
':math:merror^:math:|',
|
|
':math:mfenced^:math:|',
|
|
':math:mfrac^:math:|',
|
|
':math:mi^:math:|',
|
|
':math:mmultiscripts^:math:|',
|
|
':math:mn^:math:|',
|
|
':math:mo^:math:|',
|
|
':math:mover^:math:|',
|
|
':math:mpadded^:math:|',
|
|
':math:mphantom^:math:|',
|
|
':math:mroot^:math:|',
|
|
':math:mrow^:math:|',
|
|
':math:ms^:math:|',
|
|
':math:mspace^:math:|',
|
|
':math:msqrt^:math:|',
|
|
':math:mstyle^:math:|',
|
|
':math:msub^:math:|',
|
|
':math:msubsup^:math:|',
|
|
':math:msup^:math:|',
|
|
':math:mtable^:math:|',
|
|
':math:mtd^:math:|',
|
|
':math:mtext^:math:|',
|
|
':math:mtr^:math:|',
|
|
':math:munder^:math:|',
|
|
':math:munderover^:math:|',
|
|
':math:semantics^:math:|',
|
|
];
|
|
const _ATTR_TO_PROP = new Map(Object.entries({
|
|
'class': 'className',
|
|
'for': 'htmlFor',
|
|
'formaction': 'formAction',
|
|
'innerHtml': 'innerHTML',
|
|
'readonly': 'readOnly',
|
|
'tabindex': 'tabIndex',
|
|
// https://www.w3.org/TR/wai-aria-1.2/#accessibilityroleandproperties-correspondence
|
|
'aria-atomic': 'ariaAtomic',
|
|
'aria-autocomplete': 'ariaAutoComplete',
|
|
'aria-busy': 'ariaBusy',
|
|
'aria-checked': 'ariaChecked',
|
|
'aria-colcount': 'ariaColCount',
|
|
'aria-colindex': 'ariaColIndex',
|
|
'aria-colspan': 'ariaColSpan',
|
|
'aria-current': 'ariaCurrent',
|
|
'aria-disabled': 'ariaDisabled',
|
|
'aria-expanded': 'ariaExpanded',
|
|
'aria-haspopup': 'ariaHasPopup',
|
|
'aria-hidden': 'ariaHidden',
|
|
'aria-invalid': 'ariaInvalid',
|
|
'aria-keyshortcuts': 'ariaKeyShortcuts',
|
|
'aria-label': 'ariaLabel',
|
|
'aria-level': 'ariaLevel',
|
|
'aria-live': 'ariaLive',
|
|
'aria-modal': 'ariaModal',
|
|
'aria-multiline': 'ariaMultiLine',
|
|
'aria-multiselectable': 'ariaMultiSelectable',
|
|
'aria-orientation': 'ariaOrientation',
|
|
'aria-placeholder': 'ariaPlaceholder',
|
|
'aria-posinset': 'ariaPosInSet',
|
|
'aria-pressed': 'ariaPressed',
|
|
'aria-readonly': 'ariaReadOnly',
|
|
'aria-required': 'ariaRequired',
|
|
'aria-roledescription': 'ariaRoleDescription',
|
|
'aria-rowcount': 'ariaRowCount',
|
|
'aria-rowindex': 'ariaRowIndex',
|
|
'aria-rowspan': 'ariaRowSpan',
|
|
'aria-selected': 'ariaSelected',
|
|
'aria-setsize': 'ariaSetSize',
|
|
'aria-sort': 'ariaSort',
|
|
'aria-valuemax': 'ariaValueMax',
|
|
'aria-valuemin': 'ariaValueMin',
|
|
'aria-valuenow': 'ariaValueNow',
|
|
'aria-valuetext': 'ariaValueText',
|
|
}));
|
|
// 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 {
|
|
_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.
|
|
_eventSchema = new Map();
|
|
constructor() {
|
|
super();
|
|
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 {
|
|
closedByChildren = {};
|
|
contentType;
|
|
closedByParent = false;
|
|
implicitNamespacePrefix;
|
|
isVoid;
|
|
ignoreFirstLf;
|
|
canSelfClose;
|
|
preventNamespaceInheritance;
|
|
constructor({ closedByChildren, implicitNamespacePrefix, contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false, ignoreFirstLf = false, preventNamespaceInheritance = false, canSelfClose = 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 {
|
|
// Count the occurrence of the base name top generate a unique name
|
|
_placeHolderNameCounts = {};
|
|
// Maps signature to placeholder names
|
|
_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(new Lexer());
|
|
/**
|
|
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
|
|
*/
|
|
function createI18nMessageFactory(interpolationConfig, containerBlocks, retainEmptyTokens, preserveExpressionWhitespace) {
|
|
const visitor = new _I18nVisitor(_expParser, interpolationConfig, containerBlocks, retainEmptyTokens, preserveExpressionWhitespace);
|
|
return (nodes, meaning, description, customId, visitNodeFn) => visitor.toI18nMessage(nodes, meaning, description, customId, visitNodeFn);
|
|
}
|
|
function noopVisitNodeFn(_html, i18n) {
|
|
return i18n;
|
|
}
|
|
class _I18nVisitor {
|
|
_expressionParser;
|
|
_interpolationConfig;
|
|
_containerBlocks;
|
|
_retainEmptyTokens;
|
|
_preserveExpressionWhitespace;
|
|
constructor(_expressionParser, _interpolationConfig, _containerBlocks, _retainEmptyTokens, _preserveExpressionWhitespace) {
|
|
this._expressionParser = _expressionParser;
|
|
this._interpolationConfig = _interpolationConfig;
|
|
this._containerBlocks = _containerBlocks;
|
|
this._retainEmptyTokens = _retainEmptyTokens;
|
|
this._preserveExpressionWhitespace = _preserveExpressionWhitespace;
|
|
}
|
|
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) {
|
|
return this._visitElementLike(el, context);
|
|
}
|
|
visitComponent(component, context) {
|
|
return this._visitElementLike(component, context);
|
|
}
|
|
visitDirective(directive, context) {
|
|
throw new Error('Unreachable code');
|
|
}
|
|
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');
|
|
}
|
|
visitLetDeclaration(decl, context) {
|
|
return null;
|
|
}
|
|
_visitElementLike(node, context) {
|
|
const children = visitAll(this, node.children, context);
|
|
const attrs = {};
|
|
const visitAttribute = (attr) => {
|
|
// Do not visit the attributes, translatable ones are top-level ASTs
|
|
attrs[attr.name] = attr.value;
|
|
};
|
|
let nodeName;
|
|
let isVoid;
|
|
if (node instanceof Element) {
|
|
nodeName = node.name;
|
|
isVoid = getHtmlTagDefinition(node.name).isVoid;
|
|
}
|
|
else {
|
|
nodeName = node.fullName;
|
|
isVoid = node.tagName ? getHtmlTagDefinition(node.tagName).isVoid : false;
|
|
}
|
|
node.attrs.forEach(visitAttribute);
|
|
node.directives.forEach((dir) => dir.attrs.forEach(visitAttribute));
|
|
const startPhName = context.placeholderRegistry.getStartTagPlaceholderName(nodeName, attrs, isVoid);
|
|
context.placeholderToContent[startPhName] = {
|
|
text: node.startSourceSpan.toString(),
|
|
sourceSpan: node.startSourceSpan,
|
|
};
|
|
let closePhName = '';
|
|
if (!isVoid) {
|
|
closePhName = context.placeholderRegistry.getCloseTagPlaceholderName(nodeName);
|
|
context.placeholderToContent[closePhName] = {
|
|
text: `</${nodeName}>`,
|
|
sourceSpan: node.endSourceSpan ?? node.sourceSpan,
|
|
};
|
|
}
|
|
const i18nNode = new TagPlaceholder(nodeName, attrs, startPhName, closePhName, children, isVoid, node.sourceSpan, node.startSourceSpan, node.endSourceSpan);
|
|
return context.visitNodeFn(node, i18nNode);
|
|
}
|
|
/**
|
|
* 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 [startMarker, expression, endMarker] = token.parts;
|
|
const baseName = extractPlaceholderName(expression) || 'INTERPOLATION';
|
|
const phName = context.placeholderRegistry.getPlaceholderName(baseName, expression);
|
|
if (this._preserveExpressionWhitespace) {
|
|
context.placeholderToContent[phName] = {
|
|
text: token.parts.join(''),
|
|
sourceSpan: token.sourceSpan,
|
|
};
|
|
nodes.push(new Placeholder(expression, phName, token.sourceSpan));
|
|
}
|
|
else {
|
|
const normalized = this.normalizeExpression(token);
|
|
context.placeholderToContent[phName] = {
|
|
text: `${startMarker}${normalized}${endMarker}`,
|
|
sourceSpan: token.sourceSpan,
|
|
};
|
|
nodes.push(new Placeholder(normalized, phName, token.sourceSpan));
|
|
}
|
|
break;
|
|
default:
|
|
// Try to merge text tokens with previous tokens. We do this even for all tokens
|
|
// when `retainEmptyTokens == true` because whitespace tokens may have non-zero
|
|
// length, but will be trimmed by `WhitespaceVisitor` in one extraction pass and
|
|
// be considered "empty" there. Therefore a whitespace token with
|
|
// `retainEmptyTokens === true` should be treated like an empty token and either
|
|
// retained or merged into the previous node. Since extraction does two passes with
|
|
// different trimming behavior, the second pass needs to have identical node count
|
|
// to reuse source spans, so we need this check to get the same answer when both
|
|
// trimming and not trimming.
|
|
if (token.parts[0].length > 0 || this._retainEmptyTokens) {
|
|
// 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));
|
|
}
|
|
}
|
|
else {
|
|
// Retain empty tokens to avoid breaking dropping entire nodes such that source
|
|
// spans should not be reusable across multiple parses of a template. We *should*
|
|
// do this all the time, however we need to maintain backwards compatibility
|
|
// with existing message IDs so we can't do it by default and should only enable
|
|
// this when removing significant whitespace.
|
|
if (this._retainEmptyTokens) {
|
|
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];
|
|
}
|
|
}
|
|
// Normalize expression whitespace by parsing and re-serializing it. This makes
|
|
// message IDs more durable to insignificant whitespace changes.
|
|
normalizeExpression(token) {
|
|
const expression = token.parts[1];
|
|
const expr = this._expressionParser.parseBinding(expression,
|
|
/* location */ token.sourceSpan,
|
|
/* absoluteOffset */ token.sourceSpan.start.offset, this._interpolationConfig);
|
|
return serialize(expr);
|
|
}
|
|
}
|
|
/**
|
|
* 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.
|
|
|
|
First pass (${previousNodes.length} tokens):
|
|
${previousNodes.map((node) => `"${node.sourceSpan.toString()}"`).join('\n')}
|
|
|
|
Second pass (${nodes.length} tokens):
|
|
${nodes.map((node) => `"${node.sourceSpan.toString()}"`).join('\n')}
|
|
`.trim());
|
|
}
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* 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 = (originalNodeMap) => {
|
|
return (trimmedNode, i18nNode) => {
|
|
// We need to set i18n properties on the original, untrimmed AST nodes. The i18n nodes needs to
|
|
// use the trimmed content for message IDs to make messages more stable to whitespace changes.
|
|
// But we don't want to actually trim the content, so we can't use the trimmed HTML AST for
|
|
// general code gen. Instead we map the trimmed HTML AST back to the original AST and then
|
|
// attach the i18n nodes so we get trimmed i18n nodes on the original (untrimmed) HTML AST.
|
|
const originalNode = originalNodeMap.get(trimmedNode) ?? trimmedNode;
|
|
if (originalNode instanceof NodeWithI18n) {
|
|
if (i18nNode instanceof IcuPlaceholder && originalNode.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 = originalNode.i18n;
|
|
}
|
|
originalNode.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 {
|
|
interpolationConfig;
|
|
keepI18nAttrs;
|
|
enableI18nLegacyMessageIdFormat;
|
|
containerBlocks;
|
|
preserveSignificantWhitespace;
|
|
retainEmptyTokens;
|
|
// whether visited nodes contain i18n information
|
|
hasI18nMeta = false;
|
|
_errors = [];
|
|
constructor(interpolationConfig = DEFAULT_INTERPOLATION_CONFIG, keepI18nAttrs = false, enableI18nLegacyMessageIdFormat = false, containerBlocks = DEFAULT_CONTAINER_BLOCKS, preserveSignificantWhitespace = true,
|
|
// When dropping significant whitespace we need to retain empty tokens or
|
|
// else we won't be able to reuse source spans because empty tokens would be
|
|
// removed and cause a mismatch. Unfortunately this still needs to be
|
|
// configurable and sometimes needs to be set independently in order to make
|
|
// sure the number of nodes don't change between parses, even when
|
|
// `preserveSignificantWhitespace` changes.
|
|
retainEmptyTokens = !preserveSignificantWhitespace) {
|
|
this.interpolationConfig = interpolationConfig;
|
|
this.keepI18nAttrs = keepI18nAttrs;
|
|
this.enableI18nLegacyMessageIdFormat = enableI18nLegacyMessageIdFormat;
|
|
this.containerBlocks = containerBlocks;
|
|
this.preserveSignificantWhitespace = preserveSignificantWhitespace;
|
|
this.retainEmptyTokens = retainEmptyTokens;
|
|
}
|
|
_generateI18nMessage(nodes, meta = '', visitNodeFn) {
|
|
const { meaning, description, customId } = this._parseMetadata(meta);
|
|
const createI18nMessage = createI18nMessageFactory(this.interpolationConfig, this.containerBlocks, this.retainEmptyTokens,
|
|
/* preserveExpressionWhitespace */ this.preserveSignificantWhitespace);
|
|
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) {
|
|
this._visitElementLike(element);
|
|
return element;
|
|
}
|
|
visitComponent(component, context) {
|
|
this._visitElementLike(component);
|
|
return component;
|
|
}
|
|
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;
|
|
}
|
|
visitLetDeclaration(decl, context) {
|
|
return decl;
|
|
}
|
|
visitDirective(directive, context) {
|
|
return directive;
|
|
}
|
|
_visitElementLike(node) {
|
|
let message = undefined;
|
|
if (hasI18nAttrs(node)) {
|
|
this.hasI18nMeta = true;
|
|
const attrs = [];
|
|
const attrsMeta = {};
|
|
for (const attr of node.attrs) {
|
|
if (attr.name === I18N_ATTR) {
|
|
// root 'i18n' node attribute
|
|
const i18n = node.i18n || attr.value;
|
|
// Generate a new AST with whitespace trimmed, but also generate a map
|
|
// to correlate each new node to its original so we can apply i18n
|
|
// information to the original node based on the trimmed content.
|
|
//
|
|
// `WhitespaceVisitor` removes *insignificant* whitespace as well as
|
|
// significant whitespace. Enabling this visitor should be conditional
|
|
// on `preserveWhitespace` rather than `preserveSignificantWhitespace`,
|
|
// however this would be a breaking change for existing behavior where
|
|
// `preserveWhitespace` was not respected correctly when generating
|
|
// message IDs. This is really a bug but one we need to keep to maintain
|
|
// backwards compatibility.
|
|
const originalNodeMap = new Map();
|
|
const trimmedNodes = this.preserveSignificantWhitespace
|
|
? node.children
|
|
: visitAllWithSiblings(new WhitespaceVisitor(false /* preserveSignificantWhitespace */, originalNodeMap), node.children);
|
|
message = this._generateI18nMessage(trimmedNodes, i18n, setI18nRefs(originalNodeMap));
|
|
if (message.nodes.length === 0) {
|
|
// Ignore the message if it is empty.
|
|
message = undefined;
|
|
}
|
|
// Store the message on the element
|
|
node.i18n = message;
|
|
}
|
|
else if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
|
|
// 'i18n-*' attributes
|
|
const name = attr.name.slice(I18N_ATTR_PREFIX.length);
|
|
let isTrustedType;
|
|
if (node instanceof Component) {
|
|
isTrustedType = node.tagName === null ? false : isTrustedTypesSink(node.tagName, name);
|
|
}
|
|
else {
|
|
isTrustedType = isTrustedTypesSink(node.name, name);
|
|
}
|
|
if (isTrustedType) {
|
|
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
|
|
node.attrs = attrs;
|
|
}
|
|
}
|
|
visitAll(this, node.children, message);
|
|
}
|
|
/**
|
|
* 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 ParseError(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
|
|
* <div i18n>Sent from {{ sender }} to <span class="receiver">{{ receiver }}</span></div>
|
|
* ```
|
|
*
|
|
* Generates:
|
|
*
|
|
* ```ts
|
|
* 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': '<span class="receiver">',
|
|
* 'interpolation_1': '{{ receiver }}',
|
|
* 'closeTagSpan': '</span>',
|
|
* },
|
|
* },
|
|
* );
|
|
* 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': '<span>'}}`.
|
|
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 = new DeclareVarStmt(closureVar.name, variable(GOOG_GET_MSG).callFn(args), INFERRED_TYPE, StmtModifier.Final);
|
|
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 {
|
|
placeholderToMessage;
|
|
pieces;
|
|
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 = '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';
|
|
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
|
|
const CLOSURE_TRANSLATION_VAR_PREFIX = 'MSG_';
|
|
/**
|
|
* Generates a prefix for translation const name.
|
|
*
|
|
* @param extra Additional local prefix that should be injected into translation var name
|
|
* @returns Complete translation const prefix
|
|
*/
|
|
function getTranslationConstPrefix(extra) {
|
|
return `${CLOSURE_TRANSLATION_VAR_PREFIX}${extra}`.toUpperCase();
|
|
}
|
|
/**
|
|
* Generate AST to declare a variable. E.g. `var I18N_1;`.
|
|
* @param variable the name of the variable to declare.
|
|
*/
|
|
function declareI18nVariable(variable) {
|
|
return new DeclareVarStmt(variable.name, undefined, INFERRED_TYPE, undefined, variable.sourceSpan);
|
|
}
|
|
/**
|
|
* 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(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.
|
|
*
|
|
* ```ts
|
|
* 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(message, variable, closureVar, params, transformFn) {
|
|
const paramsObject = Object.fromEntries(params);
|
|
const statements = [
|
|
declareI18nVariable(variable),
|
|
ifStmt(createClosureModeGuard(), 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:
|
|
*
|
|
* ```ts
|
|
* typeof ngI18nClosureMode !== undefined && ngI18nClosureMode
|
|
* ```
|
|
*/
|
|
function createClosureModeGuard() {
|
|
return typeofExpr(variable(NG_I18N_CLOSURE_MODE))
|
|
.notIdentical(literal('undefined', STRING_TYPE))
|
|
.and(variable(NG_I18N_CLOSURE_MODE));
|
|
}
|
|
/**
|
|
* 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.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
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();
|
|
}
|
|
/**
|
|
* 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.kind === OpKind.ConditionalCreate ||
|
|
target.kind === OpKind.ConditionalBranchCreate) &&
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.DomProperty:
|
|
if (op.bindingKind === BindingKind.LegacyAnimation) {
|
|
op.name = '@' + op.name;
|
|
}
|
|
break;
|
|
case OpKind.Animation:
|
|
if (op.handlerFnName === null) {
|
|
const animationKind = op.name.replace('.', '');
|
|
op.handlerFnName = `${unit.fnName}_${animationKind}_cb`;
|
|
op.handlerFnName = sanitizeIdentifier(op.handlerFnName);
|
|
}
|
|
break;
|
|
case OpKind.AnimationListener:
|
|
if (op.handlerFnName !== null) {
|
|
break;
|
|
}
|
|
if (!op.hostListener && op.targetSlot.slot === null) {
|
|
throw new Error(`Expected a slot to be assigned`);
|
|
}
|
|
const animationKind = op.name.replace('.', '');
|
|
if (op.hostListener) {
|
|
op.handlerFnName = `${baseName}_${animationKind}_HostBindingHandler`;
|
|
}
|
|
else {
|
|
op.handlerFnName = `${unit.fnName}_${op.tag.replace('-', '_')}_${animationKind}_${op.targetSlot.slot}_listener`;
|
|
}
|
|
op.handlerFnName = sanitizeIdentifier(op.handlerFnName);
|
|
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.isLegacyAnimationListener) {
|
|
op.name = `@${op.name}.${op.legacyAnimationPhase}`;
|
|
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.TwoWayListener:
|
|
if (op.handlerFnName !== null) {
|
|
break;
|
|
}
|
|
if (op.targetSlot.slot === null) {
|
|
throw new Error(`Expected a slot to be assigned`);
|
|
}
|
|
op.handlerFnName = sanitizeIdentifier(`${unit.fnName}_${op.tag.replace('-', '_')}_${op.name}_${op.targetSlot.slot}_listener`);
|
|
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.Projection:
|
|
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.fallbackView !== null) {
|
|
const fallbackView = unit.job.views.get(op.fallbackView);
|
|
addNamesToView(fallbackView, `${baseName}_ProjectionFallback_${op.handle.slot}`, state, compatibility);
|
|
}
|
|
break;
|
|
case OpKind.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
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 compatibility 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 ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener ||
|
|
op.kind === OpKind.TwoWayListener) {
|
|
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:
|
|
case ExpressionKind.ContextLetReference:
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function kindTest(kind) {
|
|
return (op) => op.kind === kind;
|
|
}
|
|
function kindWithInterpolationTest(kind, interpolation) {
|
|
return (op) => {
|
|
return op.kind === kind && interpolation === op.expression instanceof Interpolation;
|
|
};
|
|
}
|
|
function basicListenerKindTest(op) {
|
|
return ((op.kind === OpKind.Listener && !(op.hostListener && op.isLegacyAnimationListener)) ||
|
|
op.kind === OpKind.TwoWayListener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener);
|
|
}
|
|
function nonInterpolationPropertyKindTest(op) {
|
|
return ((op.kind === OpKind.Property || op.kind === OpKind.TwoWayProperty) &&
|
|
!(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.isLegacyAnimationListener },
|
|
{ test: basicListenerKindTest },
|
|
];
|
|
/**
|
|
* 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: nonInterpolationPropertyKindTest },
|
|
{ test: kindWithInterpolationTest(OpKind.Attribute, false) },
|
|
];
|
|
/**
|
|
* Host bindings have their own update ordering.
|
|
*/
|
|
const UPDATE_HOST_ORDERING = [
|
|
{ test: kindWithInterpolationTest(OpKind.DomProperty, true) },
|
|
{ test: kindWithInterpolationTest(OpKind.DomProperty, 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.TwoWayListener,
|
|
OpKind.AnimationListener,
|
|
OpKind.StyleMap,
|
|
OpKind.ClassMap,
|
|
OpKind.StyleProp,
|
|
OpKind.ClassProp,
|
|
OpKind.Property,
|
|
OpKind.TwoWayProperty,
|
|
OpKind.DomProperty,
|
|
OpKind.Attribute,
|
|
OpKind.Animation,
|
|
]);
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
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(forView, 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;
|
|
case OpKind.Projection:
|
|
if (op.fallbackView !== null) {
|
|
subTemplateIndex = propagateI18nBlocksForView(unit.job.views.get(op.fallbackView), i18nBlock, op.fallbackViewI18nPlaceholder, subTemplateIndex);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return subTemplateIndex;
|
|
}
|
|
/**
|
|
* Propagate i18n blocks for a view.
|
|
*/
|
|
function propagateI18nBlocksForView(view, i18nBlock, i18nPlaceholder, subTemplateIndex) {
|
|
// We found an <ng-template> 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 receive 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 {
|
|
numArgs;
|
|
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 templateBase(instruction, 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(instruction, args, sourceSpan);
|
|
}
|
|
function propertyBase(instruction, name, expression, sanitizer, sourceSpan) {
|
|
const args = [literal(name)];
|
|
if (expression instanceof Interpolation) {
|
|
args.push(interpolationToExpression(expression, sourceSpan));
|
|
}
|
|
else {
|
|
args.push(expression);
|
|
}
|
|
if (sanitizer !== null) {
|
|
args.push(sanitizer);
|
|
}
|
|
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) {
|
|
return templateBase(Identifiers.templateCreate, slot, templateFnRef, decls, vars, tag, constIndex, localRefs, 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(importExpr(eventTargetResolver));
|
|
}
|
|
return call(syntheticHost ? Identifiers.syntheticHostListener : Identifiers.listener, args, sourceSpan);
|
|
}
|
|
function twoWayBindingSet(target, value) {
|
|
return importExpr(Identifiers.twoWayBindingSet).callFn([target, value]);
|
|
}
|
|
function twoWayListener(name, handlerFn, sourceSpan) {
|
|
return call(Identifiers.twoWayListener, [literal(name), handlerFn], 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, flags) {
|
|
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),
|
|
literal(flags),
|
|
];
|
|
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,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnIdle,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnIdle,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnIdle,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Immediate,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnImmediate,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnImmediate,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnImmediate,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Timer,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnTimer,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnTimer,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnTimer,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Hover,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnHover,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnHover,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnHover,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Interaction,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnInteraction,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnInteraction,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnInteraction,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Viewport,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferOnViewport,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferPrefetchOnViewport,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateOnViewport,
|
|
},
|
|
],
|
|
[
|
|
DeferTriggerKind.Never,
|
|
{
|
|
["none" /* ir.DeferOpModifierKind.NONE */]: Identifiers.deferHydrateNever,
|
|
["prefetch" /* ir.DeferOpModifierKind.PREFETCH */]: Identifiers.deferHydrateNever,
|
|
["hydrate" /* ir.DeferOpModifierKind.HYDRATE */]: Identifiers.deferHydrateNever,
|
|
},
|
|
],
|
|
]);
|
|
function deferOn(trigger, args, modifier, sourceSpan) {
|
|
const instructionToCall = deferTriggerToR3TriggerInstructionsMap.get(trigger)?.[modifier];
|
|
if (instructionToCall === undefined) {
|
|
throw new Error(`Unable to determine instruction for trigger ${trigger}`);
|
|
}
|
|
return call(instructionToCall, args.map((a) => literal(a)), sourceSpan);
|
|
}
|
|
function projectionDef(def) {
|
|
return call(Identifiers.projectionDef, def ? [def] : [], null);
|
|
}
|
|
function projection(slot, projectionSlotIndex, attributes, fallbackFnName, fallbackDecls, fallbackVars, sourceSpan) {
|
|
const args = [literal(slot)];
|
|
if (projectionSlotIndex !== 0 || attributes !== null || fallbackFnName !== null) {
|
|
args.push(literal(projectionSlotIndex));
|
|
if (attributes !== null) {
|
|
args.push(attributes);
|
|
}
|
|
if (fallbackFnName !== null) {
|
|
if (attributes === null) {
|
|
args.push(literal(null));
|
|
}
|
|
args.push(variable(fallbackFnName), literal(fallbackDecls), literal(fallbackVars));
|
|
}
|
|
}
|
|
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 conditionalCreate(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.conditionalCreate, args, sourceSpan);
|
|
}
|
|
function conditionalBranchCreate(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.conditionalBranchCreate, 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(modifier, expr, sourceSpan) {
|
|
if (modifier === "prefetch" /* ir.DeferOpModifierKind.PREFETCH */) {
|
|
return call(Identifiers.deferPrefetchWhen, [expr], sourceSpan);
|
|
}
|
|
else if (modifier === "hydrate" /* ir.DeferOpModifierKind.HYDRATE */) {
|
|
return call(Identifiers.deferHydrateWhen, [expr], sourceSpan);
|
|
}
|
|
return call(Identifiers.deferWhen, [expr], sourceSpan);
|
|
}
|
|
function declareLet(slot, sourceSpan) {
|
|
return call(Identifiers.declareLet, [literal(slot)], sourceSpan);
|
|
}
|
|
function storeLet(value, sourceSpan) {
|
|
return importExpr(Identifiers.storeLet).callFn([value], sourceSpan);
|
|
}
|
|
function readContextLet(slot) {
|
|
return importExpr(Identifiers.readContextLet).callFn([literal(slot)]);
|
|
}
|
|
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 ariaProperty(name, expression, sourceSpan) {
|
|
return propertyBase(Identifiers.ariaProperty, name, expression, null, sourceSpan);
|
|
}
|
|
function property(name, expression, sanitizer, sourceSpan) {
|
|
return propertyBase(Identifiers.property, name, expression, sanitizer, sourceSpan);
|
|
}
|
|
function twoWayProperty(name, expression, sanitizer, sourceSpan) {
|
|
const args = [literal(name), expression];
|
|
if (sanitizer !== null) {
|
|
args.push(sanitizer);
|
|
}
|
|
return call(Identifiers.twoWayProperty, args, sourceSpan);
|
|
}
|
|
function attribute(name, expression, sanitizer, namespace, sourceSpan) {
|
|
const args = [literal(name)];
|
|
if (expression instanceof Interpolation) {
|
|
args.push(interpolationToExpression(expression, sourceSpan));
|
|
}
|
|
else {
|
|
args.push(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)];
|
|
if (expression instanceof Interpolation) {
|
|
args.push(interpolationToExpression(expression, sourceSpan));
|
|
}
|
|
else {
|
|
args.push(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) {
|
|
const value = expression instanceof Interpolation
|
|
? interpolationToExpression(expression, sourceSpan)
|
|
: expression;
|
|
return call(Identifiers.styleMap, [value], sourceSpan);
|
|
}
|
|
function classMap(expression, sourceSpan) {
|
|
const value = expression instanceof Interpolation
|
|
? interpolationToExpression(expression, sourceSpan)
|
|
: expression;
|
|
return call(Identifiers.classMap, [value], sourceSpan);
|
|
}
|
|
function domElement(slot, tag, constIndex, localRefIndex, sourceSpan) {
|
|
return elementOrContainerBase(Identifiers.domElement, slot, tag, constIndex, localRefIndex, sourceSpan);
|
|
}
|
|
function domElementStart(slot, tag, constIndex, localRefIndex, sourceSpan) {
|
|
return elementOrContainerBase(Identifiers.domElementStart, slot, tag, constIndex, localRefIndex, sourceSpan);
|
|
}
|
|
function domElementEnd(sourceSpan) {
|
|
return call(Identifiers.domElementEnd, [], sourceSpan);
|
|
}
|
|
function domElementContainerStart(slot, constIndex, localRefIndex, sourceSpan) {
|
|
return elementOrContainerBase(Identifiers.domElementContainerStart, slot,
|
|
/* tag */ null, constIndex, localRefIndex, sourceSpan);
|
|
}
|
|
function domElementContainer(slot, constIndex, localRefIndex, sourceSpan) {
|
|
return elementOrContainerBase(Identifiers.domElementContainer, slot,
|
|
/* tag */ null, constIndex, localRefIndex, sourceSpan);
|
|
}
|
|
function domElementContainerEnd() {
|
|
return call(Identifiers.domElementContainerEnd, [], null);
|
|
}
|
|
function domListener(name, handlerFn, eventTargetResolver, sourceSpan) {
|
|
const args = [literal(name), handlerFn];
|
|
if (eventTargetResolver !== null) {
|
|
args.push(importExpr(eventTargetResolver));
|
|
}
|
|
return call(Identifiers.domListener, args, sourceSpan);
|
|
}
|
|
function domTemplate(slot, templateFnRef, decls, vars, tag, constIndex, localRefs, sourceSpan) {
|
|
return templateBase(Identifiers.domTemplate, slot, templateFnRef, decls, vars, tag, constIndex, localRefs, 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) {
|
|
const interpolationArgs = collateInterpolationArgs(strings, expressions);
|
|
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 domProperty(name, expression, sanitizer, sourceSpan) {
|
|
return propertyBase(Identifiers.domProperty, name, expression, sanitizer, sourceSpan);
|
|
}
|
|
function animation(animationKind, handlerFn, sanitizer, sourceSpan) {
|
|
const args = [handlerFn];
|
|
if (sanitizer !== null) {
|
|
args.push(sanitizer);
|
|
}
|
|
const identifier = animationKind === "enter" /* ir.AnimationKind.ENTER */
|
|
? Identifiers.animationEnter
|
|
: Identifiers.animationLeave;
|
|
return call(identifier, args, sourceSpan);
|
|
}
|
|
function animationString(animationKind, expression, sanitizer, sourceSpan) {
|
|
const value = expression instanceof Interpolation
|
|
? interpolationToExpression(expression, sourceSpan)
|
|
: expression;
|
|
const args = [value];
|
|
if (sanitizer !== null) {
|
|
args.push(sanitizer);
|
|
}
|
|
const identifier = animationKind === "enter" /* ir.AnimationKind.ENTER */
|
|
? Identifiers.animationEnter
|
|
: Identifiers.animationLeave;
|
|
return call(identifier, args, sourceSpan);
|
|
}
|
|
function animationListener(animationKind, handlerFn, eventTargetResolver, sourceSpan) {
|
|
const args = [handlerFn];
|
|
const identifier = animationKind === "enter" /* ir.AnimationKind.ENTER */
|
|
? Identifiers.animationEnterListener
|
|
: Identifiers.animationLeaveListener;
|
|
return call(identifier, 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);
|
|
}
|
|
function attachSourceLocation(templatePath, locations) {
|
|
return call(Identifiers.attachSourceLocations, [literal(templatePath), locations], 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 interpolationToExpression(interpolation, sourceSpan) {
|
|
const interpolationArgs = collateInterpolationArgs(interpolation.strings, interpolation.expressions);
|
|
return callVariadicInstructionExpr(VALUE_INTERPOLATE_CONFIG, [], interpolationArgs, [], sourceSpan);
|
|
}
|
|
function call(instruction, args, sourceSpan) {
|
|
const expr = importExpr(instruction).callFn(args, sourceSpan);
|
|
return createStatementOp(new ExpressionStatement(expr, sourceSpan));
|
|
}
|
|
function conditional(condition, contextValue, sourceSpan) {
|
|
const args = [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;
|
|
},
|
|
};
|
|
const VALUE_INTERPOLATE_CONFIG = {
|
|
constant: [
|
|
Identifiers.interpolate,
|
|
Identifiers.interpolate1,
|
|
Identifiers.interpolate2,
|
|
Identifiers.interpolate3,
|
|
Identifiers.interpolate4,
|
|
Identifiers.interpolate5,
|
|
Identifiers.interpolate6,
|
|
Identifiers.interpolate7,
|
|
Identifiers.interpolate8,
|
|
],
|
|
variable: Identifiers.interpolateV,
|
|
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) {
|
|
// mapping need to be done before potentially dropping the last interpolation argument
|
|
const n = config.mapping(interpolationArgs.length);
|
|
// In the case the interpolation instruction ends with a empty string we drop it
|
|
// And the runtime will take care of it.
|
|
const lastInterpolationArg = interpolationArgs.at(-1);
|
|
if (extraArgs.length === 0 &&
|
|
interpolationArgs.length > 1 &&
|
|
lastInterpolationArg instanceof LiteralExpr &&
|
|
lastInterpolationArg.value === '') {
|
|
interpolationArgs.pop();
|
|
}
|
|
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 = new Map([
|
|
['window', Identifiers.resolveWindow],
|
|
['document', Identifiers.resolveDocument],
|
|
['body', Identifiers.resolveBody],
|
|
]);
|
|
/**
|
|
* DOM properties that need to be remapped on the compiler side.
|
|
* Note: this mapping has to be kept in sync with the equally named mapping in the runtime.
|
|
*/
|
|
const DOM_PROPERTY_REMAPPING = new Map([
|
|
['class', 'className'],
|
|
['for', 'htmlFor'],
|
|
['formaction', 'formAction'],
|
|
['innerHtml', 'innerHTML'],
|
|
['readonly', 'readOnly'],
|
|
['tabindex', 'tabIndex'],
|
|
]);
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
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, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElementStart(op.handle.slot, op.tag, op.attributes, op.localRefs, op.startSourceSpan)
|
|
: elementStart(op.handle.slot, op.tag, op.attributes, op.localRefs, op.startSourceSpan));
|
|
break;
|
|
case OpKind.Element:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElement(op.handle.slot, op.tag, op.attributes, op.localRefs, op.wholeSourceSpan)
|
|
: element(op.handle.slot, op.tag, op.attributes, op.localRefs, op.wholeSourceSpan));
|
|
break;
|
|
case OpKind.ElementEnd:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElementEnd(op.sourceSpan)
|
|
: elementEnd(op.sourceSpan));
|
|
break;
|
|
case OpKind.ContainerStart:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElementContainerStart(op.handle.slot, op.attributes, op.localRefs, op.startSourceSpan)
|
|
: elementContainerStart(op.handle.slot, op.attributes, op.localRefs, op.startSourceSpan));
|
|
break;
|
|
case OpKind.Container:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElementContainer(op.handle.slot, op.attributes, op.localRefs, op.wholeSourceSpan)
|
|
: elementContainer(op.handle.slot, op.attributes, op.localRefs, op.wholeSourceSpan));
|
|
break;
|
|
case OpKind.ContainerEnd:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domElementContainerEnd()
|
|
: 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,
|
|
// Block templates can't have directives so we can always generate them as DOM-only.
|
|
op.templateKind === TemplateKind.Block ||
|
|
unit.job.mode === TemplateCompilationMode.DomOnly
|
|
? domTemplate(op.handle.slot, variable(childView.fnName), childView.decls, childView.vars, op.tag, op.attributes, op.localRefs, op.startSourceSpan)
|
|
: 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.DeclareLet:
|
|
OpList.replace(op, declareLet(op.handle.slot, op.sourceSpan));
|
|
break;
|
|
case OpKind.AnimationString:
|
|
OpList.replace(op, animationString(op.animationKind, op.expression, op.sanitizer, op.sourceSpan));
|
|
break;
|
|
case OpKind.Animation:
|
|
const animationCallbackFn = reifyListenerHandler(unit, op.handlerFnName, op.handlerOps,
|
|
/* consumesDollarEvent */ false);
|
|
OpList.replace(op, animation(op.animationKind, animationCallbackFn, op.sanitizer, op.sourceSpan));
|
|
break;
|
|
case OpKind.AnimationListener:
|
|
const animationListenerFn = reifyListenerHandler(unit, op.handlerFnName, op.handlerOps, op.consumesDollarEvent);
|
|
OpList.replace(op, animationListener(op.animationKind, animationListenerFn, null, op.sourceSpan));
|
|
break;
|
|
case OpKind.Listener:
|
|
const listenerFn = reifyListenerHandler(unit, op.handlerFnName, op.handlerOps, op.consumesDollarEvent);
|
|
const eventTargetResolver = op.eventTarget
|
|
? GLOBAL_TARGET_RESOLVERS.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, unit.job.mode === TemplateCompilationMode.DomOnly &&
|
|
!op.hostListener &&
|
|
!op.isLegacyAnimationListener
|
|
? domListener(op.name, listenerFn, eventTargetResolver, op.sourceSpan)
|
|
: listener(op.name, listenerFn, eventTargetResolver, op.hostListener && op.isLegacyAnimationListener, op.sourceSpan));
|
|
break;
|
|
case OpKind.TwoWayListener:
|
|
OpList.replace(op, twoWayListener(op.name, reifyListenerHandler(unit, op.handlerFnName, op.handlerOps, true), 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, op.flags));
|
|
break;
|
|
case OpKind.DeferOn:
|
|
let args = [];
|
|
switch (op.trigger.kind) {
|
|
case DeferTriggerKind.Never:
|
|
case DeferTriggerKind.Idle:
|
|
case DeferTriggerKind.Immediate:
|
|
break;
|
|
case DeferTriggerKind.Timer:
|
|
args = [op.trigger.delay];
|
|
break;
|
|
case DeferTriggerKind.Interaction:
|
|
case DeferTriggerKind.Hover:
|
|
case DeferTriggerKind.Viewport:
|
|
// `hydrate` triggers don't support targets.
|
|
if (op.modifier === "hydrate" /* ir.DeferOpModifierKind.HYDRATE */) {
|
|
args = [];
|
|
}
|
|
else {
|
|
// The slots not being defined at this point is invalid, however we
|
|
// catch it during type checking. Pass in null in such cases.
|
|
args = [op.trigger.targetSlot?.slot ?? null];
|
|
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.modifier, 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');
|
|
}
|
|
let fallbackViewFnName = null;
|
|
let fallbackDecls = null;
|
|
let fallbackVars = null;
|
|
if (op.fallbackView !== null) {
|
|
if (!(unit instanceof ViewCompilationUnit)) {
|
|
throw new Error(`AssertionError: must be compiling a component`);
|
|
}
|
|
const fallbackView = unit.job.views.get(op.fallbackView);
|
|
if (fallbackView === undefined) {
|
|
throw new Error('AssertionError: projection had fallback view xref, but fallback view was not found');
|
|
}
|
|
if (fallbackView.fnName === null ||
|
|
fallbackView.decls === null ||
|
|
fallbackView.vars === null) {
|
|
throw new Error(`AssertionError: expected projection fallback view to have been named and counted`);
|
|
}
|
|
fallbackViewFnName = fallbackView.fnName;
|
|
fallbackDecls = fallbackView.decls;
|
|
fallbackVars = fallbackView.vars;
|
|
}
|
|
OpList.replace(op, projection(op.handle.slot, op.projectionSlotIndex, op.attributes, fallbackViewFnName, fallbackDecls, fallbackVars, op.sourceSpan));
|
|
break;
|
|
case OpKind.ConditionalCreate:
|
|
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 conditionalCreateChildView = unit.job.views.get(op.xref);
|
|
OpList.replace(op, conditionalCreate(op.handle.slot, variable(conditionalCreateChildView.fnName), conditionalCreateChildView.decls, conditionalCreateChildView.vars, op.tag, op.attributes, op.localRefs, op.startSourceSpan));
|
|
break;
|
|
case OpKind.ConditionalBranchCreate:
|
|
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 conditionalBranchCreateChildView = unit.job.views.get(op.xref);
|
|
OpList.replace(op, conditionalBranchCreate(op.handle.slot, variable(conditionalBranchCreateChildView.fnName), conditionalBranchCreateChildView.decls, conditionalBranchCreateChildView.vars, op.tag, op.attributes, op.localRefs, op.startSourceSpan));
|
|
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, reifyTrackBy(unit, op), op.usesComponentInstance, emptyViewFnName, emptyDecls, emptyVars, op.emptyTag, op.emptyAttributes, op.wholeSourceSpan));
|
|
break;
|
|
case OpKind.SourceLocation:
|
|
const locationsLiteral = literalArr(op.locations.map(({ targetSlot, offset, line, column }) => {
|
|
if (targetSlot.slot === null) {
|
|
throw new Error('No slot was assigned for source location');
|
|
}
|
|
return literalArr([
|
|
literal(targetSlot.slot),
|
|
literal(offset),
|
|
literal(line),
|
|
literal(column),
|
|
]);
|
|
}));
|
|
OpList.replace(op, attachSourceLocation(op.templatePath, locationsLiteral));
|
|
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:
|
|
OpList.replace(op, unit.job.mode === TemplateCompilationMode.DomOnly &&
|
|
op.bindingKind !== BindingKind.LegacyAnimation &&
|
|
op.bindingKind !== BindingKind.Animation
|
|
? reifyDomProperty(op)
|
|
: reifyProperty(op));
|
|
break;
|
|
case OpKind.TwoWayProperty:
|
|
OpList.replace(op, twoWayProperty(op.name, op.expression, op.sanitizer, op.sourceSpan));
|
|
break;
|
|
case OpKind.StyleProp:
|
|
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:
|
|
OpList.replace(op, styleMap(op.expression, op.sourceSpan));
|
|
break;
|
|
case OpKind.ClassMap:
|
|
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:
|
|
OpList.replace(op, attribute(op.name, op.expression, op.sanitizer, op.namespace, op.sourceSpan));
|
|
break;
|
|
case OpKind.DomProperty:
|
|
if (op.expression instanceof Interpolation) {
|
|
throw new Error('not yet handled');
|
|
}
|
|
else {
|
|
if (op.bindingKind === BindingKind.LegacyAnimation ||
|
|
op.bindingKind === BindingKind.Animation) {
|
|
OpList.replace(op, syntheticHostProperty(op.name, op.expression, op.sourceSpan));
|
|
}
|
|
else {
|
|
OpList.replace(op, reifyDomProperty(op));
|
|
}
|
|
}
|
|
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.`);
|
|
}
|
|
OpList.replace(op, conditional(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.modifier, op.expr, op.sourceSpan));
|
|
break;
|
|
case OpKind.StoreLet:
|
|
throw new Error(`AssertionError: unexpected storeLet ${op.declaredName}`);
|
|
case OpKind.Statement:
|
|
// Pass statement operations directly through.
|
|
break;
|
|
default:
|
|
throw new Error(`AssertionError: Unsupported reification of update op ${OpKind[op.kind]}`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Reifies a DOM property binding operation.
|
|
*
|
|
* This is an optimized version of {@link reifyProperty} that avoids unnecessarily trying to bind
|
|
* to directive inputs at runtime for views that don't import any directives.
|
|
*
|
|
* @param op A property binding operation.
|
|
* @returns A statement to update the property at runtime.
|
|
*/
|
|
function reifyDomProperty(op) {
|
|
return domProperty(DOM_PROPERTY_REMAPPING.get(op.name) ?? op.name, op.expression, op.sanitizer, op.sourceSpan);
|
|
}
|
|
/**
|
|
* Reifies a property binding operation.
|
|
*
|
|
* The returned statement attempts to bind to directive inputs before falling back to a DOM
|
|
* property.
|
|
*
|
|
* @param op A property binding operation.
|
|
* @returns A statement to update the property at runtime.
|
|
*/
|
|
function reifyProperty(op) {
|
|
return isAriaAttribute(op.name)
|
|
? ariaProperty(op.name, op.expression, op.sourceSpan)
|
|
: property(op.name, op.expression, op.sanitizer, op.sourceSpan);
|
|
}
|
|
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.TwoWayBindingSet:
|
|
throw new Error(`AssertionError: unresolved TwoWayBindingSet`);
|
|
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);
|
|
case ExpressionKind.ContextLetReference:
|
|
return readContextLet(expr.targetSlot.slot);
|
|
case ExpressionKind.StoreLet:
|
|
return storeLet(expr.value, expr.sourceSpan);
|
|
case ExpressionKind.TrackContext:
|
|
return variable('this');
|
|
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);
|
|
}
|
|
/** Reifies the tracking expression of a `RepeaterCreateOp`. */
|
|
function reifyTrackBy(unit, op) {
|
|
// If the tracking function was created already, there's nothing left to do.
|
|
if (op.trackByFn !== null) {
|
|
return op.trackByFn;
|
|
}
|
|
const params = [new FnParam('$index'), new FnParam('$item')];
|
|
let fn$1;
|
|
if (op.trackByOps === null) {
|
|
// If there are no additional ops related to the tracking function, we just need
|
|
// to turn it into a function that returns the result of the expression.
|
|
fn$1 = op.usesComponentInstance
|
|
? fn(params, [new ReturnStatement(op.track)])
|
|
: arrowFn(params, op.track);
|
|
}
|
|
else {
|
|
// Otherwise first we need to reify the track-related ops.
|
|
reifyUpdateOperations(unit, op.trackByOps);
|
|
const statements = [];
|
|
for (const trackOp of op.trackByOps) {
|
|
if (trackOp.kind !== OpKind.Statement) {
|
|
throw new Error(`AssertionError: expected reified statements, but found op ${OpKind[trackOp.kind]}`);
|
|
}
|
|
statements.push(trackOp.statement);
|
|
}
|
|
// Afterwards we can create the function from those ops.
|
|
fn$1 =
|
|
op.usesComponentInstance ||
|
|
statements.length !== 1 ||
|
|
!(statements[0] instanceof ReturnStatement)
|
|
? fn(params, statements)
|
|
: arrowFn(params, statements[0].value);
|
|
}
|
|
op.trackByFn = unit.job.pool.getSharedFunctionReference(fn$1, '_forTrack');
|
|
return op.trackByFn;
|
|
}
|
|
|
|
/**
|
|
* Binding 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* It's not allowed to access a `@let` declaration before it has been defined. This is enforced
|
|
* already via template type checking, however it can trip some of the assertions in the pipeline.
|
|
* E.g. the naming phase can fail because we resolved the variable here, but the variable doesn't
|
|
* exist anymore because the optimization phase removed it since it's invalid. To avoid surfacing
|
|
* confusing errors to users in the case where template type checking isn't running (e.g. in JIT
|
|
* mode) this phase detects illegal forward references and replaces them with `undefined`.
|
|
* Eventually users will see the proper error from the template type checker.
|
|
*/
|
|
function removeIllegalLetReferences(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
if (op.kind !== OpKind.Variable ||
|
|
op.variable.kind !== SemanticVariableKind.Identifier ||
|
|
!(op.initializer instanceof StoreLetExpr)) {
|
|
continue;
|
|
}
|
|
const name = op.variable.identifier;
|
|
let current = op;
|
|
while (current && current.kind !== OpKind.ListEnd) {
|
|
transformExpressionsInOp(current, (expr) => expr instanceof LexicalReadExpr && expr.name === name ? literal(undefined) : expr, VisitorContextFlag.None);
|
|
current = current.prev;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.Animation:
|
|
case OpKind.AnimationListener:
|
|
case OpKind.Listener:
|
|
case OpKind.TwoWayListener:
|
|
processLexicalScope$1(view, op.handlerOps);
|
|
break;
|
|
case OpKind.RepeaterCreate:
|
|
if (op.trackByOps !== null) {
|
|
processLexicalScope$1(view, op.trackByOps);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the dependency function of a deferred block.
|
|
*/
|
|
function resolveDeferDepsFns(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.Defer) {
|
|
if (op.resolverFn !== null) {
|
|
continue;
|
|
}
|
|
if (op.ownResolverFn !== null) {
|
|
if (op.handle.slot === null) {
|
|
throw new Error('AssertionError: slot must be assigned before extracting defer deps functions');
|
|
}
|
|
const fullPathName = unit.fnName?.replace('_Template', '');
|
|
op.resolverFn = job.pool.getSharedFunctionReference(op.ownResolverFn, `${fullPathName}_Defer_${op.handle.slot}_DepsFn`,
|
|
/* Don't use unique names for TDB compatibility */ false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.create);
|
|
transformDollarEvent(unit.update);
|
|
}
|
|
}
|
|
function transformDollarEvent(ops) {
|
|
for (const op of ops) {
|
|
if (op.kind === OpKind.Listener ||
|
|
op.kind === OpKind.TwoWayListener ||
|
|
op.kind === OpKind.AnimationListener) {
|
|
transformExpressionsInOp(op, (expr) => {
|
|
if (expr instanceof LexicalReadExpr && expr.name === '$event') {
|
|
// Two-way listeners always consume `$event` so they omit this field.
|
|
if (op.kind === OpKind.Listener || op.kind === OpKind.AnimationListener) {
|
|
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;
|
|
}
|
|
if (op.fallbackView !== null) {
|
|
const view = job.views.get(op.fallbackView);
|
|
if (op.fallbackViewI18nPlaceholder === undefined) {
|
|
resolvePlaceholdersForView(job, view, i18nContexts, elements);
|
|
}
|
|
else {
|
|
if (currentOps === null) {
|
|
throw Error('i18n tag placeholder should only occur inside an i18n block');
|
|
}
|
|
recordTemplateStart(job, view, op.handle.slot, op.fallbackViewI18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
|
|
resolvePlaceholdersForView(job, view, i18nContexts, elements);
|
|
recordTemplateClose(job, view, op.handle.slot, op.fallbackViewI18nPlaceholder, currentOps.i18nContext, currentOps.i18nBlock, pendingStructuralDirective);
|
|
pendingStructuralDirective = undefined;
|
|
}
|
|
}
|
|
break;
|
|
case OpKind.ConditionalCreate:
|
|
case OpKind.ConditionalBranchCreate:
|
|
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 { 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 subTemplateIndices = 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:
|
|
subTemplateIndices.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 = subTemplateIndices.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();
|
|
// Symbols defined within the current scope. They take precedence over ones defined outside.
|
|
const localDefinitions = 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:
|
|
if (op.variable.local) {
|
|
if (localDefinitions.has(op.variable.identifier)) {
|
|
continue;
|
|
}
|
|
localDefinitions.set(op.variable.identifier, op.xref);
|
|
}
|
|
else if (scope.has(op.variable.identifier)) {
|
|
continue;
|
|
}
|
|
scope.set(op.variable.identifier, op.xref);
|
|
break;
|
|
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.Animation:
|
|
case OpKind.AnimationListener:
|
|
case OpKind.Listener:
|
|
case OpKind.TwoWayListener:
|
|
// Listener functions have separate variable declarations, so process them as a separate
|
|
// lexical scope.
|
|
processLexicalScope(unit, op.handlerOps, savedView);
|
|
break;
|
|
case OpKind.RepeaterCreate:
|
|
if (op.trackByOps !== null) {
|
|
processLexicalScope(unit, op.trackByOps, 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 ||
|
|
op.kind === OpKind.TwoWayListener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener) {
|
|
// Listeners were already processed above with their own scopes.
|
|
continue;
|
|
}
|
|
transformExpressionsInOp(op, (expr) => {
|
|
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 (localDefinitions.has(expr.name)) {
|
|
return new ReadVariableExpr(localDefinitions.get(expr.name));
|
|
}
|
|
else 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.DomProperty:
|
|
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 <iframe> attributes (and that the current element is actually an
|
|
// <iframe>).
|
|
if (op.sanitizer === null) {
|
|
let isIframe = false;
|
|
if (job.kind === CompilationJobKind.Host || op.kind === OpKind.DomProperty) {
|
|
// Note: for host bindings defined on a directive, we do not try to find all
|
|
// possible places where it can be matched, so we can not determine whether
|
|
// the host element is an <iframe>. In this case, we just assume it is and append a
|
|
// validation function, which is invoked at runtime and would have access to the
|
|
// underlying DOM element to check if it's an <iframe> and if so - run extra checks.
|
|
isIframe = true;
|
|
}
|
|
else {
|
|
// For a normal binding we can just check if the element its on is an iframe.
|
|
const ownerOp = elements.get(op.target);
|
|
if (ownerOp === undefined || !isElementOrContainerOp(ownerOp)) {
|
|
throw Error('Property should have an element-like owner');
|
|
}
|
|
isIframe = isIframeElement(ownerOp);
|
|
}
|
|
if (isIframe && isIframeSecuritySensitiveAttr(op.name)) {
|
|
op.sanitizer = importExpr(Identifiers.validateIframeAttribute);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Checks whether the given op represents an iframe element.
|
|
*/
|
|
function isIframeElement(op) {
|
|
return op.kind === OpKind.ElementStart && op.tag?.toLowerCase() === 'iframe';
|
|
}
|
|
/**
|
|
* Asserts that there is only a single security context and returns it.
|
|
*/
|
|
function getOnlySecurityContext(securityContext) {
|
|
if (Array.isArray(securityContext)) {
|
|
if (securityContext.length > 1) {
|
|
// TODO: What should we do here? TDB just took the first one, but this feels like something we
|
|
// would want to know about and create a special case for like we did for Url/ResourceUrl. My
|
|
// guess is that, outside of the Url/ResourceUrl case, this never actually happens. If there
|
|
// do turn out to be other cases, throwing an error until we can address it feels safer.
|
|
throw Error(`AssertionError: Ambiguous security context`);
|
|
}
|
|
return securityContext[0] || SecurityContext.NONE;
|
|
}
|
|
return securityContext;
|
|
}
|
|
|
|
/**
|
|
* When inside of a listener, we may need access to one or more enclosing views. Therefore, each
|
|
* view should save the current view, and each listener must have the ability to restore the
|
|
* appropriate view. We eagerly generate all save view variables; they will be optimized away later.
|
|
*/
|
|
function saveAndRestoreView(job) {
|
|
for (const unit of job.units) {
|
|
unit.create.prepend([
|
|
createVariableOp(unit.job.allocateXrefId(), {
|
|
kind: SemanticVariableKind.SavedView,
|
|
name: null,
|
|
view: unit.xref,
|
|
}, new GetCurrentViewExpr(), VariableFlags.None),
|
|
]);
|
|
for (const op of unit.create) {
|
|
if (op.kind !== OpKind.Listener &&
|
|
op.kind !== OpKind.TwoWayListener &&
|
|
op.kind !== OpKind.Animation &&
|
|
op.kind !== OpKind.AnimationListener) {
|
|
continue;
|
|
}
|
|
// Embedded views always need the save/restore view operation.
|
|
let needsRestoreView = unit !== job.root;
|
|
if (!needsRestoreView) {
|
|
for (const handlerOp of op.handlerOps) {
|
|
visitExpressionsInOp(handlerOp, (expr) => {
|
|
if (expr instanceof ReferenceExpr || expr instanceof ContextLetReferenceExpr) {
|
|
// Listeners that reference() a local ref need the save/restore view operation.
|
|
needsRestoreView = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (needsRestoreView) {
|
|
addSaveRestoreViewOperationToListener(unit, op);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function addSaveRestoreViewOperationToListener(unit, op) {
|
|
op.handlerOps.prepend([
|
|
createVariableOp(unit.job.allocateXrefId(), {
|
|
kind: SemanticVariableKind.Context,
|
|
name: null,
|
|
view: unit.xref,
|
|
}, new RestoreViewExpr(unit.xref), VariableFlags.None),
|
|
]);
|
|
// The "restore view" operation in listeners requires a call to `resetView` to reset the
|
|
// context prior to returning from the listener operation. Find any `return` statements in
|
|
// the listener body and wrap them in a call to reset the view.
|
|
for (const handlerOp of op.handlerOps) {
|
|
if (handlerOp.kind === OpKind.Statement &&
|
|
handlerOp.statement instanceof ReturnStatement) {
|
|
handlerOp.statement.value = new ResetViewExpr(handlerOp.statement.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign data slots for all operations which implement `ConsumesSlotOpTrait`, and propagate the
|
|
* assigned data slots of those operations to any expressions which reference them via
|
|
* `UsesSlotIndexTrait`.
|
|
*
|
|
* This phase is also responsible for counting the number of slots used for each view (its `decls`)
|
|
* and propagating that number into the `Template` operations which declare embedded views.
|
|
*/
|
|
function allocateSlots(job) {
|
|
// Map of all declarations in all views within the component which require an assigned slot index.
|
|
// This map needs to be global (across all views within the component) since it's possible to
|
|
// reference a slot from one view from an expression within another (e.g. local references work
|
|
// this way).
|
|
const slotMap = new Map();
|
|
// Process all views in the component and assign slot indexes.
|
|
for (const unit of job.units) {
|
|
// Slot indices start at 0 for each view (and are not unique between views).
|
|
let slotCount = 0;
|
|
for (const op of unit.create) {
|
|
// Only consider declarations which consume data slots.
|
|
if (!hasConsumesSlotTrait(op)) {
|
|
continue;
|
|
}
|
|
// Assign slots to this declaration starting at the current `slotCount`.
|
|
op.handle.slot = slotCount;
|
|
// And track its assigned slot in the `slotMap`.
|
|
slotMap.set(op.xref, op.handle.slot);
|
|
// Each declaration may use more than 1 slot, so increment `slotCount` to reserve the number
|
|
// of slots required.
|
|
slotCount += op.numSlotsUsed;
|
|
}
|
|
// Record the total number of slots used on the view itself. This will later be propagated into
|
|
// `ir.TemplateOp`s which declare those views (except for the root view).
|
|
unit.decls = slotCount;
|
|
}
|
|
// After slot assignment, `slotMap` now contains slot assignments for every declaration in the
|
|
// whole template, across all views. Next, look for expressions which implement
|
|
// `UsesSlotIndexExprTrait` and propagate the assigned slot indexes into them.
|
|
// Additionally, this second scan allows us to find `ir.TemplateOp`s which declare views and
|
|
// propagate the number of slots used for each view into the operation which declares it.
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
if (op.kind === OpKind.Template ||
|
|
op.kind === OpKind.ConditionalCreate ||
|
|
op.kind === OpKind.ConditionalBranchCreate ||
|
|
op.kind === OpKind.RepeaterCreate) {
|
|
// Record the number of slots used by the view this `ir.TemplateOp` declares in the
|
|
// operation itself, so it can be emitted later.
|
|
const childView = job.views.get(op.xref);
|
|
op.decls = childView.decls;
|
|
// TODO: currently we handle the decls for the RepeaterCreate empty template in the reify
|
|
// phase. We should handle that here instead.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
/**
|
|
* Removes any `storeLet` calls that aren't referenced outside of the current view.
|
|
*/
|
|
function optimizeStoreLet(job) {
|
|
const letUsedExternally = new Set();
|
|
const declareLetOps = new Map();
|
|
// Since `@let` declarations can be referenced in child views, both in
|
|
// the creation block (via listeners) and in the update block, we have
|
|
// to look through all the ops to find the references.
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
// Take advantage that we're already looking through all the ops and track some more info.
|
|
if (op.kind === OpKind.DeclareLet) {
|
|
declareLetOps.set(op.xref, op);
|
|
}
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (expr instanceof ContextLetReferenceExpr) {
|
|
letUsedExternally.add(expr.target);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
transformExpressionsInOp(op, (expr) => {
|
|
// If a @let isn't used in other views, we don't have to store its value.
|
|
if (expr instanceof StoreLetExpr && !letUsedExternally.has(expr.target)) {
|
|
// Furthermore, if the @let isn't using pipes, we can also drop its declareLet op.
|
|
// We need to keep the declareLet if there are pipes, because they can use DI which
|
|
// requires the TNode created by declareLet.
|
|
if (!hasPipe(expr)) {
|
|
OpList.remove(declareLetOps.get(expr.target));
|
|
}
|
|
return expr.value;
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
}
|
|
}
|
|
}
|
|
/** Determines if a `storeLet` expression contains a pipe. */
|
|
function hasPipe(root) {
|
|
let result = false;
|
|
transformExpressionsInExpression(root, (expr) => {
|
|
if (expr instanceof PipeBindingExpr || expr instanceof PipeBindingVariadicExpr) {
|
|
result = true;
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* In most cases we can drop user added parentheses from expressions. However, in some cases
|
|
* parentheses are needed for the expression to be considered valid JavaScript or for Typescript to
|
|
* generate the correct output. This phases strips all parentheses except in the following
|
|
* siturations where they are required:
|
|
*
|
|
* 1. Unary operators in the base of an exponentiation expression. For example, `-2 ** 3` is not
|
|
* valid JavaScript, but `(-2) ** 3` is.
|
|
*
|
|
* 2. When mixing nullish coalescing (`??`) and logical and/or operators (`&&`, `||`), we need
|
|
* parentheses. For example, `a ?? b && c` is not valid JavaScript, but `a ?? (b && c)` is.
|
|
* Note: Because of the outcome of https://github.com/microsoft/TypeScript/issues/62307
|
|
* We need (for now) to keep parentheses around the `??` operator when it is used with and/or operators.
|
|
* For example, `a ?? b && c` is not valid JavaScript, but `(a ?? b) && c` is.
|
|
*
|
|
* 3. Ternary expression used as an operand for nullish coalescing. Typescript generates incorrect
|
|
* code if the parentheses are missing. For example when `(a ? b : c) ?? d` is translated to
|
|
* typescript AST, the parentheses node is removed, and then the remaining AST is printed, it
|
|
* incorrectly prints `a ? b : c ?? d`. This is different from how it handles the same situation
|
|
* with `||` and `&&` where it prints the parentheses even if they are not present in the AST.
|
|
* Note: We may be able to remove this case if Typescript resolves the following issue:
|
|
* https://github.com/microsoft/TypeScript/issues/61369
|
|
*/
|
|
function stripNonrequiredParentheses(job) {
|
|
// Check which parentheses are required.
|
|
const requiredParens = new Set();
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (expr instanceof BinaryOperatorExpr) {
|
|
switch (expr.operator) {
|
|
case BinaryOperator.Exponentiation:
|
|
checkExponentiationParens(expr, requiredParens);
|
|
break;
|
|
case BinaryOperator.NullishCoalesce:
|
|
checkNullishCoalescingParens(expr, requiredParens);
|
|
break;
|
|
// these 2 cases can be dropped if the regression introduced in 5.9.2 is fixed
|
|
// see https://github.com/microsoft/TypeScript/issues/62307
|
|
case BinaryOperator.And:
|
|
case BinaryOperator.Or:
|
|
checkAndOrParens(expr, requiredParens);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Remove any non-required parentheses.
|
|
for (const unit of job.units) {
|
|
for (const op of unit.ops()) {
|
|
transformExpressionsInOp(op, (expr) => {
|
|
if (expr instanceof ParenthesizedExpr) {
|
|
return requiredParens.has(expr) ? expr : expr.expr;
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
}
|
|
}
|
|
}
|
|
function checkExponentiationParens(expr, requiredParens) {
|
|
if (expr.lhs instanceof ParenthesizedExpr && expr.lhs.expr instanceof UnaryOperatorExpr) {
|
|
requiredParens.add(expr.lhs);
|
|
}
|
|
}
|
|
function checkNullishCoalescingParens(expr, requiredParens) {
|
|
if (expr.lhs instanceof ParenthesizedExpr &&
|
|
(isLogicalAndOr(expr.lhs.expr) || expr.lhs.expr instanceof ConditionalExpr)) {
|
|
requiredParens.add(expr.lhs);
|
|
}
|
|
if (expr.rhs instanceof ParenthesizedExpr &&
|
|
(isLogicalAndOr(expr.rhs.expr) || expr.rhs.expr instanceof ConditionalExpr)) {
|
|
requiredParens.add(expr.rhs);
|
|
}
|
|
}
|
|
function checkAndOrParens(expr, requiredParens) {
|
|
if (expr.lhs instanceof ParenthesizedExpr &&
|
|
expr.lhs.expr instanceof BinaryOperatorExpr &&
|
|
expr.lhs.expr.operator === BinaryOperator.NullishCoalesce) {
|
|
requiredParens.add(expr.lhs);
|
|
}
|
|
}
|
|
function isLogicalAndOr(expr) {
|
|
return (expr instanceof BinaryOperatorExpr &&
|
|
(expr.operator === BinaryOperator.And || expr.operator === BinaryOperator.Or));
|
|
}
|
|
|
|
/**
|
|
* Transforms special-case bindings with 'style' or 'class' in their names. Must run before the
|
|
* main binding specialization pass.
|
|
*/
|
|
function specializeStyleBindings(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.update) {
|
|
if (op.kind !== OpKind.Binding) {
|
|
continue;
|
|
}
|
|
switch (op.bindingKind) {
|
|
case BindingKind.ClassName:
|
|
if (op.expression instanceof Interpolation) {
|
|
throw new Error(`Unexpected interpolation in ClassName binding`);
|
|
}
|
|
OpList.replace(op, createClassPropOp(op.target, op.name, op.expression, op.sourceSpan));
|
|
break;
|
|
case BindingKind.StyleProperty:
|
|
OpList.replace(op, createStylePropOp(op.target, op.name, op.expression, op.unit, op.sourceSpan));
|
|
break;
|
|
case BindingKind.Property:
|
|
case BindingKind.Template:
|
|
if (op.name === 'style') {
|
|
OpList.replace(op, createStyleMapOp(op.target, op.expression, op.sourceSpan));
|
|
}
|
|
else if (op.name === 'class') {
|
|
OpList.replace(op, createClassMapOp(op.target, op.expression, op.sourceSpan));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find all assignments and usages of temporary variables, which are linked to each other with cross
|
|
* references. Generate names for each cross-reference, and add a `DeclareVarStmt` to initialize
|
|
* them at the beginning of the update block.
|
|
*
|
|
* TODO: Sometimes, it will be possible to reuse names across different subexpressions. For example,
|
|
* in the double keyed read `a?.[f()]?.[f()]`, the two function calls have non-overlapping scopes.
|
|
* Implement an algorithm for reuse.
|
|
*/
|
|
function generateTemporaryVariables(job) {
|
|
for (const unit of job.units) {
|
|
unit.create.prepend(generateTemporaries(unit.create));
|
|
unit.update.prepend(generateTemporaries(unit.update));
|
|
}
|
|
}
|
|
function generateTemporaries(ops) {
|
|
let opCount = 0;
|
|
let generatedStatements = [];
|
|
// For each op, search for any variables that are assigned or read. For each variable, generate a
|
|
// name and produce a `DeclareVarStmt` to the beginning of the block.
|
|
for (const op of ops) {
|
|
// Identify the final time each temp var is read.
|
|
const finalReads = new Map();
|
|
visitExpressionsInOp(op, (expr, flag) => {
|
|
if (flag & VisitorContextFlag.InChildOperation) {
|
|
return;
|
|
}
|
|
if (expr instanceof ReadTemporaryExpr) {
|
|
finalReads.set(expr.xref, expr);
|
|
}
|
|
});
|
|
// Name the temp vars, accounting for the fact that a name can be reused after it has been
|
|
// read for the final time.
|
|
let count = 0;
|
|
const assigned = new Set();
|
|
const released = new Set();
|
|
const defs = new Map();
|
|
visitExpressionsInOp(op, (expr, flag) => {
|
|
if (flag & VisitorContextFlag.InChildOperation) {
|
|
return;
|
|
}
|
|
if (expr instanceof AssignTemporaryExpr) {
|
|
if (!assigned.has(expr.xref)) {
|
|
assigned.add(expr.xref);
|
|
// TODO: Exactly replicate the naming scheme used by `TemplateDefinitionBuilder`.
|
|
// It seems to rely on an expression index instead of an op index.
|
|
defs.set(expr.xref, `tmp_${opCount}_${count++}`);
|
|
}
|
|
assignName(defs, expr);
|
|
}
|
|
else if (expr instanceof ReadTemporaryExpr) {
|
|
if (finalReads.get(expr.xref) === expr) {
|
|
released.add(expr.xref);
|
|
count--;
|
|
}
|
|
assignName(defs, expr);
|
|
}
|
|
});
|
|
// Add declarations for the temp vars.
|
|
generatedStatements.push(...Array.from(new Set(defs.values())).map((name) => createStatementOp(new DeclareVarStmt(name))));
|
|
opCount++;
|
|
if (op.kind === OpKind.Listener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener ||
|
|
op.kind === OpKind.TwoWayListener) {
|
|
op.handlerOps.prepend(generateTemporaries(op.handlerOps));
|
|
}
|
|
else if (op.kind === OpKind.RepeaterCreate && op.trackByOps !== null) {
|
|
op.trackByOps.prepend(generateTemporaries(op.trackByOps));
|
|
}
|
|
}
|
|
return generatedStatements;
|
|
}
|
|
/**
|
|
* Assigns a name to the temporary variable in the given temporary variable expression.
|
|
*/
|
|
function assignName(names, expr) {
|
|
const name = names.get(expr.xref);
|
|
if (name === undefined) {
|
|
throw new Error(`Found xref with unassigned name: ${expr.xref}`);
|
|
}
|
|
expr.name = name;
|
|
}
|
|
|
|
/**
|
|
* `track` functions in `for` repeaters can sometimes be "optimized," i.e. transformed into inline
|
|
* expressions, in lieu of an external function call. For example, tracking by `$index` can be be
|
|
* optimized into an inline `trackByIndex` reference. This phase checks track expressions for
|
|
* optimizable cases.
|
|
*/
|
|
function optimizeTrackFns(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind !== OpKind.RepeaterCreate) {
|
|
continue;
|
|
}
|
|
if (op.track instanceof ReadVarExpr && op.track.name === '$index') {
|
|
// Top-level access of `$index` uses the built in `repeaterTrackByIndex`.
|
|
op.trackByFn = importExpr(Identifiers.repeaterTrackByIndex);
|
|
}
|
|
else if (op.track instanceof ReadVarExpr && op.track.name === '$item') {
|
|
// Top-level access of the item uses the built in `repeaterTrackByIdentity`.
|
|
op.trackByFn = importExpr(Identifiers.repeaterTrackByIdentity);
|
|
}
|
|
else if (isTrackByFunctionCall(job.root.xref, op.track)) {
|
|
// Mark the function as using the component instance to play it safe
|
|
// since the method might be using `this` internally (see #53628).
|
|
op.usesComponentInstance = true;
|
|
// Top-level method calls in the form of `fn($index, item)` can be passed in directly.
|
|
if (op.track.receiver.receiver.view === unit.xref) {
|
|
// TODO: this may be wrong
|
|
op.trackByFn = op.track.receiver;
|
|
}
|
|
else {
|
|
// This is a plain method call, but not in the component's root view.
|
|
// We need to get the component instance, and then call the method on it.
|
|
op.trackByFn = importExpr(Identifiers.componentInstance)
|
|
.callFn([])
|
|
.prop(op.track.receiver.name);
|
|
// Because the context is not avaiable (without a special function), we don't want to
|
|
// try to resolve it later. Let's get rid of it by overwriting the original track
|
|
// expression (which won't be used anyway).
|
|
op.track = op.trackByFn;
|
|
}
|
|
}
|
|
else {
|
|
// The track function could not be optimized.
|
|
// Replace context reads with a special IR expression, since context reads in a track
|
|
// function are emitted specially.
|
|
op.track = transformExpressionsInExpression(op.track, (expr) => {
|
|
if (expr instanceof PipeBindingExpr || expr instanceof PipeBindingVariadicExpr) {
|
|
throw new Error(`Illegal State: Pipes are not allowed in this context`);
|
|
}
|
|
else if (expr instanceof ContextExpr) {
|
|
op.usesComponentInstance = true;
|
|
return new TrackContextExpr(expr.view);
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
// Also create an OpList for the tracking expression since it may need
|
|
// additional ops when generating the final code (e.g. temporary variables).
|
|
const trackOpList = new OpList();
|
|
trackOpList.push(createStatementOp(new ReturnStatement(op.track, op.track.sourceSpan)));
|
|
op.trackByOps = trackOpList;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function isTrackByFunctionCall(rootView, expr) {
|
|
if (!(expr instanceof InvokeFunctionExpr) || expr.args.length === 0 || expr.args.length > 2) {
|
|
return false;
|
|
}
|
|
if (!(expr.receiver instanceof ReadPropExpr && expr.receiver.receiver instanceof ContextExpr) ||
|
|
expr.receiver.receiver.view !== rootView) {
|
|
return false;
|
|
}
|
|
const [arg0, arg1] = expr.args;
|
|
if (!(arg0 instanceof ReadVarExpr) || arg0.name !== '$index') {
|
|
return false;
|
|
}
|
|
else if (expr.args.length === 1) {
|
|
return true;
|
|
}
|
|
if (!(arg1 instanceof ReadVarExpr) || arg1.name !== '$item') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Inside the `track` expression on a `for` repeater, the `$index` and `$item` variables are
|
|
* ambiently available. In this phase, we find those variable usages, and replace them with the
|
|
* appropriate output read.
|
|
*/
|
|
function generateTrackVariables(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind !== OpKind.RepeaterCreate) {
|
|
continue;
|
|
}
|
|
op.track = transformExpressionsInExpression(op.track, (expr) => {
|
|
if (expr instanceof LexicalReadExpr) {
|
|
if (op.varNames.$index.has(expr.name)) {
|
|
return variable('$index');
|
|
}
|
|
else if (expr.name === op.varNames.$implicit) {
|
|
return variable('$item');
|
|
}
|
|
// TODO: handle prohibited context variables (emit as globals?)
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms a `TwoWayBindingSet` expression into an expression that either
|
|
* sets a value through the `twoWayBindingSet` instruction or falls back to setting
|
|
* the value directly. E.g. the expression `TwoWayBindingSet(target, value)` becomes:
|
|
* `ng.twoWayBindingSet(target, value) || (target = value)`.
|
|
*/
|
|
function transformTwoWayBindingSet(job) {
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.TwoWayListener) {
|
|
transformExpressionsInOp(op, (expr) => {
|
|
if (!(expr instanceof TwoWayBindingSetExpr)) {
|
|
return expr;
|
|
}
|
|
const { target, value } = expr;
|
|
if (target instanceof ReadPropExpr || target instanceof ReadKeyExpr) {
|
|
return twoWayBindingSet(target, value).or(target.set(value));
|
|
}
|
|
// ASSUMPTION: here we're assuming that `ReadVariableExpr` will be a reference
|
|
// to a local template variable. This appears to be the case at the time of writing.
|
|
// If the expression is targeting a variable read, we only emit the `twoWayBindingSet`
|
|
// since the fallback would be attempting to write into a constant. Invalid usages will be
|
|
// flagged during template type checking.
|
|
if (target instanceof ReadVariableExpr) {
|
|
return twoWayBindingSet(target, value);
|
|
}
|
|
throw new Error(`Unsupported expression in two-way action binding.`);
|
|
}, VisitorContextFlag.InChildOperation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Counts the number of variable slots used within each view, and stores that on the view itself, as
|
|
* well as propagates it to the `ir.TemplateOp` for embedded views.
|
|
*/
|
|
function countVariables(job) {
|
|
// First, count the vars used in each view, and update the view-level counter.
|
|
for (const unit of job.units) {
|
|
let varCount = 0;
|
|
// Count variables on top-level ops first. Don't explore nested expressions just yet.
|
|
for (const op of unit.ops()) {
|
|
if (hasConsumesVarsTrait(op)) {
|
|
varCount += varsUsedByOp(op);
|
|
}
|
|
}
|
|
// Count variables on expressions inside ops. We do this later because some of these expressions
|
|
// might be conditional (e.g. `pipeBinding` inside of a ternary), and we don't want to interfere
|
|
// with indices for top-level binding slots (e.g. `property`).
|
|
for (const op of unit.ops()) {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (!isIrExpression(expr)) {
|
|
return;
|
|
}
|
|
// TemplateDefinitionBuilder assigns variable offsets for everything but pure functions
|
|
// first, and then assigns offsets to pure functions lazily. We emulate that behavior by
|
|
// assigning offsets in two passes instead of one, only in compatibility mode.
|
|
if (job.compatibility === CompatibilityMode.TemplateDefinitionBuilder &&
|
|
expr instanceof PureFunctionExpr) {
|
|
return;
|
|
}
|
|
// Some expressions require knowledge of the number of variable slots consumed.
|
|
if (hasUsesVarOffsetTrait(expr)) {
|
|
expr.varOffset = varCount;
|
|
}
|
|
if (hasConsumesVarsTrait(expr)) {
|
|
varCount += varsUsedByIrExpression(expr);
|
|
}
|
|
});
|
|
}
|
|
// Compatibility mode pass for pure function offsets (as explained above).
|
|
if (job.compatibility === CompatibilityMode.TemplateDefinitionBuilder) {
|
|
for (const op of unit.ops()) {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (!isIrExpression(expr) || !(expr instanceof PureFunctionExpr)) {
|
|
return;
|
|
}
|
|
// Some expressions require knowledge of the number of variable slots consumed.
|
|
if (hasUsesVarOffsetTrait(expr)) {
|
|
expr.varOffset = varCount;
|
|
}
|
|
if (hasConsumesVarsTrait(expr)) {
|
|
varCount += varsUsedByIrExpression(expr);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
unit.vars = varCount;
|
|
}
|
|
if (job instanceof ComponentCompilationJob) {
|
|
// Add var counts for each view to the `ir.TemplateOp` which declares that view (if the view is
|
|
// an embedded view).
|
|
for (const unit of job.units) {
|
|
for (const op of unit.create) {
|
|
if (op.kind !== OpKind.Template &&
|
|
op.kind !== OpKind.RepeaterCreate &&
|
|
op.kind !== OpKind.ConditionalCreate &&
|
|
op.kind !== OpKind.ConditionalBranchCreate) {
|
|
continue;
|
|
}
|
|
const childView = job.views.get(op.xref);
|
|
op.vars = childView.vars;
|
|
// TODO: currently we handle the vars for the RepeaterCreate empty template in the reify
|
|
// phase. We should handle that here instead.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Different operations that implement `ir.UsesVarsTrait` use different numbers of variables, so
|
|
* count the variables used by any particular `op`.
|
|
*/
|
|
function varsUsedByOp(op) {
|
|
let slots;
|
|
switch (op.kind) {
|
|
case OpKind.Attribute:
|
|
// All of these bindings use 1 variable slot, plus 1 slot for every interpolated expression,
|
|
// if any.
|
|
slots = 1;
|
|
if (op.expression instanceof Interpolation && !isSingletonInterpolation(op.expression)) {
|
|
slots += op.expression.expressions.length;
|
|
}
|
|
return slots;
|
|
case OpKind.Property:
|
|
case OpKind.DomProperty:
|
|
slots = 1;
|
|
// We need to assign a slot even for singleton interpolations, because the
|
|
// runtime needs to store both the raw value and the stringified one.
|
|
if (op.expression instanceof Interpolation) {
|
|
slots += op.expression.expressions.length;
|
|
}
|
|
return slots;
|
|
case OpKind.TwoWayProperty:
|
|
// Two-way properties can only have expressions so they only need one variable slot.
|
|
return 1;
|
|
case OpKind.StyleProp:
|
|
case OpKind.ClassProp:
|
|
case OpKind.StyleMap:
|
|
case OpKind.ClassMap:
|
|
// Style & class bindings use 2 variable slots, plus 1 slot for every interpolated expression,
|
|
// if any.
|
|
slots = 2;
|
|
if (op.expression instanceof Interpolation) {
|
|
slots += op.expression.expressions.length;
|
|
}
|
|
return slots;
|
|
case OpKind.InterpolateText:
|
|
// `ir.InterpolateTextOp`s use a variable slot for each dynamic expression.
|
|
return op.interpolation.expressions.length;
|
|
case OpKind.I18nExpression:
|
|
case OpKind.Conditional:
|
|
case OpKind.DeferWhen:
|
|
case OpKind.StoreLet:
|
|
return 1;
|
|
case OpKind.RepeaterCreate:
|
|
// Repeaters may require an extra variable binding slot, if they have an empty view, for the
|
|
// empty block tracking.
|
|
// TODO: It's a bit odd to have a create mode instruction consume variable slots. Maybe we can
|
|
// find a way to use the Repeater update op instead.
|
|
return op.emptyView ? 1 : 0;
|
|
default:
|
|
throw new Error(`Unhandled op: ${OpKind[op.kind]}`);
|
|
}
|
|
}
|
|
function varsUsedByIrExpression(expr) {
|
|
switch (expr.kind) {
|
|
case ExpressionKind.PureFunctionExpr:
|
|
return 1 + expr.args.length;
|
|
case ExpressionKind.PipeBinding:
|
|
return 1 + expr.args.length;
|
|
case ExpressionKind.PipeBindingVariadic:
|
|
return 1 + expr.numArgs;
|
|
case ExpressionKind.StoreLet:
|
|
return 1;
|
|
default:
|
|
throw new Error(`AssertionError: unhandled ConsumesVarsTrait expression ${expr.constructor.name}`);
|
|
}
|
|
}
|
|
function isSingletonInterpolation(expr) {
|
|
if (expr.expressions.length !== 1 || expr.strings.length !== 2) {
|
|
return false;
|
|
}
|
|
if (expr.strings[0] !== '' || expr.strings[1] !== '') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Optimize variables declared and used in the IR.
|
|
*
|
|
* Variables are eagerly generated by pipeline stages for all possible values that could be
|
|
* referenced. This stage processes the list of declared variables and all variable usages,
|
|
* and optimizes where possible. It performs 3 main optimizations:
|
|
*
|
|
* * It transforms variable declarations to side effectful expressions when the
|
|
* variable is not used, but its initializer has global effects which other
|
|
* operations rely upon.
|
|
* * It removes variable declarations if those variables are not referenced and
|
|
* either they do not have global effects, or nothing relies on them.
|
|
* * It inlines variable declarations when those variables are only used once
|
|
* and the inlining is semantically safe.
|
|
*
|
|
* To guarantee correctness, analysis of "fences" in the instruction lists is used to determine
|
|
* which optimizations are safe to perform.
|
|
*/
|
|
function optimizeVariables(job) {
|
|
for (const unit of job.units) {
|
|
inlineAlwaysInlineVariables(unit.create);
|
|
inlineAlwaysInlineVariables(unit.update);
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.Listener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener ||
|
|
op.kind === OpKind.TwoWayListener) {
|
|
inlineAlwaysInlineVariables(op.handlerOps);
|
|
}
|
|
else if (op.kind === OpKind.RepeaterCreate && op.trackByOps !== null) {
|
|
inlineAlwaysInlineVariables(op.trackByOps);
|
|
}
|
|
}
|
|
optimizeVariablesInOpList(unit.create, job.compatibility);
|
|
optimizeVariablesInOpList(unit.update, job.compatibility);
|
|
for (const op of unit.create) {
|
|
if (op.kind === OpKind.Listener ||
|
|
op.kind === OpKind.Animation ||
|
|
op.kind === OpKind.AnimationListener ||
|
|
op.kind === OpKind.TwoWayListener) {
|
|
optimizeVariablesInOpList(op.handlerOps, job.compatibility);
|
|
}
|
|
else if (op.kind === OpKind.RepeaterCreate && op.trackByOps !== null) {
|
|
optimizeVariablesInOpList(op.trackByOps, job.compatibility);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* A [fence](https://en.wikipedia.org/wiki/Memory_barrier) flag for an expression which indicates
|
|
* how that expression can be optimized in relation to other expressions or instructions.
|
|
*
|
|
* `Fence`s are a bitfield, so multiple flags may be set on a single expression.
|
|
*/
|
|
var Fence;
|
|
(function (Fence) {
|
|
/**
|
|
* Empty flag (no fence exists).
|
|
*/
|
|
Fence[Fence["None"] = 0] = "None";
|
|
/**
|
|
* A context read fence, meaning that the expression in question reads from the "current view"
|
|
* context of the runtime.
|
|
*/
|
|
Fence[Fence["ViewContextRead"] = 1] = "ViewContextRead";
|
|
/**
|
|
* A context write fence, meaning that the expression in question writes to the "current view"
|
|
* context of the runtime.
|
|
*
|
|
* Note that all `ContextWrite` fences are implicitly `ContextRead` fences as operations which
|
|
* change the view context do so based on the current one.
|
|
*/
|
|
Fence[Fence["ViewContextWrite"] = 2] = "ViewContextWrite";
|
|
/**
|
|
* Indicates that a call is required for its side-effects, even if nothing reads its result.
|
|
*
|
|
* This is also true of `ViewContextWrite` operations **if** they are followed by a
|
|
* `ViewContextRead`.
|
|
*/
|
|
Fence[Fence["SideEffectful"] = 4] = "SideEffectful";
|
|
})(Fence || (Fence = {}));
|
|
function inlineAlwaysInlineVariables(ops) {
|
|
const vars = new Map();
|
|
for (const op of ops) {
|
|
if (op.kind === OpKind.Variable && op.flags & VariableFlags.AlwaysInline) {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (isIrExpression(expr) && fencesForIrExpression(expr) !== Fence.None) {
|
|
throw new Error(`AssertionError: A context-sensitive variable was marked AlwaysInline`);
|
|
}
|
|
});
|
|
vars.set(op.xref, op);
|
|
}
|
|
transformExpressionsInOp(op, (expr) => {
|
|
if (expr instanceof ReadVariableExpr && vars.has(expr.xref)) {
|
|
const varOp = vars.get(expr.xref);
|
|
// Inline by cloning, because we might inline into multiple places.
|
|
return varOp.initializer.clone();
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
}
|
|
for (const op of vars.values()) {
|
|
OpList.remove(op);
|
|
}
|
|
}
|
|
/**
|
|
* Process a list of operations and optimize variables within that list.
|
|
*/
|
|
function optimizeVariablesInOpList(ops, compatibility) {
|
|
const varDecls = new Map();
|
|
const varUsages = new Map();
|
|
// Track variables that are used outside of the immediate operation list. For example, within
|
|
// `ListenerOp` handler operations of listeners in the current operation list.
|
|
const varRemoteUsages = new Set();
|
|
const opMap = new Map();
|
|
// First, extract information about variables declared or used within the whole list.
|
|
for (const op of ops) {
|
|
if (op.kind === OpKind.Variable) {
|
|
if (varDecls.has(op.xref) || varUsages.has(op.xref)) {
|
|
throw new Error(`Should not see two declarations of the same variable: ${op.xref}`);
|
|
}
|
|
varDecls.set(op.xref, op);
|
|
varUsages.set(op.xref, 0);
|
|
}
|
|
opMap.set(op, collectOpInfo(op));
|
|
countVariableUsages(op, varUsages, varRemoteUsages);
|
|
}
|
|
// The next step is to remove any variable declarations for variables that aren't used. The
|
|
// variable initializer expressions may be side-effectful, so they may need to be retained as
|
|
// expression statements.
|
|
// Track whether we've seen an operation which reads from the view context yet. This is used to
|
|
// determine whether a write to the view context in a variable initializer can be observed.
|
|
let contextIsUsed = false;
|
|
// Note that iteration through the list happens in reverse, which guarantees that we'll process
|
|
// all reads of a variable prior to processing its declaration.
|
|
for (const op of ops.reversed()) {
|
|
const opInfo = opMap.get(op);
|
|
if (op.kind === OpKind.Variable && varUsages.get(op.xref) === 0) {
|
|
// This variable is unused and can be removed. We might need to keep the initializer around,
|
|
// though, if something depends on it running.
|
|
if ((contextIsUsed && opInfo.fences & Fence.ViewContextWrite) ||
|
|
opInfo.fences & Fence.SideEffectful) {
|
|
// This variable initializer has a side effect which must be retained. Either:
|
|
// * it writes to the view context, and we know there is a future operation which depends
|
|
// on that write, or
|
|
// * it's an operation which is inherently side-effectful.
|
|
// We can't remove the initializer, but we can remove the variable declaration itself and
|
|
// replace it with a side-effectful statement.
|
|
const stmtOp = createStatementOp(op.initializer.toStmt());
|
|
opMap.set(stmtOp, opInfo);
|
|
OpList.replace(op, stmtOp);
|
|
}
|
|
else {
|
|
// It's safe to delete this entire variable declaration as nothing depends on it, even
|
|
// side-effectfully. Note that doing this might make other variables unused. Since we're
|
|
// iterating in reverse order, we should always be processing usages before declarations
|
|
// and therefore by the time we get to a declaration, all removable usages will have been
|
|
// removed.
|
|
uncountVariableUsages(op, varUsages);
|
|
OpList.remove(op);
|
|
}
|
|
opMap.delete(op);
|
|
varDecls.delete(op.xref);
|
|
varUsages.delete(op.xref);
|
|
continue;
|
|
}
|
|
// Does this operation depend on the view context?
|
|
if (opInfo.fences & Fence.ViewContextRead) {
|
|
contextIsUsed = true;
|
|
}
|
|
}
|
|
// Next, inline any remaining variables with exactly one usage.
|
|
const toInline = [];
|
|
for (const [id, count] of varUsages) {
|
|
const decl = varDecls.get(id);
|
|
// We can inline variables that:
|
|
// - are used exactly once, and
|
|
// - are not used remotely
|
|
// OR
|
|
// - are marked for always inlining
|
|
const isAlwaysInline = !!(decl.flags & VariableFlags.AlwaysInline);
|
|
if (count !== 1 || isAlwaysInline) {
|
|
// We can't inline this variable as it's used more than once.
|
|
continue;
|
|
}
|
|
if (varRemoteUsages.has(id)) {
|
|
// This variable is used once, but across an operation boundary, so it can't be inlined.
|
|
continue;
|
|
}
|
|
toInline.push(id);
|
|
}
|
|
let candidate;
|
|
while ((candidate = toInline.pop())) {
|
|
// We will attempt to inline this variable. If inlining fails (due to fences for example),
|
|
// no future operation will make inlining legal.
|
|
const decl = varDecls.get(candidate);
|
|
const varInfo = opMap.get(decl);
|
|
const isAlwaysInline = !!(decl.flags & VariableFlags.AlwaysInline);
|
|
if (isAlwaysInline) {
|
|
throw new Error(`AssertionError: Found an 'AlwaysInline' variable after the always inlining pass.`);
|
|
}
|
|
// Scan operations following the variable declaration and look for the point where that variable
|
|
// is used. There should only be one usage given the precondition above.
|
|
for (let targetOp = decl.next; targetOp.kind !== OpKind.ListEnd; targetOp = targetOp.next) {
|
|
const opInfo = opMap.get(targetOp);
|
|
// Is the variable used in this operation?
|
|
if (opInfo.variablesUsed.has(candidate)) {
|
|
if (compatibility === CompatibilityMode.TemplateDefinitionBuilder &&
|
|
!allowConservativeInlining(decl, targetOp)) {
|
|
// We're in conservative mode, and this variable is not eligible for inlining into the
|
|
// target operation in this mode.
|
|
break;
|
|
}
|
|
// Yes, try to inline it. Inlining may not be successful if fences in this operation before
|
|
// the variable's usage cannot be safely crossed.
|
|
if (tryInlineVariableInitializer(candidate, decl.initializer, targetOp, varInfo.fences)) {
|
|
// Inlining was successful! Update the tracking structures to reflect the inlined
|
|
// variable.
|
|
opInfo.variablesUsed.delete(candidate);
|
|
// Add all variables used in the variable's initializer to its new usage site.
|
|
for (const id of varInfo.variablesUsed) {
|
|
opInfo.variablesUsed.add(id);
|
|
}
|
|
// Merge fences in the variable's initializer into its new usage site.
|
|
opInfo.fences |= varInfo.fences;
|
|
// Delete tracking info related to the declaration.
|
|
varDecls.delete(candidate);
|
|
varUsages.delete(candidate);
|
|
opMap.delete(decl);
|
|
// And finally, delete the original declaration from the operation list.
|
|
OpList.remove(decl);
|
|
}
|
|
// Whether inlining succeeded or failed, we're done processing this variable.
|
|
break;
|
|
}
|
|
// If the variable is not used in this operation, then we'd need to inline across it. Check if
|
|
// that's safe to do.
|
|
if (!safeToInlinePastFences(opInfo.fences, varInfo.fences)) {
|
|
// We can't safely inline this variable beyond this operation, so don't proceed with
|
|
// inlining this variable.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Given an `ir.Expression`, returns the `Fence` flags for that expression type.
|
|
*/
|
|
function fencesForIrExpression(expr) {
|
|
switch (expr.kind) {
|
|
case ExpressionKind.NextContext:
|
|
return Fence.ViewContextRead | Fence.ViewContextWrite;
|
|
case ExpressionKind.RestoreView:
|
|
return Fence.ViewContextRead | Fence.ViewContextWrite | Fence.SideEffectful;
|
|
case ExpressionKind.StoreLet:
|
|
return Fence.SideEffectful;
|
|
case ExpressionKind.Reference:
|
|
case ExpressionKind.ContextLetReference:
|
|
return Fence.ViewContextRead;
|
|
default:
|
|
return Fence.None;
|
|
}
|
|
}
|
|
/**
|
|
* Build the `OpInfo` structure for the given `op`. This performs two operations:
|
|
*
|
|
* * It tracks which variables are used in the operation's expressions.
|
|
* * It rolls up fence flags for expressions within the operation.
|
|
*/
|
|
function collectOpInfo(op) {
|
|
let fences = Fence.None;
|
|
const variablesUsed = new Set();
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (!isIrExpression(expr)) {
|
|
return;
|
|
}
|
|
switch (expr.kind) {
|
|
case ExpressionKind.ReadVariable:
|
|
variablesUsed.add(expr.xref);
|
|
break;
|
|
default:
|
|
fences |= fencesForIrExpression(expr);
|
|
}
|
|
});
|
|
return { fences, variablesUsed };
|
|
}
|
|
/**
|
|
* Count the number of usages of each variable, being careful to track whether those usages are
|
|
* local or remote.
|
|
*/
|
|
function countVariableUsages(op, varUsages, varRemoteUsage) {
|
|
visitExpressionsInOp(op, (expr, flags) => {
|
|
if (!isIrExpression(expr)) {
|
|
return;
|
|
}
|
|
if (expr.kind !== ExpressionKind.ReadVariable) {
|
|
return;
|
|
}
|
|
const count = varUsages.get(expr.xref);
|
|
if (count === undefined) {
|
|
// This variable is declared outside the current scope of optimization.
|
|
return;
|
|
}
|
|
varUsages.set(expr.xref, count + 1);
|
|
if (flags & VisitorContextFlag.InChildOperation) {
|
|
varRemoteUsage.add(expr.xref);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Remove usages of a variable in `op` from the `varUsages` tracking.
|
|
*/
|
|
function uncountVariableUsages(op, varUsages) {
|
|
visitExpressionsInOp(op, (expr) => {
|
|
if (!isIrExpression(expr)) {
|
|
return;
|
|
}
|
|
if (expr.kind !== ExpressionKind.ReadVariable) {
|
|
return;
|
|
}
|
|
const count = varUsages.get(expr.xref);
|
|
if (count === undefined) {
|
|
// This variable is declared outside the current scope of optimization.
|
|
return;
|
|
}
|
|
else if (count === 0) {
|
|
throw new Error(`Inaccurate variable count: ${expr.xref} - found another read but count is already 0`);
|
|
}
|
|
varUsages.set(expr.xref, count - 1);
|
|
});
|
|
}
|
|
/**
|
|
* Checks whether it's safe to inline a variable across a particular operation.
|
|
*
|
|
* @param fences the fences of the operation which the inlining will cross
|
|
* @param declFences the fences of the variable being inlined.
|
|
*/
|
|
function safeToInlinePastFences(fences, declFences) {
|
|
if (fences & Fence.ViewContextWrite) {
|
|
// It's not safe to inline context reads across context writes.
|
|
if (declFences & Fence.ViewContextRead) {
|
|
return false;
|
|
}
|
|
}
|
|
else if (fences & Fence.ViewContextRead) {
|
|
// It's not safe to inline context writes across context reads.
|
|
if (declFences & Fence.ViewContextWrite) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Attempt to inline the initializer of a variable into a target operation's expressions.
|
|
*
|
|
* This may or may not be safe to do. For example, the variable could be read following the
|
|
* execution of an expression with fences that don't permit the variable to be inlined across them.
|
|
*/
|
|
function tryInlineVariableInitializer(id, initializer, target, declFences) {
|
|
// We use `ir.transformExpressionsInOp` to walk the expressions and inline the variable if
|
|
// possible. Since this operation is callback-based, once inlining succeeds or fails we can't
|
|
// "stop" the expression processing, and have to keep track of whether inlining has succeeded or
|
|
// is no longer allowed.
|
|
let inlined = false;
|
|
let inliningAllowed = true;
|
|
transformExpressionsInOp(target, (expr, flags) => {
|
|
if (!isIrExpression(expr)) {
|
|
return expr;
|
|
}
|
|
if (inlined || !inliningAllowed) {
|
|
// Either the inlining has already succeeded, or we've passed a fence that disallows inlining
|
|
// at this point, so don't try.
|
|
return expr;
|
|
}
|
|
else if (flags & VisitorContextFlag.InChildOperation &&
|
|
declFences & Fence.ViewContextRead) {
|
|
// We cannot inline variables that are sensitive to the current context across operation
|
|
// boundaries.
|
|
return expr;
|
|
}
|
|
switch (expr.kind) {
|
|
case ExpressionKind.ReadVariable:
|
|
if (expr.xref === id) {
|
|
// This is the usage site of the variable. Since nothing has disallowed inlining, it's
|
|
// safe to inline the initializer here.
|
|
inlined = true;
|
|
return initializer;
|
|
}
|
|
break;
|
|
default:
|
|
// For other types of `ir.Expression`s, whether inlining is allowed depends on their fences.
|
|
const exprFences = fencesForIrExpression(expr);
|
|
inliningAllowed = inliningAllowed && safeToInlinePastFences(exprFences, declFences);
|
|
break;
|
|
}
|
|
return expr;
|
|
}, VisitorContextFlag.None);
|
|
return inlined;
|
|
}
|
|
/**
|
|
* Determines whether inlining of `decl` should be allowed in "conservative" mode.
|
|
*
|
|
* In conservative mode, inlining behavior is limited to those operations which the
|
|
* `TemplateDefinitionBuilder` supported, with the goal of producing equivalent output.
|
|
*/
|
|
function allowConservativeInlining(decl, target) {
|
|
// TODO(alxhub): understand exactly how TemplateDefinitionBuilder approaches inlining, and record
|
|
// that behavior here.
|
|
switch (decl.variable.kind) {
|
|
case SemanticVariableKind.Identifier:
|
|
if (decl.initializer instanceof ReadVarExpr && decl.initializer.name === 'ctx') {
|
|
// Although TemplateDefinitionBuilder is cautious about inlining, we still want to do so
|
|
// when the variable is the context, to imitate its behavior with aliases in control flow
|
|
// blocks. This quirky behavior will become dead code once compatibility mode is no longer
|
|
// supported.
|
|
return true;
|
|
}
|
|
return false;
|
|
case SemanticVariableKind.Context:
|
|
// Context can only be inlined into other variables.
|
|
return target.kind === OpKind.Variable;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wraps ICUs that do not already belong to an i18n block in a new i18n block.
|
|
*/
|
|
function wrapI18nIcus(job) {
|
|
for (const unit of job.units) {
|
|
let currentI18nOp = null;
|
|
let addedI18nId = null;
|
|
for (const op of unit.create) {
|
|
switch (op.kind) {
|
|
case OpKind.I18nStart:
|
|
currentI18nOp = op;
|
|
break;
|
|
case OpKind.I18nEnd:
|
|
currentI18nOp = null;
|
|
break;
|
|
case OpKind.IcuStart:
|
|
if (currentI18nOp === null) {
|
|
addedI18nId = job.allocateXrefId();
|
|
// ICU i18n start/end ops should not receive source spans.
|
|
OpList.insertBefore(createI18nStartOp(addedI18nId, op.message, undefined, null), op);
|
|
}
|
|
break;
|
|
case OpKind.IcuEnd:
|
|
if (addedI18nId !== null) {
|
|
OpList.insertAfter(createI18nEndOp(addedI18nId, null), op);
|
|
addedI18nId = null;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
const phases = [
|
|
{ kind: CompilationJobKind.Tmpl, fn: removeContentSelectors },
|
|
{ kind: CompilationJobKind.Host, fn: parseHostStyleProperties },
|
|
{ kind: CompilationJobKind.Tmpl, fn: emitNamespaceChanges },
|
|
{ kind: CompilationJobKind.Tmpl, fn: propagateI18nBlocks },
|
|
{ kind: CompilationJobKind.Tmpl, fn: wrapI18nIcus },
|
|
{ kind: CompilationJobKind.Both, fn: deduplicateTextBindings },
|
|
{ kind: CompilationJobKind.Both, fn: specializeStyleBindings },
|
|
{ kind: CompilationJobKind.Both, fn: specializeBindings },
|
|
{ kind: CompilationJobKind.Both, fn: convertAnimations },
|
|
{ kind: CompilationJobKind.Both, fn: extractAttributes },
|
|
{ kind: CompilationJobKind.Tmpl, fn: createI18nContexts },
|
|
{ kind: CompilationJobKind.Both, fn: parseExtractedStyles },
|
|
{ kind: CompilationJobKind.Tmpl, fn: removeEmptyBindings },
|
|
{ kind: CompilationJobKind.Both, fn: collapseSingletonInterpolations },
|
|
{ kind: CompilationJobKind.Both, fn: orderOps },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateConditionalExpressions },
|
|
{ kind: CompilationJobKind.Tmpl, fn: createPipes },
|
|
{ kind: CompilationJobKind.Tmpl, fn: configureDeferInstructions },
|
|
{ kind: CompilationJobKind.Tmpl, fn: createVariadicPipes },
|
|
{ kind: CompilationJobKind.Both, fn: generatePureLiteralStructures },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateProjectionDefs },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateLocalLetReferences },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateVariables },
|
|
{ kind: CompilationJobKind.Tmpl, fn: saveAndRestoreView },
|
|
{ kind: CompilationJobKind.Both, fn: deleteAnyCasts },
|
|
{ kind: CompilationJobKind.Both, fn: resolveDollarEvent },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateTrackVariables },
|
|
{ kind: CompilationJobKind.Tmpl, fn: removeIllegalLetReferences },
|
|
{ kind: CompilationJobKind.Both, fn: resolveNames },
|
|
{ kind: CompilationJobKind.Tmpl, fn: resolveDeferTargetNames },
|
|
{ kind: CompilationJobKind.Tmpl, fn: transformTwoWayBindingSet },
|
|
{ kind: CompilationJobKind.Tmpl, fn: optimizeTrackFns },
|
|
{ kind: CompilationJobKind.Both, fn: resolveContexts },
|
|
{ kind: CompilationJobKind.Both, fn: resolveSanitizers },
|
|
{ kind: CompilationJobKind.Tmpl, fn: liftLocalRefs },
|
|
{ kind: CompilationJobKind.Both, fn: expandSafeReads },
|
|
{ kind: CompilationJobKind.Both, fn: stripNonrequiredParentheses },
|
|
{ kind: CompilationJobKind.Both, fn: generateTemporaryVariables },
|
|
{ kind: CompilationJobKind.Both, fn: optimizeVariables },
|
|
{ kind: CompilationJobKind.Both, fn: optimizeStoreLet },
|
|
{ kind: CompilationJobKind.Tmpl, fn: convertI18nText },
|
|
{ kind: CompilationJobKind.Tmpl, fn: convertI18nBindings },
|
|
{ kind: CompilationJobKind.Tmpl, fn: removeUnusedI18nAttributesOps },
|
|
{ kind: CompilationJobKind.Tmpl, fn: assignI18nSlotDependencies },
|
|
{ kind: CompilationJobKind.Tmpl, fn: applyI18nExpressions },
|
|
{ kind: CompilationJobKind.Tmpl, fn: allocateSlots },
|
|
{ kind: CompilationJobKind.Tmpl, fn: resolveI18nElementPlaceholders },
|
|
{ kind: CompilationJobKind.Tmpl, fn: resolveI18nExpressionPlaceholders },
|
|
{ kind: CompilationJobKind.Tmpl, fn: extractI18nMessages },
|
|
{ kind: CompilationJobKind.Tmpl, fn: collectI18nConsts },
|
|
{ kind: CompilationJobKind.Tmpl, fn: collectConstExpressions },
|
|
{ kind: CompilationJobKind.Both, fn: collectElementConsts },
|
|
{ kind: CompilationJobKind.Tmpl, fn: removeI18nContexts },
|
|
{ kind: CompilationJobKind.Both, fn: countVariables },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateAdvance },
|
|
{ kind: CompilationJobKind.Both, fn: nameFunctionsAndVariables },
|
|
{ kind: CompilationJobKind.Tmpl, fn: resolveDeferDepsFns },
|
|
{ kind: CompilationJobKind.Tmpl, fn: mergeNextContextExpressions },
|
|
{ kind: CompilationJobKind.Tmpl, fn: generateNgContainerOps },
|
|
{ kind: CompilationJobKind.Tmpl, fn: collapseEmptyInstructions },
|
|
{ kind: CompilationJobKind.Tmpl, fn: attachSourceLocations },
|
|
{ kind: CompilationJobKind.Tmpl, fn: disableBindings$1 },
|
|
{ kind: CompilationJobKind.Both, fn: extractPureFunctions },
|
|
{ kind: CompilationJobKind.Both, fn: reify },
|
|
{ kind: CompilationJobKind.Both, fn: chain },
|
|
];
|
|
/**
|
|
* Run all transformation phases in the correct order against a compilation job. After this
|
|
* processing, the compilation should be in a state where it can be emitted.
|
|
*/
|
|
function transform(job, kind) {
|
|
for (const phase of phases) {
|
|
if (phase.kind === kind || phase.kind === CompilationJobKind.Both) {
|
|
// The type of `Phase` above ensures it is impossible to call a phase that doesn't support the
|
|
// job kind.
|
|
phase.fn(job);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Compile all views in the given `ComponentCompilation` into the final template function, which may
|
|
* reference constants defined in a `ConstantPool`.
|
|
*/
|
|
function emitTemplateFn(tpl, pool) {
|
|
const rootFn = emitView(tpl.root);
|
|
emitChildViews(tpl.root, pool);
|
|
return rootFn;
|
|
}
|
|
function emitChildViews(parent, pool) {
|
|
for (const unit of parent.job.units) {
|
|
if (unit.parent !== parent.xref) {
|
|
continue;
|
|
}
|
|
// Child views are emitted depth-first.
|
|
emitChildViews(unit, pool);
|
|
const viewFn = emitView(unit);
|
|
pool.statements.push(viewFn.toDeclStmt(viewFn.name));
|
|
}
|
|
}
|
|
/**
|
|
* Emit a template function for an individual `ViewCompilation` (which may be either the root view
|
|
* or an embedded view).
|
|
*/
|
|
function emitView(view) {
|
|
if (view.fnName === null) {
|
|
throw new Error(`AssertionError: view ${view.xref} is unnamed`);
|
|
}
|
|
const createStatements = [];
|
|
for (const op of view.create) {
|
|
if (op.kind !== OpKind.Statement) {
|
|
throw new Error(`AssertionError: expected all create ops to have been compiled, but got ${OpKind[op.kind]}`);
|
|
}
|
|
createStatements.push(op.statement);
|
|
}
|
|
const updateStatements = [];
|
|
for (const op of view.update) {
|
|
if (op.kind !== OpKind.Statement) {
|
|
throw new Error(`AssertionError: expected all update ops to have been compiled, but got ${OpKind[op.kind]}`);
|
|
}
|
|
updateStatements.push(op.statement);
|
|
}
|
|
const createCond = maybeGenerateRfBlock(1, createStatements);
|
|
const updateCond = maybeGenerateRfBlock(2, updateStatements);
|
|
return fn([new FnParam('rf'), new FnParam('ctx')], [...createCond, ...updateCond],
|
|
/* type */ undefined,
|
|
/* sourceSpan */ undefined, view.fnName);
|
|
}
|
|
function maybeGenerateRfBlock(flag, statements) {
|
|
if (statements.length === 0) {
|
|
return [];
|
|
}
|
|
return [
|
|
ifStmt(new BinaryOperatorExpr(BinaryOperator.BitwiseAnd, variable('rf'), literal(flag)), statements),
|
|
];
|
|
}
|
|
function emitHostBindingFunction(job) {
|
|
if (job.root.fnName === null) {
|
|
throw new Error(`AssertionError: host binding function is unnamed`);
|
|
}
|
|
const createStatements = [];
|
|
for (const op of job.root.create) {
|
|
if (op.kind !== OpKind.Statement) {
|
|
throw new Error(`AssertionError: expected all create ops to have been compiled, but got ${OpKind[op.kind]}`);
|
|
}
|
|
createStatements.push(op.statement);
|
|
}
|
|
const updateStatements = [];
|
|
for (const op of job.root.update) {
|
|
if (op.kind !== OpKind.Statement) {
|
|
throw new Error(`AssertionError: expected all update ops to have been compiled, but got ${OpKind[op.kind]}`);
|
|
}
|
|
updateStatements.push(op.statement);
|
|
}
|
|
if (createStatements.length === 0 && updateStatements.length === 0) {
|
|
return null;
|
|
}
|
|
const createCond = maybeGenerateRfBlock(1, createStatements);
|
|
const updateCond = maybeGenerateRfBlock(2, updateStatements);
|
|
return fn([new FnParam('rf'), new FnParam('ctx')], [...createCond, ...updateCond],
|
|
/* type */ undefined,
|
|
/* sourceSpan */ undefined, job.root.fnName);
|
|
}
|
|
|
|
const compatibilityMode = CompatibilityMode.TemplateDefinitionBuilder;
|
|
// Schema containing DOM elements and their properties.
|
|
const domSchema = new DomElementSchemaRegistry();
|
|
// Tag name of the `ng-template` element.
|
|
const NG_TEMPLATE_TAG_NAME = 'ng-template';
|
|
// prefix for any animation binding
|
|
const ANIMATE_PREFIX$1 = 'animate.';
|
|
function isI18nRootNode(meta) {
|
|
return meta instanceof Message;
|
|
}
|
|
function isSingleI18nIcu(meta) {
|
|
return isI18nRootNode(meta) && meta.nodes.length === 1 && meta.nodes[0] instanceof Icu;
|
|
}
|
|
/**
|
|
* Process a template AST and convert it into a `ComponentCompilation` in the intermediate
|
|
* representation.
|
|
* TODO: Refactor more of the ingestion code into phases.
|
|
*/
|
|
function ingestComponent(componentName, template, constantPool, compilationMode, relativeContextFilePath, i18nUseExternalIds, deferMeta, allDeferrableDepsFn, relativeTemplatePath, enableDebugLocations) {
|
|
const job = new ComponentCompilationJob(componentName, constantPool, compatibilityMode, compilationMode, relativeContextFilePath, i18nUseExternalIds, deferMeta, allDeferrableDepsFn, relativeTemplatePath, enableDebugLocations);
|
|
ingestNodes(job.root, template);
|
|
return job;
|
|
}
|
|
/**
|
|
* Process a host binding AST and convert it into a `HostBindingCompilationJob` in the intermediate
|
|
* representation.
|
|
*/
|
|
function ingestHostBinding(input, bindingParser, constantPool) {
|
|
const job = new HostBindingCompilationJob(input.componentName, constantPool, compatibilityMode, TemplateCompilationMode.DomOnly);
|
|
for (const property of input.properties ?? []) {
|
|
let bindingKind = BindingKind.Property;
|
|
// TODO: this should really be handled in the parser.
|
|
if (property.name.startsWith('attr.')) {
|
|
property.name = property.name.substring('attr.'.length);
|
|
bindingKind = BindingKind.Attribute;
|
|
}
|
|
if (property.isLegacyAnimation) {
|
|
bindingKind = BindingKind.LegacyAnimation;
|
|
}
|
|
if (property.isAnimation) {
|
|
bindingKind = BindingKind.Animation;
|
|
}
|
|
const securityContexts = bindingParser
|
|
.calcPossibleSecurityContexts(input.componentSelector, property.name, bindingKind === BindingKind.Attribute)
|
|
.filter((context) => context !== SecurityContext.NONE);
|
|
ingestDomProperty(job, property, bindingKind, securityContexts);
|
|
}
|
|
for (const [name, expr] of Object.entries(input.attributes) ?? []) {
|
|
const securityContexts = bindingParser
|
|
.calcPossibleSecurityContexts(input.componentSelector, name, true)
|
|
.filter((context) => context !== SecurityContext.NONE);
|
|
ingestHostAttribute(job, name, expr, securityContexts);
|
|
}
|
|
for (const event of input.events ?? []) {
|
|
ingestHostEvent(job, event);
|
|
}
|
|
return job;
|
|
}
|
|
// TODO: We should refactor the parser to use the same types and structures for host bindings as
|
|
// with ordinary components. This would allow us to share a lot more ingestion code.
|
|
function ingestDomProperty(job, property, bindingKind, securityContexts) {
|
|
let expression;
|
|
const ast = property.expression.ast;
|
|
if (ast instanceof Interpolation$1) {
|
|
expression = new Interpolation(ast.strings, ast.expressions.map((expr) => convertAst(expr, job, property.sourceSpan)), []);
|
|
}
|
|
else {
|
|
expression = convertAst(ast, job, property.sourceSpan);
|
|
}
|
|
job.root.update.push(createBindingOp(job.root.xref, bindingKind, property.name, expression, null, securityContexts, false, false, null,
|
|
/* TODO: How do Host bindings handle i18n attrs? */ null, property.sourceSpan));
|
|
}
|
|
function ingestHostAttribute(job, name, value, securityContexts) {
|
|
const attrBinding = createBindingOp(job.root.xref, BindingKind.Attribute, name, value, null, securityContexts,
|
|
/* Host attributes should always be extracted to const hostAttrs, even if they are not
|
|
*strictly* text literals */
|
|
true, false, null,
|
|
/* TODO */ null,
|
|
/** TODO: May be null? */ value.sourceSpan);
|
|
job.root.update.push(attrBinding);
|
|
}
|
|
function ingestHostEvent(job, event) {
|
|
let eventBinding;
|
|
if (event.type === ParsedEventType.Animation) {
|
|
eventBinding = createAnimationListenerOp(job.root.xref, new SlotHandle(), event.name, null, makeListenerHandlerOps(job.root, event.handler, event.handlerSpan), event.name.endsWith('enter') ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, event.targetOrPhase, true, event.sourceSpan);
|
|
}
|
|
else {
|
|
const [phase, target] = event.type !== ParsedEventType.LegacyAnimation
|
|
? [null, event.targetOrPhase]
|
|
: [event.targetOrPhase, null];
|
|
eventBinding = createListenerOp(job.root.xref, new SlotHandle(), event.name, null, makeListenerHandlerOps(job.root, event.handler, event.handlerSpan), phase, target, true, event.sourceSpan);
|
|
}
|
|
job.root.create.push(eventBinding);
|
|
}
|
|
/**
|
|
* Ingest the nodes of a template AST into the given `ViewCompilation`.
|
|
*/
|
|
function ingestNodes(unit, template) {
|
|
for (const node of template) {
|
|
if (node instanceof Element$1) {
|
|
ingestElement(unit, node);
|
|
}
|
|
else if (node instanceof Template) {
|
|
ingestTemplate(unit, node);
|
|
}
|
|
else if (node instanceof Content) {
|
|
ingestContent(unit, node);
|
|
}
|
|
else if (node instanceof Text$3) {
|
|
ingestText(unit, node, null);
|
|
}
|
|
else if (node instanceof BoundText) {
|
|
ingestBoundText(unit, node, null);
|
|
}
|
|
else if (node instanceof IfBlock) {
|
|
ingestIfBlock(unit, node);
|
|
}
|
|
else if (node instanceof SwitchBlock) {
|
|
ingestSwitchBlock(unit, node);
|
|
}
|
|
else if (node instanceof DeferredBlock) {
|
|
ingestDeferBlock(unit, node);
|
|
}
|
|
else if (node instanceof Icu$1) {
|
|
ingestIcu(unit, node);
|
|
}
|
|
else if (node instanceof ForLoopBlock) {
|
|
ingestForBlock(unit, node);
|
|
}
|
|
else if (node instanceof LetDeclaration$1) {
|
|
ingestLetDeclaration(unit, node);
|
|
}
|
|
else if (node instanceof Component$1) ;
|
|
else {
|
|
throw new Error(`Unsupported template node: ${node.constructor.name}`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Ingest an element AST from the template into the given `ViewCompilation`.
|
|
*/
|
|
function ingestElement(unit, element) {
|
|
if (element.i18n !== undefined &&
|
|
!(element.i18n instanceof Message || element.i18n instanceof TagPlaceholder)) {
|
|
throw Error(`Unhandled i18n metadata type for element: ${element.i18n.constructor.name}`);
|
|
}
|
|
const id = unit.job.allocateXrefId();
|
|
const [namespaceKey, elementName] = splitNsName(element.name);
|
|
const startOp = createElementStartOp(elementName, id, namespaceForKey(namespaceKey), element.i18n instanceof TagPlaceholder ? element.i18n : undefined, element.startSourceSpan, element.sourceSpan);
|
|
unit.create.push(startOp);
|
|
ingestElementBindings(unit, startOp, element);
|
|
ingestReferences(startOp, element);
|
|
// Start i18n, if needed, goes after the element create and bindings, but before the nodes
|
|
let i18nBlockId = null;
|
|
if (element.i18n instanceof Message) {
|
|
i18nBlockId = unit.job.allocateXrefId();
|
|
unit.create.push(createI18nStartOp(i18nBlockId, element.i18n, undefined, element.startSourceSpan));
|
|
}
|
|
ingestNodes(unit, element.children);
|
|
// The source span for the end op is typically the element closing tag. However, if no closing tag
|
|
// exists, such as in `<input>`, we use the start source span instead. Usually the start and end
|
|
// instructions will be collapsed into one `element` instruction, negating the purpose of this
|
|
// fallback, but in cases when it is not collapsed (such as an input with a binding), we still
|
|
// want to map the end instruction to the main element.
|
|
const endOp = createElementEndOp(id, element.endSourceSpan ?? element.startSourceSpan);
|
|
unit.create.push(endOp);
|
|
// If there is an i18n message associated with this element, insert i18n start and end ops.
|
|
if (i18nBlockId !== null) {
|
|
OpList.insertBefore(createI18nEndOp(i18nBlockId, element.endSourceSpan ?? element.startSourceSpan), endOp);
|
|
}
|
|
}
|
|
/**
|
|
* Ingest an `ng-template` node from the AST into the given `ViewCompilation`.
|
|
*/
|
|
function ingestTemplate(unit, tmpl) {
|
|
if (tmpl.i18n !== undefined &&
|
|
!(tmpl.i18n instanceof Message || tmpl.i18n instanceof TagPlaceholder)) {
|
|
throw Error(`Unhandled i18n metadata type for template: ${tmpl.i18n.constructor.name}`);
|
|
}
|
|
const childView = unit.job.allocateView(unit.xref);
|
|
let tagNameWithoutNamespace = tmpl.tagName;
|
|
let namespacePrefix = '';
|
|
if (tmpl.tagName) {
|
|
[namespacePrefix, tagNameWithoutNamespace] = splitNsName(tmpl.tagName);
|
|
}
|
|
const i18nPlaceholder = tmpl.i18n instanceof TagPlaceholder ? tmpl.i18n : undefined;
|
|
const namespace = namespaceForKey(namespacePrefix);
|
|
const functionNameSuffix = tagNameWithoutNamespace === null ? '' : prefixWithNamespace(tagNameWithoutNamespace, namespace);
|
|
const templateKind = isPlainTemplate(tmpl)
|
|
? TemplateKind.NgTemplate
|
|
: TemplateKind.Structural;
|
|
const templateOp = createTemplateOp(childView.xref, templateKind, tagNameWithoutNamespace, functionNameSuffix, namespace, i18nPlaceholder, tmpl.startSourceSpan, tmpl.sourceSpan);
|
|
unit.create.push(templateOp);
|
|
ingestTemplateBindings(unit, templateOp, tmpl, templateKind);
|
|
ingestReferences(templateOp, tmpl);
|
|
ingestNodes(childView, tmpl.children);
|
|
for (const { name, value } of tmpl.variables) {
|
|
childView.contextVariables.set(name, value !== '' ? value : '$implicit');
|
|
}
|
|
// If this is a plain template and there is an i18n message associated with it, insert i18n start
|
|
// and end ops. For structural directive templates, the i18n ops will be added when ingesting the
|
|
// element/template the directive is placed on.
|
|
if (templateKind === TemplateKind.NgTemplate && tmpl.i18n instanceof Message) {
|
|
const id = unit.job.allocateXrefId();
|
|
OpList.insertAfter(createI18nStartOp(id, tmpl.i18n, undefined, tmpl.startSourceSpan), childView.create.head);
|
|
OpList.insertBefore(createI18nEndOp(id, tmpl.endSourceSpan ?? tmpl.startSourceSpan), childView.create.tail);
|
|
}
|
|
}
|
|
/**
|
|
* Ingest a content node from the AST into the given `ViewCompilation`.
|
|
*/
|
|
function ingestContent(unit, content) {
|
|
if (content.i18n !== undefined && !(content.i18n instanceof TagPlaceholder)) {
|
|
throw Error(`Unhandled i18n metadata type for element: ${content.i18n.constructor.name}`);
|
|
}
|
|
let fallbackView = null;
|
|
// Don't capture default content that's only made up of empty text nodes and comments.
|
|
// Note that we process the default content before the projection in order to match the
|
|
// insertion order at runtime.
|
|
if (content.children.some((child) => !(child instanceof Comment$1) &&
|
|
(!(child instanceof Text$3) || child.value.trim().length > 0))) {
|
|
fallbackView = unit.job.allocateView(unit.xref);
|
|
ingestNodes(fallbackView, content.children);
|
|
}
|
|
const id = unit.job.allocateXrefId();
|
|
const op = createProjectionOp(id, content.selector, content.i18n, fallbackView?.xref ?? null, content.sourceSpan);
|
|
for (const attr of content.attributes) {
|
|
const securityContext = domSchema.securityContext(content.name, attr.name, true);
|
|
unit.update.push(createBindingOp(op.xref, BindingKind.Attribute, attr.name, literal(attr.value), null, securityContext, true, false, null, asMessage(attr.i18n), attr.sourceSpan));
|
|
}
|
|
unit.create.push(op);
|
|
}
|
|
/**
|
|
* Ingest a literal text node from the AST into the given `ViewCompilation`.
|
|
*/
|
|
function ingestText(unit, text, icuPlaceholder) {
|
|
unit.create.push(createTextOp(unit.job.allocateXrefId(), text.value, icuPlaceholder, text.sourceSpan));
|
|
}
|
|
/**
|
|
* Ingest an interpolated text node from the AST into the given `ViewCompilation`.
|
|
*/
|
|
function ingestBoundText(unit, text, icuPlaceholder) {
|
|
let value = text.value;
|
|
if (value instanceof ASTWithSource) {
|
|
value = value.ast;
|
|
}
|
|
if (!(value instanceof Interpolation$1)) {
|
|
throw new Error(`AssertionError: expected Interpolation for BoundText node, got ${value.constructor.name}`);
|
|
}
|
|
if (text.i18n !== undefined && !(text.i18n instanceof Container)) {
|
|
throw Error(`Unhandled i18n metadata type for text interpolation: ${text.i18n?.constructor.name}`);
|
|
}
|
|
const i18nPlaceholders = text.i18n instanceof Container
|
|
? text.i18n.children
|
|
.filter((node) => node instanceof Placeholder)
|
|
.map((placeholder) => placeholder.name)
|
|
: [];
|
|
if (i18nPlaceholders.length > 0 && i18nPlaceholders.length !== value.expressions.length) {
|
|
throw Error(`Unexpected number of i18n placeholders (${value.expressions.length}) for BoundText with ${value.expressions.length} expressions`);
|
|
}
|
|
const textXref = unit.job.allocateXrefId();
|
|
unit.create.push(createTextOp(textXref, '', icuPlaceholder, text.sourceSpan));
|
|
// TemplateDefinitionBuilder does not generate source maps for sub-expressions inside an
|
|
// interpolation. We copy that behavior in compatibility mode.
|
|
// TODO: is it actually correct to generate these extra maps in modern mode?
|
|
const baseSourceSpan = unit.job.compatibility ? null : text.sourceSpan;
|
|
unit.update.push(createInterpolateTextOp(textXref, new Interpolation(value.strings, value.expressions.map((expr) => convertAst(expr, unit.job, baseSourceSpan)), i18nPlaceholders), text.sourceSpan));
|
|
}
|
|
/**
|
|
* Ingest an `@if` block into the given `ViewCompilation`.
|
|
*/
|
|
function ingestIfBlock(unit, ifBlock) {
|
|
let firstXref = null;
|
|
let conditions = [];
|
|
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
const ifCase = ifBlock.branches[i];
|
|
const cView = unit.job.allocateView(unit.xref);
|
|
const tagName = ingestControlFlowInsertionPoint(unit, cView.xref, ifCase);
|
|
if (ifCase.expressionAlias !== null) {
|
|
cView.contextVariables.set(ifCase.expressionAlias.name, CTX_REF);
|
|
}
|
|
let ifCaseI18nMeta = undefined;
|
|
if (ifCase.i18n !== undefined) {
|
|
if (!(ifCase.i18n instanceof BlockPlaceholder)) {
|
|
throw Error(`Unhandled i18n metadata type for if block: ${ifCase.i18n?.constructor.name}`);
|
|
}
|
|
ifCaseI18nMeta = ifCase.i18n;
|
|
}
|
|
const createOp = i === 0 ? createConditionalCreateOp : createConditionalBranchCreateOp;
|
|
const conditionalCreateOp = createOp(cView.xref, TemplateKind.Block, tagName, 'Conditional', Namespace.HTML, ifCaseI18nMeta, ifCase.startSourceSpan, ifCase.sourceSpan);
|
|
unit.create.push(conditionalCreateOp);
|
|
if (firstXref === null) {
|
|
firstXref = cView.xref;
|
|
}
|
|
const caseExpr = ifCase.expression ? convertAst(ifCase.expression, unit.job, null) : null;
|
|
const conditionalCaseExpr = new ConditionalCaseExpr(caseExpr, conditionalCreateOp.xref, conditionalCreateOp.handle, ifCase.expressionAlias);
|
|
conditions.push(conditionalCaseExpr);
|
|
ingestNodes(cView, ifCase.children);
|
|
}
|
|
unit.update.push(createConditionalOp(firstXref, null, conditions, ifBlock.sourceSpan));
|
|
}
|
|
/**
|
|
* Ingest an `@switch` block into the given `ViewCompilation`.
|
|
*/
|
|
function ingestSwitchBlock(unit, switchBlock) {
|
|
// Don't ingest empty switches since they won't render anything.
|
|
if (switchBlock.cases.length === 0) {
|
|
return;
|
|
}
|
|
let firstXref = null;
|
|
let conditions = [];
|
|
for (let i = 0; i < switchBlock.cases.length; i++) {
|
|
const switchCase = switchBlock.cases[i];
|
|
const cView = unit.job.allocateView(unit.xref);
|
|
const tagName = ingestControlFlowInsertionPoint(unit, cView.xref, switchCase);
|
|
let switchCaseI18nMeta = undefined;
|
|
if (switchCase.i18n !== undefined) {
|
|
if (!(switchCase.i18n instanceof BlockPlaceholder)) {
|
|
throw Error(`Unhandled i18n metadata type for switch block: ${switchCase.i18n?.constructor.name}`);
|
|
}
|
|
switchCaseI18nMeta = switchCase.i18n;
|
|
}
|
|
const createOp = i === 0 ? createConditionalCreateOp : createConditionalBranchCreateOp;
|
|
const conditionalCreateOp = createOp(cView.xref, TemplateKind.Block, tagName, 'Case', Namespace.HTML, switchCaseI18nMeta, switchCase.startSourceSpan, switchCase.sourceSpan);
|
|
unit.create.push(conditionalCreateOp);
|
|
if (firstXref === null) {
|
|
firstXref = cView.xref;
|
|
}
|
|
const caseExpr = switchCase.expression
|
|
? convertAst(switchCase.expression, unit.job, switchBlock.startSourceSpan)
|
|
: null;
|
|
const conditionalCaseExpr = new ConditionalCaseExpr(caseExpr, conditionalCreateOp.xref, conditionalCreateOp.handle);
|
|
conditions.push(conditionalCaseExpr);
|
|
ingestNodes(cView, switchCase.children);
|
|
}
|
|
unit.update.push(createConditionalOp(firstXref, convertAst(switchBlock.expression, unit.job, null), conditions, switchBlock.sourceSpan));
|
|
}
|
|
function ingestDeferView(unit, suffix, i18nMeta, children, sourceSpan) {
|
|
if (i18nMeta !== undefined && !(i18nMeta instanceof BlockPlaceholder)) {
|
|
throw Error('Unhandled i18n metadata type for defer block');
|
|
}
|
|
if (children === undefined) {
|
|
return null;
|
|
}
|
|
const secondaryView = unit.job.allocateView(unit.xref);
|
|
ingestNodes(secondaryView, children);
|
|
const templateOp = createTemplateOp(secondaryView.xref, TemplateKind.Block, null, `Defer${suffix}`, Namespace.HTML, i18nMeta, sourceSpan, sourceSpan);
|
|
unit.create.push(templateOp);
|
|
return templateOp;
|
|
}
|
|
function ingestDeferBlock(unit, deferBlock) {
|
|
let ownResolverFn = null;
|
|
if (unit.job.deferMeta.mode === 0 /* DeferBlockDepsEmitMode.PerBlock */) {
|
|
if (!unit.job.deferMeta.blocks.has(deferBlock)) {
|
|
throw new Error(`AssertionError: unable to find a dependency function for this deferred block`);
|
|
}
|
|
ownResolverFn = unit.job.deferMeta.blocks.get(deferBlock) ?? null;
|
|
}
|
|
// Generate the defer main view and all secondary views.
|
|
const main = ingestDeferView(unit, '', deferBlock.i18n, deferBlock.children, deferBlock.sourceSpan);
|
|
const loading = ingestDeferView(unit, 'Loading', deferBlock.loading?.i18n, deferBlock.loading?.children, deferBlock.loading?.sourceSpan);
|
|
const placeholder = ingestDeferView(unit, 'Placeholder', deferBlock.placeholder?.i18n, deferBlock.placeholder?.children, deferBlock.placeholder?.sourceSpan);
|
|
const error = ingestDeferView(unit, 'Error', deferBlock.error?.i18n, deferBlock.error?.children, deferBlock.error?.sourceSpan);
|
|
// Create the main defer op, and ops for all secondary views.
|
|
const deferXref = unit.job.allocateXrefId();
|
|
const deferOp = createDeferOp(deferXref, main.xref, main.handle, ownResolverFn, unit.job.allDeferrableDepsFn, deferBlock.sourceSpan);
|
|
deferOp.placeholderView = placeholder?.xref ?? null;
|
|
deferOp.placeholderSlot = placeholder?.handle ?? null;
|
|
deferOp.loadingSlot = loading?.handle ?? null;
|
|
deferOp.errorSlot = error?.handle ?? null;
|
|
deferOp.placeholderMinimumTime = deferBlock.placeholder?.minimumTime ?? null;
|
|
deferOp.loadingMinimumTime = deferBlock.loading?.minimumTime ?? null;
|
|
deferOp.loadingAfterTime = deferBlock.loading?.afterTime ?? null;
|
|
deferOp.flags = calcDeferBlockFlags(deferBlock);
|
|
unit.create.push(deferOp);
|
|
// Configure all defer `on` conditions.
|
|
// TODO: refactor prefetch triggers to use a separate op type, with a shared superclass. This will
|
|
// make it easier to refactor prefetch behavior in the future.
|
|
const deferOnOps = [];
|
|
const deferWhenOps = [];
|
|
// Ingest the hydrate triggers first since they set up all the other triggers during SSR.
|
|
ingestDeferTriggers("hydrate" /* ir.DeferOpModifierKind.HYDRATE */, deferBlock.hydrateTriggers, deferOnOps, deferWhenOps, unit, deferXref);
|
|
ingestDeferTriggers("none" /* ir.DeferOpModifierKind.NONE */, deferBlock.triggers, deferOnOps, deferWhenOps, unit, deferXref);
|
|
ingestDeferTriggers("prefetch" /* ir.DeferOpModifierKind.PREFETCH */, deferBlock.prefetchTriggers, deferOnOps, deferWhenOps, unit, deferXref);
|
|
// If no (non-prefetching or hydrating) defer triggers were provided, default to `idle`.
|
|
const hasConcreteTrigger = deferOnOps.some((op) => op.modifier === "none" /* ir.DeferOpModifierKind.NONE */) ||
|
|
deferWhenOps.some((op) => op.modifier === "none" /* ir.DeferOpModifierKind.NONE */);
|
|
if (!hasConcreteTrigger) {
|
|
deferOnOps.push(createDeferOnOp(deferXref, { kind: DeferTriggerKind.Idle }, "none" /* ir.DeferOpModifierKind.NONE */, null));
|
|
}
|
|
unit.create.push(deferOnOps);
|
|
unit.update.push(deferWhenOps);
|
|
}
|
|
function calcDeferBlockFlags(deferBlockDetails) {
|
|
if (Object.keys(deferBlockDetails.hydrateTriggers).length > 0) {
|
|
return 1 /* ir.TDeferDetailsFlags.HasHydrateTriggers */;
|
|
}
|
|
return null;
|
|
}
|
|
function ingestDeferTriggers(modifier, triggers, onOps, whenOps, unit, deferXref) {
|
|
if (triggers.idle !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, { kind: DeferTriggerKind.Idle }, modifier, triggers.idle.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.immediate !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, { kind: DeferTriggerKind.Immediate }, modifier, triggers.immediate.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.timer !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, { kind: DeferTriggerKind.Timer, delay: triggers.timer.delay }, modifier, triggers.timer.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.hover !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, {
|
|
kind: DeferTriggerKind.Hover,
|
|
targetName: triggers.hover.reference,
|
|
targetXref: null,
|
|
targetSlot: null,
|
|
targetView: null,
|
|
targetSlotViewSteps: null,
|
|
}, modifier, triggers.hover.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.interaction !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, {
|
|
kind: DeferTriggerKind.Interaction,
|
|
targetName: triggers.interaction.reference,
|
|
targetXref: null,
|
|
targetSlot: null,
|
|
targetView: null,
|
|
targetSlotViewSteps: null,
|
|
}, modifier, triggers.interaction.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.viewport !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, {
|
|
kind: DeferTriggerKind.Viewport,
|
|
targetName: triggers.viewport.reference,
|
|
targetXref: null,
|
|
targetSlot: null,
|
|
targetView: null,
|
|
targetSlotViewSteps: null,
|
|
}, modifier, triggers.viewport.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.never !== undefined) {
|
|
const deferOnOp = createDeferOnOp(deferXref, { kind: DeferTriggerKind.Never }, modifier, triggers.never.sourceSpan);
|
|
onOps.push(deferOnOp);
|
|
}
|
|
if (triggers.when !== undefined) {
|
|
if (triggers.when.value instanceof Interpolation$1) {
|
|
// TemplateDefinitionBuilder supports this case, but it's very strange to me. What would it
|
|
// even mean?
|
|
throw new Error(`Unexpected interpolation in defer block when trigger`);
|
|
}
|
|
const deferOnOp = createDeferWhenOp(deferXref, convertAst(triggers.when.value, unit.job, triggers.when.sourceSpan), modifier, triggers.when.sourceSpan);
|
|
whenOps.push(deferOnOp);
|
|
}
|
|
}
|
|
function ingestIcu(unit, icu) {
|
|
if (icu.i18n instanceof Message && isSingleI18nIcu(icu.i18n)) {
|
|
const xref = unit.job.allocateXrefId();
|
|
unit.create.push(createIcuStartOp(xref, icu.i18n, icuFromI18nMessage(icu.i18n).name, null));
|
|
for (const [placeholder, text] of Object.entries({ ...icu.vars, ...icu.placeholders })) {
|
|
if (text instanceof BoundText) {
|
|
ingestBoundText(unit, text, placeholder);
|
|
}
|
|
else {
|
|
ingestText(unit, text, placeholder);
|
|
}
|
|
}
|
|
unit.create.push(createIcuEndOp(xref));
|
|
}
|
|
else {
|
|
throw Error(`Unhandled i18n metadata type for ICU: ${icu.i18n?.constructor.name}`);
|
|
}
|
|
}
|
|
/**
|
|
* Ingest an `@for` block into the given `ViewCompilation`.
|
|
*/
|
|
function ingestForBlock(unit, forBlock) {
|
|
const repeaterView = unit.job.allocateView(unit.xref);
|
|
// We copy TemplateDefinitionBuilder's scheme of creating names for `$count` and `$index`
|
|
// that are suffixed with special information, to disambiguate which level of nested loop
|
|
// the below aliases refer to.
|
|
// TODO: We should refactor Template Pipeline's variable phases to gracefully handle
|
|
// shadowing, and arbitrarily many levels of variables depending on each other.
|
|
const indexName = `ɵ$index_${repeaterView.xref}`;
|
|
const countName = `ɵ$count_${repeaterView.xref}`;
|
|
const indexVarNames = new Set();
|
|
// Set all the context variables and aliases available in the repeater.
|
|
repeaterView.contextVariables.set(forBlock.item.name, forBlock.item.value);
|
|
for (const variable of forBlock.contextVariables) {
|
|
if (variable.value === '$index') {
|
|
indexVarNames.add(variable.name);
|
|
}
|
|
if (variable.name === '$index') {
|
|
repeaterView.contextVariables.set('$index', variable.value).set(indexName, variable.value);
|
|
}
|
|
else if (variable.name === '$count') {
|
|
repeaterView.contextVariables.set('$count', variable.value).set(countName, variable.value);
|
|
}
|
|
else {
|
|
repeaterView.aliases.add({
|
|
kind: SemanticVariableKind.Alias,
|
|
name: null,
|
|
identifier: variable.name,
|
|
expression: getComputedForLoopVariableExpression(variable, indexName, countName),
|
|
});
|
|
}
|
|
}
|
|
const sourceSpan = convertSourceSpan(forBlock.trackBy.span, forBlock.sourceSpan);
|
|
const track = convertAst(forBlock.trackBy, unit.job, sourceSpan);
|
|
ingestNodes(repeaterView, forBlock.children);
|
|
let emptyView = null;
|
|
let emptyTagName = null;
|
|
if (forBlock.empty !== null) {
|
|
emptyView = unit.job.allocateView(unit.xref);
|
|
ingestNodes(emptyView, forBlock.empty.children);
|
|
emptyTagName = ingestControlFlowInsertionPoint(unit, emptyView.xref, forBlock.empty);
|
|
}
|
|
const varNames = {
|
|
$index: indexVarNames,
|
|
$implicit: forBlock.item.name,
|
|
};
|
|
if (forBlock.i18n !== undefined && !(forBlock.i18n instanceof BlockPlaceholder)) {
|
|
throw Error('AssertionError: Unhandled i18n metadata type or @for');
|
|
}
|
|
if (forBlock.empty?.i18n !== undefined &&
|
|
!(forBlock.empty.i18n instanceof BlockPlaceholder)) {
|
|
throw Error('AssertionError: Unhandled i18n metadata type or @empty');
|
|
}
|
|
const i18nPlaceholder = forBlock.i18n;
|
|
const emptyI18nPlaceholder = forBlock.empty?.i18n;
|
|
const tagName = ingestControlFlowInsertionPoint(unit, repeaterView.xref, forBlock);
|
|
const repeaterCreate = createRepeaterCreateOp(repeaterView.xref, emptyView?.xref ?? null, tagName, track, varNames, emptyTagName, i18nPlaceholder, emptyI18nPlaceholder, forBlock.startSourceSpan, forBlock.sourceSpan);
|
|
unit.create.push(repeaterCreate);
|
|
const expression = convertAst(forBlock.expression, unit.job, convertSourceSpan(forBlock.expression.span, forBlock.sourceSpan));
|
|
const repeater = createRepeaterOp(repeaterCreate.xref, repeaterCreate.handle, expression, forBlock.sourceSpan);
|
|
unit.update.push(repeater);
|
|
}
|
|
/**
|
|
* Gets an expression that represents a variable in an `@for` loop.
|
|
* @param variable AST representing the variable.
|
|
* @param indexName Loop-specific name for `$index`.
|
|
* @param countName Loop-specific name for `$count`.
|
|
*/
|
|
function getComputedForLoopVariableExpression(variable, indexName, countName) {
|
|
switch (variable.value) {
|
|
case '$index':
|
|
return new LexicalReadExpr(indexName);
|
|
case '$count':
|
|
return new LexicalReadExpr(countName);
|
|
case '$first':
|
|
return new LexicalReadExpr(indexName).identical(literal(0));
|
|
case '$last':
|
|
return new LexicalReadExpr(indexName).identical(new LexicalReadExpr(countName).minus(literal(1)));
|
|
case '$even':
|
|
return new LexicalReadExpr(indexName).modulo(literal(2)).identical(literal(0));
|
|
case '$odd':
|
|
return new LexicalReadExpr(indexName).modulo(literal(2)).notIdentical(literal(0));
|
|
default:
|
|
throw new Error(`AssertionError: unknown @for loop variable ${variable.value}`);
|
|
}
|
|
}
|
|
function ingestLetDeclaration(unit, node) {
|
|
const target = unit.job.allocateXrefId();
|
|
unit.create.push(createDeclareLetOp(target, node.name, node.sourceSpan));
|
|
unit.update.push(createStoreLetOp(target, node.name, convertAst(node.value, unit.job, node.valueSpan), node.sourceSpan));
|
|
}
|
|
/**
|
|
* Convert a template AST expression into an output AST expression.
|
|
*/
|
|
function convertAst(ast, job, baseSourceSpan) {
|
|
if (ast instanceof ASTWithSource) {
|
|
return convertAst(ast.ast, job, baseSourceSpan);
|
|
}
|
|
else if (ast instanceof PropertyRead) {
|
|
// Whether this is an implicit receiver, *excluding* explicit reads of `this`.
|
|
const isImplicitReceiver = ast.receiver instanceof ImplicitReceiver && !(ast.receiver instanceof ThisReceiver);
|
|
if (isImplicitReceiver) {
|
|
return new LexicalReadExpr(ast.name);
|
|
}
|
|
else {
|
|
return new ReadPropExpr(convertAst(ast.receiver, job, baseSourceSpan), ast.name, null, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
}
|
|
else if (ast instanceof Call) {
|
|
if (ast.receiver instanceof ImplicitReceiver) {
|
|
throw new Error(`Unexpected ImplicitReceiver`);
|
|
}
|
|
else {
|
|
return new InvokeFunctionExpr(convertAst(ast.receiver, job, baseSourceSpan), ast.args.map((arg) => convertAst(arg, job, baseSourceSpan)), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
}
|
|
else if (ast instanceof LiteralPrimitive) {
|
|
return literal(ast.value, undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof Unary) {
|
|
switch (ast.operator) {
|
|
case '+':
|
|
return new UnaryOperatorExpr(UnaryOperator.Plus, convertAst(ast.expr, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
case '-':
|
|
return new UnaryOperatorExpr(UnaryOperator.Minus, convertAst(ast.expr, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
default:
|
|
throw new Error(`AssertionError: unknown unary operator ${ast.operator}`);
|
|
}
|
|
}
|
|
else if (ast instanceof Binary) {
|
|
const operator = BINARY_OPERATORS.get(ast.operation);
|
|
if (operator === undefined) {
|
|
throw new Error(`AssertionError: unknown binary operator ${ast.operation}`);
|
|
}
|
|
return new BinaryOperatorExpr(operator, convertAst(ast.left, job, baseSourceSpan), convertAst(ast.right, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof ThisReceiver) {
|
|
// TODO: should context expressions have source maps?
|
|
return new ContextExpr(job.root.xref);
|
|
}
|
|
else if (ast instanceof KeyedRead) {
|
|
return new ReadKeyExpr(convertAst(ast.receiver, job, baseSourceSpan), convertAst(ast.key, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof Chain) {
|
|
throw new Error(`AssertionError: Chain in unknown context`);
|
|
}
|
|
else if (ast instanceof LiteralMap) {
|
|
const entries = ast.keys.map((key, idx) => {
|
|
const value = ast.values[idx];
|
|
// TODO: should literals have source maps, or do we just map the whole surrounding
|
|
// expression?
|
|
return new LiteralMapEntry(key.key, convertAst(value, job, baseSourceSpan), key.quoted);
|
|
});
|
|
return new LiteralMapExpr(entries, undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof LiteralArray) {
|
|
// TODO: should literals have source maps, or do we just map the whole surrounding expression?
|
|
return new LiteralArrayExpr(ast.expressions.map((expr) => convertAst(expr, job, baseSourceSpan)));
|
|
}
|
|
else if (ast instanceof Conditional) {
|
|
return new ConditionalExpr(convertAst(ast.condition, job, baseSourceSpan), convertAst(ast.trueExp, job, baseSourceSpan), convertAst(ast.falseExp, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof NonNullAssert) {
|
|
// A non-null assertion shouldn't impact generated instructions, so we can just drop it.
|
|
return convertAst(ast.expression, job, baseSourceSpan);
|
|
}
|
|
else if (ast instanceof BindingPipe) {
|
|
// TODO: pipes should probably have source maps; figure out details.
|
|
return new PipeBindingExpr(job.allocateXrefId(), new SlotHandle(), ast.name, [
|
|
convertAst(ast.exp, job, baseSourceSpan),
|
|
...ast.args.map((arg) => convertAst(arg, job, baseSourceSpan)),
|
|
]);
|
|
}
|
|
else if (ast instanceof SafeKeyedRead) {
|
|
return new SafeKeyedReadExpr(convertAst(ast.receiver, job, baseSourceSpan), convertAst(ast.key, job, baseSourceSpan), convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof SafePropertyRead) {
|
|
// TODO: source span
|
|
return new SafePropertyReadExpr(convertAst(ast.receiver, job, baseSourceSpan), ast.name);
|
|
}
|
|
else if (ast instanceof SafeCall) {
|
|
// TODO: source span
|
|
return new SafeInvokeFunctionExpr(convertAst(ast.receiver, job, baseSourceSpan), ast.args.map((a) => convertAst(a, job, baseSourceSpan)));
|
|
}
|
|
else if (ast instanceof EmptyExpr$1) {
|
|
return new EmptyExpr(convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof PrefixNot) {
|
|
return not(convertAst(ast.expression, job, baseSourceSpan), convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof TypeofExpression) {
|
|
return typeofExpr(convertAst(ast.expression, job, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof VoidExpression) {
|
|
return new VoidExpr(convertAst(ast.expression, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof TemplateLiteral) {
|
|
return convertTemplateLiteral(ast, job, baseSourceSpan);
|
|
}
|
|
else if (ast instanceof TaggedTemplateLiteral) {
|
|
return new TaggedTemplateLiteralExpr(convertAst(ast.tag, job, baseSourceSpan), convertTemplateLiteral(ast.template, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else if (ast instanceof ParenthesizedExpression) {
|
|
return new ParenthesizedExpr(convertAst(ast.expression, job, baseSourceSpan), undefined, convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
else {
|
|
throw new Error(`Unhandled expression type "${ast.constructor.name}" in file "${baseSourceSpan?.start.file.url}"`);
|
|
}
|
|
}
|
|
function convertTemplateLiteral(ast, job, baseSourceSpan) {
|
|
return new TemplateLiteralExpr(ast.elements.map((el) => {
|
|
return new TemplateLiteralElementExpr(el.text, convertSourceSpan(el.span, baseSourceSpan));
|
|
}), ast.expressions.map((expr) => convertAst(expr, job, baseSourceSpan)), convertSourceSpan(ast.span, baseSourceSpan));
|
|
}
|
|
function convertAstWithInterpolation(job, value, i18nMeta, sourceSpan) {
|
|
let expression;
|
|
if (value instanceof Interpolation$1) {
|
|
expression = new Interpolation(value.strings, value.expressions.map((e) => convertAst(e, job, null)), Object.keys(asMessage(i18nMeta)?.placeholders ?? {}));
|
|
}
|
|
else if (value instanceof AST) {
|
|
expression = convertAst(value, job, null);
|
|
}
|
|
else {
|
|
expression = literal(value);
|
|
}
|
|
return expression;
|
|
}
|
|
// TODO: Can we populate Template binding kinds in ingest?
|
|
const BINDING_KINDS = new Map([
|
|
[BindingType.Property, BindingKind.Property],
|
|
[BindingType.TwoWay, BindingKind.TwoWayProperty],
|
|
[BindingType.Attribute, BindingKind.Attribute],
|
|
[BindingType.Class, BindingKind.ClassName],
|
|
[BindingType.Style, BindingKind.StyleProperty],
|
|
[BindingType.LegacyAnimation, BindingKind.LegacyAnimation],
|
|
[BindingType.Animation, BindingKind.Animation],
|
|
]);
|
|
/**
|
|
* Checks whether the given template is a plain ng-template (as opposed to another kind of template
|
|
* such as a structural directive template or control flow template). This is checked based on the
|
|
* tagName. We can expect that only plain ng-templates will come through with a tagName of
|
|
* 'ng-template'.
|
|
*
|
|
* Here are some of the cases we expect:
|
|
*
|
|
* | Angular HTML | Template tagName |
|
|
* | ---------------------------------- | ------------------ |
|
|
* | `<ng-template>` | 'ng-template' |
|
|
* | `<div *ngIf="true">` | 'div' |
|
|
* | `<svg><ng-template>` | 'svg:ng-template' |
|
|
* | `@if (true) {` | 'Conditional' |
|
|
* | `<ng-template *ngIf>` (plain) | 'ng-template' |
|
|
* | `<ng-template *ngIf>` (structural) | null |
|
|
*/
|
|
function isPlainTemplate(tmpl) {
|
|
return splitNsName(tmpl.tagName ?? '')[1] === NG_TEMPLATE_TAG_NAME;
|
|
}
|
|
/**
|
|
* Ensures that the i18nMeta, if provided, is an i18n.Message.
|
|
*/
|
|
function asMessage(i18nMeta) {
|
|
if (i18nMeta == null) {
|
|
return null;
|
|
}
|
|
if (!(i18nMeta instanceof Message)) {
|
|
throw Error(`Expected i18n meta to be a Message, but got: ${i18nMeta.constructor.name}`);
|
|
}
|
|
return i18nMeta;
|
|
}
|
|
/**
|
|
* Process all of the bindings on an element in the template AST and convert them to their IR
|
|
* representation.
|
|
*/
|
|
function ingestElementBindings(unit, op, element) {
|
|
let bindings = new Array();
|
|
let i18nAttributeBindingNames = new Set();
|
|
for (const attr of element.attributes) {
|
|
// Attribute literal bindings, such as `attr.foo="bar"`.
|
|
const securityContext = domSchema.securityContext(element.name, attr.name, true);
|
|
bindings.push(createBindingOp(op.xref, BindingKind.Attribute, attr.name, convertAstWithInterpolation(unit.job, attr.value, attr.i18n), null, securityContext, true, false, null, asMessage(attr.i18n), attr.sourceSpan));
|
|
if (attr.i18n) {
|
|
i18nAttributeBindingNames.add(attr.name);
|
|
}
|
|
}
|
|
for (const input of element.inputs) {
|
|
if (i18nAttributeBindingNames.has(input.name)) {
|
|
console.error(`On component ${unit.job.componentName}, the binding ${input.name} is both an i18n attribute and a property. You may want to remove the property binding. This will become a compilation error in future versions of Angular.`);
|
|
}
|
|
// All dynamic bindings (both attribute and property bindings).
|
|
bindings.push(createBindingOp(op.xref, BINDING_KINDS.get(input.type), input.name, convertAstWithInterpolation(unit.job, astOf(input.value), input.i18n), input.unit, input.securityContext, false, false, null, asMessage(input.i18n) ?? null, input.sourceSpan));
|
|
}
|
|
unit.create.push(bindings.filter((b) => b?.kind === OpKind.ExtractedAttribute));
|
|
unit.update.push(bindings.filter((b) => b?.kind === OpKind.Binding));
|
|
for (const output of element.outputs) {
|
|
if (output.type === ParsedEventType.LegacyAnimation && output.phase === null) {
|
|
throw Error('Animation listener should have a phase');
|
|
}
|
|
if (output.type === ParsedEventType.TwoWay) {
|
|
unit.create.push(createTwoWayListenerOp(op.xref, op.handle, output.name, op.tag, makeTwoWayListenerHandlerOps(unit, output.handler, output.handlerSpan), output.sourceSpan));
|
|
}
|
|
else if (output.type === ParsedEventType.Animation) {
|
|
unit.create.push(createAnimationListenerOp(op.xref, op.handle, output.name, op.tag, makeListenerHandlerOps(unit, output.handler, output.handlerSpan), output.name.endsWith('enter') ? "enter" /* ir.AnimationKind.ENTER */ : "leave" /* ir.AnimationKind.LEAVE */, output.target, false, output.sourceSpan));
|
|
}
|
|
else {
|
|
unit.create.push(createListenerOp(op.xref, op.handle, output.name, op.tag, makeListenerHandlerOps(unit, output.handler, output.handlerSpan), output.phase, output.target, false, output.sourceSpan));
|
|
}
|
|
}
|
|
// If any of the bindings on this element have an i18n message, then an i18n attrs configuration
|
|
// op is also required.
|
|
if (bindings.some((b) => b?.i18nMessage) !== null) {
|
|
unit.create.push(createI18nAttributesOp(unit.job.allocateXrefId(), new SlotHandle(), op.xref));
|
|
}
|
|
}
|
|
/**
|
|
* Process all of the bindings on a template in the template AST and convert them to their IR
|
|
* representation.
|
|
*/
|
|
function ingestTemplateBindings(unit, op, template, templateKind) {
|
|
let bindings = new Array();
|
|
for (const attr of template.templateAttrs) {
|
|
if (attr instanceof TextAttribute) {
|
|
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
|
|
bindings.push(createTemplateBinding(unit, op.xref, BindingType.Attribute, attr.name, attr.value, null, securityContext, true, templateKind, asMessage(attr.i18n), attr.sourceSpan));
|
|
}
|
|
else {
|
|
bindings.push(createTemplateBinding(unit, op.xref, attr.type, attr.name, astOf(attr.value), attr.unit, attr.securityContext, true, templateKind, asMessage(attr.i18n), attr.sourceSpan));
|
|
}
|
|
}
|
|
for (const attr of template.attributes) {
|
|
// Attribute literal bindings, such as `attr.foo="bar"`.
|
|
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
|
|
bindings.push(createTemplateBinding(unit, op.xref, BindingType.Attribute, attr.name, attr.value, null, securityContext, false, templateKind, asMessage(attr.i18n), attr.sourceSpan));
|
|
}
|
|
for (const input of template.inputs) {
|
|
// Dynamic bindings (both attribute and property bindings).
|
|
bindings.push(createTemplateBinding(unit, op.xref, input.type, input.name, astOf(input.value), input.unit, input.securityContext, false, templateKind, asMessage(input.i18n), input.sourceSpan));
|
|
}
|
|
unit.create.push(bindings.filter((b) => b?.kind === OpKind.ExtractedAttribute));
|
|
unit.update.push(bindings.filter((b) => b?.kind === OpKind.Binding));
|
|
for (const output of template.outputs) {
|
|
if (output.type === ParsedEventType.LegacyAnimation && output.phase === null) {
|
|
throw Error('Animation listener should have a phase');
|
|
}
|
|
if (templateKind === TemplateKind.NgTemplate) {
|
|
if (output.type === ParsedEventType.TwoWay) {
|
|
unit.create.push(createTwoWayListenerOp(op.xref, op.handle, output.name, op.tag, makeTwoWayListenerHandlerOps(unit, output.handler, output.handlerSpan), output.sourceSpan));
|
|
}
|
|
else {
|
|
unit.create.push(createListenerOp(op.xref, op.handle, output.name, op.tag, makeListenerHandlerOps(unit, output.handler, output.handlerSpan), output.phase, output.target, false, output.sourceSpan));
|
|
}
|
|
}
|
|
if (templateKind === TemplateKind.Structural &&
|
|
output.type !== ParsedEventType.LegacyAnimation) {
|
|
// Animation bindings are excluded from the structural template's const array.
|
|
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, output.name, false);
|
|
unit.create.push(createExtractedAttributeOp(op.xref, BindingKind.Property, null, output.name, null, null, null, securityContext));
|
|
}
|
|
}
|
|
// TODO: Perhaps we could do this in a phase? (It likely wouldn't change the slot indices.)
|
|
if (bindings.some((b) => b?.i18nMessage) !== null) {
|
|
unit.create.push(createI18nAttributesOp(unit.job.allocateXrefId(), new SlotHandle(), op.xref));
|
|
}
|
|
}
|
|
/**
|
|
* Helper to ingest an individual binding on a template, either an explicit `ng-template`, or an
|
|
* implicit template created via structural directive.
|
|
*
|
|
* Bindings on templates are *extremely* tricky. I have tried to isolate all of the confusing edge
|
|
* cases into this function, and to comment it well to document the behavior.
|
|
*
|
|
* Some of this behavior is intuitively incorrect, and we should consider changing it in the future.
|
|
*
|
|
* @param view The compilation unit for the view containing the template.
|
|
* @param xref The xref of the template op.
|
|
* @param type The binding type, according to the parser. This is fairly reasonable, e.g. both
|
|
* dynamic and static attributes have e.BindingType.Attribute.
|
|
* @param name The binding's name.
|
|
* @param value The bindings's value, which will either be an input AST expression, or a string
|
|
* literal. Note that the input AST expression may or may not be const -- it will only be a
|
|
* string literal if the parser considered it a text binding.
|
|
* @param unit If the binding has a unit (e.g. `px` for style bindings), then this is the unit.
|
|
* @param securityContext The security context of the binding.
|
|
* @param isStructuralTemplateAttribute Whether this binding actually applies to the structural
|
|
* ng-template. For example, an `ngFor` would actually apply to the structural template. (Most
|
|
* bindings on structural elements target the inner element, not the template.)
|
|
* @param templateKind Whether this is an explicit `ng-template` or an implicit template created by
|
|
* a structural directive. This should never be a block template.
|
|
* @param i18nMessage The i18n metadata for the binding, if any.
|
|
* @param sourceSpan The source span of the binding.
|
|
* @returns An IR binding op, or null if the binding should be skipped.
|
|
*/
|
|
function createTemplateBinding(view, xref, type, name, value, unit, securityContext, isStructuralTemplateAttribute, templateKind, i18nMessage, sourceSpan) {
|
|
const isTextBinding = typeof value === 'string';
|
|
// If this is a structural template, then several kinds of bindings should not result in an
|
|
// update instruction.
|
|
if (templateKind === TemplateKind.Structural) {
|
|
if (!isStructuralTemplateAttribute) {
|
|
switch (type) {
|
|
case BindingType.Property:
|
|
case BindingType.Class:
|
|
case BindingType.Style:
|
|
// Because this binding doesn't really target the ng-template, it must be a binding on an
|
|
// inner node of a structural template. We can't skip it entirely, because we still need
|
|
// it on the ng-template's consts (e.g. for the purposes of directive matching). However,
|
|
// we should not generate an update instruction for it.
|
|
return createExtractedAttributeOp(xref, BindingKind.Property, null, name, null, null, i18nMessage, securityContext);
|
|
case BindingType.TwoWay:
|
|
return createExtractedAttributeOp(xref, BindingKind.TwoWayProperty, null, name, null, null, i18nMessage, securityContext);
|
|
}
|
|
}
|
|
if (!isTextBinding &&
|
|
(type === BindingType.Attribute ||
|
|
type === BindingType.LegacyAnimation ||
|
|
type === BindingType.Animation)) {
|
|
// Again, this binding doesn't really target the ng-template; it actually targets the element
|
|
// inside the structural template. In the case of non-text attribute or animation bindings,
|
|
// the binding doesn't even show up on the ng-template const array, so we just skip it
|
|
// entirely.
|
|
return null;
|
|
}
|
|
}
|
|
let bindingType = BINDING_KINDS.get(type);
|
|
if (templateKind === TemplateKind.NgTemplate) {
|
|
// We know we are dealing with bindings directly on an explicit ng-template.
|
|
// Static attribute bindings should be collected into the const array as k/v pairs. Property
|
|
// bindings should result in a `property` instruction, and `AttributeMarker.Bindings` const
|
|
// entries.
|
|
//
|
|
// The difficulty is with dynamic attribute, style, and class bindings. These don't really make
|
|
// sense on an `ng-template` and should probably be parser errors. However,
|
|
// TemplateDefinitionBuilder generates `property` instructions for them, and so we do that as
|
|
// well.
|
|
//
|
|
// Note that we do have a slight behavior difference with TemplateDefinitionBuilder: although
|
|
// TDB emits `property` instructions for dynamic attributes, styles, and classes, only styles
|
|
// and classes also get const collected into the `AttributeMarker.Bindings` field. Dynamic
|
|
// attribute bindings are missing from the consts entirely. We choose to emit them into the
|
|
// consts field anyway, to avoid creating special cases for something so arcane and nonsensical.
|
|
if (type === BindingType.Class ||
|
|
type === BindingType.Style ||
|
|
(type === BindingType.Attribute && !isTextBinding)) {
|
|
// TODO: These cases should be parse errors.
|
|
bindingType = BindingKind.Property;
|
|
}
|
|
}
|
|
return createBindingOp(xref, bindingType, name, convertAstWithInterpolation(view.job, value, i18nMessage), unit, securityContext, isTextBinding, isStructuralTemplateAttribute, templateKind, i18nMessage, sourceSpan);
|
|
}
|
|
function makeListenerHandlerOps(unit, handler, handlerSpan) {
|
|
handler = astOf(handler);
|
|
const handlerOps = new Array();
|
|
let handlerExprs = handler instanceof Chain ? handler.expressions : [handler];
|
|
if (handlerExprs.length === 0) {
|
|
throw new Error('Expected listener to have non-empty expression list.');
|
|
}
|
|
const expressions = handlerExprs.map((expr) => convertAst(expr, unit.job, handlerSpan));
|
|
const returnExpr = expressions.pop();
|
|
handlerOps.push(...expressions.map((e) => createStatementOp(new ExpressionStatement(e, e.sourceSpan))));
|
|
handlerOps.push(createStatementOp(new ReturnStatement(returnExpr, returnExpr.sourceSpan)));
|
|
return handlerOps;
|
|
}
|
|
function makeTwoWayListenerHandlerOps(unit, handler, handlerSpan) {
|
|
handler = astOf(handler);
|
|
const handlerOps = new Array();
|
|
if (handler instanceof Chain) {
|
|
if (handler.expressions.length === 1) {
|
|
handler = handler.expressions[0];
|
|
}
|
|
else {
|
|
// This is validated during parsing already, but we do it here just in case.
|
|
throw new Error('Expected two-way listener to have a single expression.');
|
|
}
|
|
}
|
|
const handlerExpr = convertAst(handler, unit.job, handlerSpan);
|
|
const eventReference = new LexicalReadExpr('$event');
|
|
const twoWaySetExpr = new TwoWayBindingSetExpr(handlerExpr, eventReference);
|
|
handlerOps.push(createStatementOp(new ExpressionStatement(twoWaySetExpr)));
|
|
handlerOps.push(createStatementOp(new ReturnStatement(eventReference)));
|
|
return handlerOps;
|
|
}
|
|
function astOf(ast) {
|
|
return ast instanceof ASTWithSource ? ast.ast : ast;
|
|
}
|
|
/**
|
|
* Process all of the local references on an element-like structure in the template AST and
|
|
* convert them to their IR representation.
|
|
*/
|
|
function ingestReferences(op, element) {
|
|
assertIsArray(op.localRefs);
|
|
for (const { name, value } of element.references) {
|
|
op.localRefs.push({
|
|
name,
|
|
target: value,
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Assert that the given value is an array.
|
|
*/
|
|
function assertIsArray(value) {
|
|
if (!Array.isArray(value)) {
|
|
throw new Error(`AssertionError: expected an array`);
|
|
}
|
|
}
|
|
/**
|
|
* Creates an absolute `ParseSourceSpan` from the relative `ParseSpan`.
|
|
*
|
|
* `ParseSpan` objects are relative to the start of the expression.
|
|
* This method converts these to full `ParseSourceSpan` objects that
|
|
* show where the span is within the overall source file.
|
|
*
|
|
* @param span the relative span to convert.
|
|
* @param baseSourceSpan a span corresponding to the base of the expression tree.
|
|
* @returns a `ParseSourceSpan` for the given span or null if no `baseSourceSpan` was provided.
|
|
*/
|
|
function convertSourceSpan(span, baseSourceSpan) {
|
|
if (baseSourceSpan === null) {
|
|
return null;
|
|
}
|
|
const start = baseSourceSpan.start.moveBy(span.start);
|
|
const end = baseSourceSpan.start.moveBy(span.end);
|
|
const fullStart = baseSourceSpan.fullStart.moveBy(span.start);
|
|
return new ParseSourceSpan(start, end, fullStart);
|
|
}
|
|
/**
|
|
* With the directive-based control flow users were able to conditionally project content using
|
|
* the `*` syntax. E.g. `<div *ngIf="expr" projectMe></div>` will be projected into
|
|
* `<ng-content select="[projectMe]"/>`, because the attributes and tag name from the `div` are
|
|
* copied to the template via the template creation instruction. With `@if` and `@for` that is
|
|
* not the case, because the conditional is placed *around* elements, rather than *on* them.
|
|
* The result is that content projection won't work in the same way if a user converts from
|
|
* `*ngIf` to `@if`.
|
|
*
|
|
* This function aims to cover the most common case by doing the same copying when a control flow
|
|
* node has *one and only one* root element or template node.
|
|
*
|
|
* This approach comes with some caveats:
|
|
* 1. As soon as any other node is added to the root, the copying behavior won't work anymore.
|
|
* A diagnostic will be added to flag cases like this and to explain how to work around it.
|
|
* 2. If `preserveWhitespaces` is enabled, it's very likely that indentation will break this
|
|
* workaround, because it'll include an additional text node as the first child. We can work
|
|
* around it here, but in a discussion it was decided not to, because the user explicitly opted
|
|
* into preserving the whitespace and we would have to drop it from the generated code.
|
|
* The diagnostic mentioned point in #1 will flag such cases to users.
|
|
*
|
|
* @returns Tag name to be used for the control flow template.
|
|
*/
|
|
function ingestControlFlowInsertionPoint(unit, xref, node) {
|
|
let root = null;
|
|
for (const child of node.children) {
|
|
// Skip over comment nodes and @let declarations since
|
|
// it doesn't matter where they end up in the DOM.
|
|
if (child instanceof Comment$1 || child instanceof LetDeclaration$1) {
|
|
continue;
|
|
}
|
|
// We can only infer the tag name/attributes if there's a single root node.
|
|
if (root !== null) {
|
|
return null;
|
|
}
|
|
// Root nodes can only elements or templates with a tag name (e.g. `<div *foo></div>`).
|
|
if (child instanceof Element$1 || (child instanceof Template && child.tagName !== null)) {
|
|
root = child;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
// If we've found a single root node, its tag name and attributes can be
|
|
// copied to the surrounding template to be used for content projection.
|
|
if (root !== null) {
|
|
// Collect the static attributes for content projection purposes.
|
|
for (const attr of root.attributes) {
|
|
if (!attr.name.startsWith(ANIMATE_PREFIX$1)) {
|
|
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
|
|
unit.update.push(createBindingOp(xref, BindingKind.Attribute, attr.name, literal(attr.value), null, securityContext, true, false, null, asMessage(attr.i18n), attr.sourceSpan));
|
|
}
|
|
}
|
|
// Also collect the inputs since they participate in content projection as well.
|
|
// Note that TDB used to collect the outputs as well, but it wasn't passing them into
|
|
// the template instruction. Here we just don't collect them.
|
|
for (const attr of root.inputs) {
|
|
if (attr.type !== BindingType.LegacyAnimation &&
|
|
attr.type !== BindingType.Animation &&
|
|
attr.type !== BindingType.Attribute) {
|
|
const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true);
|
|
unit.create.push(createExtractedAttributeOp(xref, BindingKind.Property, null, attr.name, null, null, null, securityContext));
|
|
}
|
|
}
|
|
const tagName = root instanceof Element$1 ? root.name : root.tagName;
|
|
// Don't pass along `ng-template` tag name since it enables directive matching.
|
|
return tagName === NG_TEMPLATE_TAG_NAME ? null : tagName;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
/**
|
|
* Whether to produce instructions that will attach the source location to each DOM node.
|
|
*
|
|
* !!!Important!!! at the time of writing this flag isn't exposed externally, but internal debug
|
|
* tools enable it via a local change. Any modifications to this flag need to update the
|
|
* internal tooling as well.
|
|
*/
|
|
let ENABLE_TEMPLATE_SOURCE_LOCATIONS = false;
|
|
/**
|
|
* Utility function to enable source locations. Intended to be used **only** inside unit tests.
|
|
*/
|
|
function setEnableTemplateSourceLocations(value) {
|
|
ENABLE_TEMPLATE_SOURCE_LOCATIONS = value;
|
|
}
|
|
/** Gets whether template source locations are enabled. */
|
|
function getTemplateSourceLocationsEnabled() {
|
|
return ENABLE_TEMPLATE_SOURCE_LOCATIONS;
|
|
}
|
|
|
|
// if (rf & flags) { .. }
|
|
function renderFlagCheckIfStmt(flags, statements) {
|
|
return ifStmt(variable(RENDER_FLAGS).bitwiseAnd(literal(flags), null), statements);
|
|
}
|
|
/**
|
|
* Translates query flags into `TQueryFlags` type in
|
|
* packages/core/src/render3/interfaces/query.ts
|
|
* @param query
|
|
*/
|
|
function toQueryFlags(query) {
|
|
return ((query.descendants ? 1 /* QueryFlags.descendants */ : 0 /* QueryFlags.none */) |
|
|
(query.static ? 2 /* QueryFlags.isStatic */ : 0 /* QueryFlags.none */) |
|
|
(query.emitDistinctChangesOnly ? 4 /* QueryFlags.emitDistinctChangesOnly */ : 0 /* QueryFlags.none */));
|
|
}
|
|
function getQueryPredicate(query, constantPool) {
|
|
if (Array.isArray(query.predicate)) {
|
|
let predicate = [];
|
|
query.predicate.forEach((selector) => {
|
|
// Each item in predicates array may contain strings with comma-separated refs
|
|
// (for ex. 'ref, ref1, ..., refN'), thus we extract individual refs and store them
|
|
// as separate array entities
|
|
const selectors = selector.split(',').map((token) => literal(token.trim()));
|
|
predicate.push(...selectors);
|
|
});
|
|
return constantPool.getConstLiteral(literalArr(predicate), true);
|
|
}
|
|
else {
|
|
// The original predicate may have been wrapped in a `forwardRef()` call.
|
|
switch (query.predicate.forwardRef) {
|
|
case 0 /* ForwardRefHandling.None */:
|
|
case 2 /* ForwardRefHandling.Unwrapped */:
|
|
return query.predicate.expression;
|
|
case 1 /* ForwardRefHandling.Wrapped */:
|
|
return importExpr(Identifiers.resolveForwardRef).callFn([query.predicate.expression]);
|
|
}
|
|
}
|
|
}
|
|
function createQueryCreateCall(query, constantPool, queryTypeFns, prependParams) {
|
|
const parameters = [];
|
|
if (prependParams !== undefined) {
|
|
parameters.push(...prependParams);
|
|
}
|
|
if (query.isSignal) {
|
|
parameters.push(new ReadPropExpr(variable(CONTEXT_NAME), query.propertyName));
|
|
}
|
|
parameters.push(getQueryPredicate(query, constantPool), literal(toQueryFlags(query)));
|
|
if (query.read) {
|
|
parameters.push(query.read);
|
|
}
|
|
const queryCreateFn = query.isSignal ? queryTypeFns.signalBased : queryTypeFns.nonSignal;
|
|
return importExpr(queryCreateFn).callFn(parameters);
|
|
}
|
|
const queryAdvancePlaceholder = Symbol('queryAdvancePlaceholder');
|
|
/**
|
|
* Collapses query advance placeholders in a list of statements.
|
|
*
|
|
* This allows for less generated code because multiple sibling query advance
|
|
* statements can be collapsed into a single call with the count as argument.
|
|
*
|
|
* e.g.
|
|
*
|
|
* ```ts
|
|
* bla();
|
|
* queryAdvance();
|
|
* queryAdvance();
|
|
* bla();
|
|
* ```
|
|
*
|
|
* --> will turn into
|
|
*
|
|
* ```ts
|
|
* bla();
|
|
* queryAdvance(2);
|
|
* bla();
|
|
* ```
|
|
*/
|
|
function collapseAdvanceStatements(statements) {
|
|
const result = [];
|
|
let advanceCollapseCount = 0;
|
|
const flushAdvanceCount = () => {
|
|
if (advanceCollapseCount > 0) {
|
|
result.unshift(importExpr(Identifiers.queryAdvance)
|
|
.callFn(advanceCollapseCount === 1 ? [] : [literal(advanceCollapseCount)])
|
|
.toStmt());
|
|
advanceCollapseCount = 0;
|
|
}
|
|
};
|
|
// Iterate through statements in reverse and collapse advance placeholders.
|
|
for (let i = statements.length - 1; i >= 0; i--) {
|
|
const st = statements[i];
|
|
if (st === queryAdvancePlaceholder) {
|
|
advanceCollapseCount++;
|
|
}
|
|
else {
|
|
flushAdvanceCount();
|
|
result.unshift(st);
|
|
}
|
|
}
|
|
flushAdvanceCount();
|
|
return result;
|
|
}
|
|
// Define and update any view queries
|
|
function createViewQueriesFunction(viewQueries, constantPool, name) {
|
|
const createStatements = [];
|
|
const updateStatements = [];
|
|
const tempAllocator = temporaryAllocator((st) => updateStatements.push(st), TEMPORARY_NAME);
|
|
viewQueries.forEach((query) => {
|
|
// creation call, e.g. r3.viewQuery(somePredicate, true) or
|
|
// r3.viewQuerySignal(ctx.prop, somePredicate, true);
|
|
const queryDefinitionCall = createQueryCreateCall(query, constantPool, {
|
|
signalBased: Identifiers.viewQuerySignal,
|
|
nonSignal: Identifiers.viewQuery,
|
|
});
|
|
createStatements.push(queryDefinitionCall.toStmt());
|
|
// Signal queries update lazily and we just advance the index.
|
|
if (query.isSignal) {
|
|
updateStatements.push(queryAdvancePlaceholder);
|
|
return;
|
|
}
|
|
// update, e.g. (r3.queryRefresh(tmp = r3.loadQuery()) && (ctx.someDir = tmp));
|
|
const temporary = tempAllocator();
|
|
const getQueryList = importExpr(Identifiers.loadQuery).callFn([]);
|
|
const refresh = importExpr(Identifiers.queryRefresh).callFn([temporary.set(getQueryList)]);
|
|
const updateDirective = variable(CONTEXT_NAME)
|
|
.prop(query.propertyName)
|
|
.set(query.first ? temporary.prop('first') : temporary);
|
|
updateStatements.push(refresh.and(updateDirective).toStmt());
|
|
});
|
|
const viewQueryFnName = name ? `${name}_Query` : null;
|
|
return fn([new FnParam(RENDER_FLAGS, NUMBER_TYPE), new FnParam(CONTEXT_NAME, null)], [
|
|
renderFlagCheckIfStmt(1 /* core.RenderFlags.Create */, createStatements),
|
|
renderFlagCheckIfStmt(2 /* core.RenderFlags.Update */, collapseAdvanceStatements(updateStatements)),
|
|
], INFERRED_TYPE, null, viewQueryFnName);
|
|
}
|
|
// Define and update any content queries
|
|
function createContentQueriesFunction(queries, constantPool, name) {
|
|
const createStatements = [];
|
|
const updateStatements = [];
|
|
const tempAllocator = temporaryAllocator((st) => updateStatements.push(st), TEMPORARY_NAME);
|
|
for (const query of queries) {
|
|
// creation, e.g. r3.contentQuery(dirIndex, somePredicate, true, null) or
|
|
// r3.contentQuerySignal(dirIndex, propName, somePredicate, <flags>, <read>).
|
|
createStatements.push(createQueryCreateCall(query, constantPool, { nonSignal: Identifiers.contentQuery, signalBased: Identifiers.contentQuerySignal },
|
|
/* prependParams */ [variable('dirIndex')]).toStmt());
|
|
// Signal queries update lazily and we just advance the index.
|
|
if (query.isSignal) {
|
|
updateStatements.push(queryAdvancePlaceholder);
|
|
continue;
|
|
}
|
|
// update, e.g. (r3.queryRefresh(tmp = r3.loadQuery()) && (ctx.someDir = tmp));
|
|
const temporary = tempAllocator();
|
|
const getQueryList = importExpr(Identifiers.loadQuery).callFn([]);
|
|
const refresh = importExpr(Identifiers.queryRefresh).callFn([temporary.set(getQueryList)]);
|
|
const updateDirective = variable(CONTEXT_NAME)
|
|
.prop(query.propertyName)
|
|
.set(query.first ? temporary.prop('first') : temporary);
|
|
updateStatements.push(refresh.and(updateDirective).toStmt());
|
|
}
|
|
const contentQueriesFnName = name ? `${name}_ContentQueries` : null;
|
|
return fn([
|
|
new FnParam(RENDER_FLAGS, NUMBER_TYPE),
|
|
new FnParam(CONTEXT_NAME, null),
|
|
new FnParam('dirIndex', null),
|
|
], [
|
|
renderFlagCheckIfStmt(1 /* core.RenderFlags.Create */, createStatements),
|
|
renderFlagCheckIfStmt(2 /* core.RenderFlags.Update */, collapseAdvanceStatements(updateStatements)),
|
|
], INFERRED_TYPE, null, contentQueriesFnName);
|
|
}
|
|
|
|
class HtmlParser extends Parser$1 {
|
|
constructor() {
|
|
super(getHtmlTagDefinition);
|
|
}
|
|
parse(source, url, options) {
|
|
return super.parse(source, url, options);
|
|
}
|
|
}
|
|
|
|
const PROPERTY_PARTS_SEPARATOR = '.';
|
|
const ATTRIBUTE_PREFIX = 'attr';
|
|
const ANIMATE_PREFIX = 'animate';
|
|
const CLASS_PREFIX = 'class';
|
|
const STYLE_PREFIX = 'style';
|
|
const TEMPLATE_ATTR_PREFIX$1 = '*';
|
|
const LEGACY_ANIMATE_PROP_PREFIX = 'animate-';
|
|
/**
|
|
* Parses bindings in templates and in the directive host area.
|
|
*/
|
|
class BindingParser {
|
|
_exprParser;
|
|
_interpolationConfig;
|
|
_schemaRegistry;
|
|
errors;
|
|
constructor(_exprParser, _interpolationConfig, _schemaRegistry, errors) {
|
|
this._exprParser = _exprParser;
|
|
this._interpolationConfig = _interpolationConfig;
|
|
this._schemaRegistry = _schemaRegistry;
|
|
this.errors = errors;
|
|
}
|
|
get interpolationConfig() {
|
|
return this._interpolationConfig;
|
|
}
|
|
createBoundHostProperties(properties, sourceSpan) {
|
|
const boundProps = [];
|
|
for (const propName of Object.keys(properties)) {
|
|
const expression = properties[propName];
|
|
if (typeof expression === 'string') {
|
|
this.parsePropertyBinding(propName, expression, true, false, sourceSpan, sourceSpan.start.offset, undefined, [],
|
|
// Use the `sourceSpan` for `keySpan`. This isn't really accurate, but neither is the
|
|
// sourceSpan, as it represents the sourceSpan of the host itself rather than the
|
|
// source of the host binding (which doesn't exist in the template). Regardless,
|
|
// neither of these values are used in Ivy but are only here to satisfy the function
|
|
// signature. This should likely be refactored in the future so that `sourceSpan`
|
|
// isn't being used inaccurately.
|
|
boundProps, sourceSpan);
|
|
}
|
|
else {
|
|
this._reportError(`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, sourceSpan);
|
|
}
|
|
}
|
|
return boundProps;
|
|
}
|
|
createDirectiveHostEventAsts(hostListeners, sourceSpan) {
|
|
const targetEvents = [];
|
|
for (const propName of Object.keys(hostListeners)) {
|
|
const expression = hostListeners[propName];
|
|
if (typeof expression === 'string') {
|
|
// Use the `sourceSpan` for `keySpan` and `handlerSpan`. This isn't really accurate, but
|
|
// neither is the `sourceSpan`, as it represents the `sourceSpan` of the host itself
|
|
// rather than the source of the host binding (which doesn't exist in the template).
|
|
// Regardless, neither of these values are used in Ivy but are only here to satisfy the
|
|
// function signature. This should likely be refactored in the future so that `sourceSpan`
|
|
// isn't being used inaccurately.
|
|
this.parseEvent(propName, expression,
|
|
/* isAssignmentEvent */ false, sourceSpan, sourceSpan, [], targetEvents, sourceSpan);
|
|
}
|
|
else {
|
|
this._reportError(`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, sourceSpan);
|
|
}
|
|
}
|
|
return targetEvents;
|
|
}
|
|
parseInterpolation(value, sourceSpan, interpolatedTokens) {
|
|
const absoluteOffset = sourceSpan.fullStart.offset;
|
|
try {
|
|
const ast = this._exprParser.parseInterpolation(value, sourceSpan, absoluteOffset, interpolatedTokens, this._interpolationConfig);
|
|
if (ast) {
|
|
this.errors.push(...ast.errors);
|
|
}
|
|
return ast;
|
|
}
|
|
catch (e) {
|
|
this._reportError(`${e}`, sourceSpan);
|
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceSpan, 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, sourceSpan) {
|
|
const absoluteOffset = sourceSpan.start.offset;
|
|
try {
|
|
const ast = this._exprParser.parseInterpolationExpression(expression, sourceSpan, absoluteOffset);
|
|
if (ast) {
|
|
this.errors.push(...ast.errors);
|
|
}
|
|
return ast;
|
|
}
|
|
catch (e) {
|
|
this._reportError(`${e}`, sourceSpan);
|
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceSpan, absoluteOffset);
|
|
}
|
|
}
|
|
/**
|
|
* Parses the bindings in a microsyntax expression, and converts them to
|
|
* `ParsedProperty` or `ParsedVariable`.
|
|
*
|
|
* @param tplKey template binding name
|
|
* @param tplValue template binding value
|
|
* @param sourceSpan span of template binding relative to entire the template
|
|
* @param absoluteValueOffset start of the tplValue relative to the entire template
|
|
* @param targetMatchableAttrs potential attributes to match in the template
|
|
* @param targetProps target property bindings in the template
|
|
* @param targetVars target variables in the template
|
|
*/
|
|
parseInlineTemplateBinding(tplKey, tplValue, sourceSpan, absoluteValueOffset, targetMatchableAttrs, targetProps, targetVars, isIvyAst) {
|
|
const absoluteKeyOffset = sourceSpan.start.offset + TEMPLATE_ATTR_PREFIX$1.length;
|
|
const bindings = this._parseTemplateBindings(tplKey, tplValue, sourceSpan, absoluteKeyOffset, absoluteValueOffset);
|
|
for (const binding of bindings) {
|
|
// sourceSpan is for the entire HTML attribute. bindingSpan is for a particular
|
|
// binding within the microsyntax expression so it's more narrow than sourceSpan.
|
|
const bindingSpan = moveParseSourceSpan(sourceSpan, binding.sourceSpan);
|
|
const key = binding.key.source;
|
|
const keySpan = moveParseSourceSpan(sourceSpan, binding.key.span);
|
|
if (binding instanceof VariableBinding) {
|
|
const value = binding.value ? binding.value.source : '$implicit';
|
|
const valueSpan = binding.value
|
|
? moveParseSourceSpan(sourceSpan, binding.value.span)
|
|
: undefined;
|
|
targetVars.push(new ParsedVariable(key, value, bindingSpan, keySpan, valueSpan));
|
|
}
|
|
else if (binding.value) {
|
|
const srcSpan = isIvyAst ? bindingSpan : sourceSpan;
|
|
const valueSpan = moveParseSourceSpan(sourceSpan, binding.value.ast.sourceSpan);
|
|
this._parsePropertyAst(key, binding.value, false, srcSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
}
|
|
else {
|
|
targetMatchableAttrs.push([key, '' /* value */]);
|
|
// Since this is a literal attribute with no RHS, source span should be
|
|
// just the key span.
|
|
this.parseLiteralAttr(key, null /* value */, keySpan, absoluteValueOffset, undefined /* valueSpan */, targetMatchableAttrs, targetProps, keySpan);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Parses the bindings in a microsyntax expression, e.g.
|
|
* ```html
|
|
* <tag *tplKey="let value1 = prop; let value2 = localVar">
|
|
* ```
|
|
*
|
|
* @param tplKey template binding name
|
|
* @param tplValue template binding value
|
|
* @param sourceSpan span of template binding relative to entire the template
|
|
* @param absoluteKeyOffset start of the `tplKey`
|
|
* @param absoluteValueOffset start of the `tplValue`
|
|
*/
|
|
_parseTemplateBindings(tplKey, tplValue, sourceSpan, absoluteKeyOffset, absoluteValueOffset) {
|
|
try {
|
|
const bindingsResult = this._exprParser.parseTemplateBindings(tplKey, tplValue, sourceSpan, absoluteKeyOffset, absoluteValueOffset);
|
|
bindingsResult.errors.forEach((e) => this.errors.push(e));
|
|
bindingsResult.warnings.forEach((warning) => {
|
|
this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING);
|
|
});
|
|
return bindingsResult.templateBindings;
|
|
}
|
|
catch (e) {
|
|
this._reportError(`${e}`, sourceSpan);
|
|
return [];
|
|
}
|
|
}
|
|
parseLiteralAttr(name, value, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, targetProps, keySpan) {
|
|
if (isLegacyAnimationLabel(name)) {
|
|
name = name.substring(1);
|
|
if (keySpan !== undefined) {
|
|
keySpan = moveParseSourceSpan(keySpan, new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset));
|
|
}
|
|
if (value) {
|
|
this._reportError(`Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
|
|
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`, sourceSpan, ParseErrorLevel.ERROR);
|
|
}
|
|
this._parseLegacyAnimation(name, value, sourceSpan, absoluteOffset, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
}
|
|
else {
|
|
targetProps.push(new ParsedProperty(name, this._exprParser.wrapLiteralPrimitive(value, '', absoluteOffset), ParsedPropertyType.LITERAL_ATTR, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
}
|
|
parsePropertyBinding(name, expression, isHost, isPartOfAssignmentBinding, sourceSpan, absoluteOffset, valueSpan, targetMatchableAttrs, targetProps, keySpan) {
|
|
if (name.length === 0) {
|
|
this._reportError(`Property name is missing in binding`, sourceSpan);
|
|
}
|
|
let isLegacyAnimationProp = false;
|
|
if (name.startsWith(LEGACY_ANIMATE_PROP_PREFIX)) {
|
|
isLegacyAnimationProp = true;
|
|
name = name.substring(LEGACY_ANIMATE_PROP_PREFIX.length);
|
|
if (keySpan !== undefined) {
|
|
keySpan = moveParseSourceSpan(keySpan, new AbsoluteSourceSpan(keySpan.start.offset + LEGACY_ANIMATE_PROP_PREFIX.length, keySpan.end.offset));
|
|
}
|
|
}
|
|
else if (isLegacyAnimationLabel(name)) {
|
|
isLegacyAnimationProp = true;
|
|
name = name.substring(1);
|
|
if (keySpan !== undefined) {
|
|
keySpan = moveParseSourceSpan(keySpan, new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset));
|
|
}
|
|
}
|
|
if (isLegacyAnimationProp) {
|
|
this._parseLegacyAnimation(name, expression, sourceSpan, absoluteOffset, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
}
|
|
else if (name.startsWith(`${ANIMATE_PREFIX}${PROPERTY_PARTS_SEPARATOR}`)) {
|
|
this._parseAnimation(name, this.parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset), sourceSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
}
|
|
else {
|
|
this._parsePropertyAst(name, this.parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset), isPartOfAssignmentBinding, sourceSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
}
|
|
}
|
|
parsePropertyInterpolation(name, value, sourceSpan, valueSpan, targetMatchableAttrs, targetProps, keySpan, interpolatedTokens) {
|
|
const expr = this.parseInterpolation(value, valueSpan || sourceSpan, interpolatedTokens);
|
|
if (expr) {
|
|
this._parsePropertyAst(name, expr, false, sourceSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
_parsePropertyAst(name, ast, isPartOfAssignmentBinding, sourceSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps) {
|
|
targetMatchableAttrs.push([name, ast.source]);
|
|
targetProps.push(new ParsedProperty(name, ast, isPartOfAssignmentBinding ? ParsedPropertyType.TWO_WAY : ParsedPropertyType.DEFAULT, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
_parseAnimation(name, ast, sourceSpan, keySpan, valueSpan, targetMatchableAttrs, targetProps) {
|
|
targetMatchableAttrs.push([name, ast.source]);
|
|
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
_parseLegacyAnimation(name, expression, sourceSpan, absoluteOffset, keySpan, valueSpan, targetMatchableAttrs, targetProps) {
|
|
if (name.length === 0) {
|
|
this._reportError('Animation trigger is missing', sourceSpan);
|
|
}
|
|
// This will occur when a @trigger is not paired with an expression.
|
|
// For animations it is valid to not have an expression since */void
|
|
// states will be applied by angular when the element is attached/detached
|
|
const ast = this.parseBinding(expression || 'undefined', false, valueSpan || sourceSpan, absoluteOffset);
|
|
targetMatchableAttrs.push([name, ast.source]);
|
|
targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.LEGACY_ANIMATION, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
parseBinding(value, isHostBinding, sourceSpan, absoluteOffset) {
|
|
try {
|
|
const ast = isHostBinding
|
|
? this._exprParser.parseSimpleBinding(value, sourceSpan, absoluteOffset, this._interpolationConfig)
|
|
: this._exprParser.parseBinding(value, sourceSpan, absoluteOffset, this._interpolationConfig);
|
|
if (ast) {
|
|
this.errors.push(...ast.errors);
|
|
}
|
|
return ast;
|
|
}
|
|
catch (e) {
|
|
this._reportError(`${e}`, sourceSpan);
|
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceSpan, absoluteOffset);
|
|
}
|
|
}
|
|
createBoundElementProperty(elementSelector, boundProp, skipValidation = false, mapPropertyName = true) {
|
|
if (boundProp.isLegacyAnimation) {
|
|
return new BoundElementProperty(boundProp.name, BindingType.LegacyAnimation, SecurityContext.NONE, boundProp.expression, null, boundProp.sourceSpan, boundProp.keySpan, boundProp.valueSpan);
|
|
}
|
|
let unit = null;
|
|
let bindingType = undefined;
|
|
let boundPropertyName = null;
|
|
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
|
|
let securityContexts = undefined;
|
|
// Check for special cases (prefix style, attr, class)
|
|
if (parts.length > 1) {
|
|
if (parts[0] == ATTRIBUTE_PREFIX) {
|
|
boundPropertyName = parts.slice(1).join(PROPERTY_PARTS_SEPARATOR);
|
|
if (!skipValidation) {
|
|
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
|
|
}
|
|
securityContexts = calcPossibleSecurityContexts(this._schemaRegistry, elementSelector, boundPropertyName, true);
|
|
const nsSeparatorIdx = boundPropertyName.indexOf(':');
|
|
if (nsSeparatorIdx > -1) {
|
|
const ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
|
const name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
|
boundPropertyName = mergeNsAndName(ns, name);
|
|
}
|
|
bindingType = BindingType.Attribute;
|
|
}
|
|
else if (parts[0] == CLASS_PREFIX) {
|
|
boundPropertyName = parts[1];
|
|
bindingType = BindingType.Class;
|
|
securityContexts = [SecurityContext.NONE];
|
|
}
|
|
else if (parts[0] == STYLE_PREFIX) {
|
|
unit = parts.length > 2 ? parts[2] : null;
|
|
boundPropertyName = parts[1];
|
|
bindingType = BindingType.Style;
|
|
securityContexts = [SecurityContext.STYLE];
|
|
}
|
|
else if (parts[0] == ANIMATE_PREFIX) {
|
|
boundPropertyName = boundProp.name;
|
|
bindingType = BindingType.Animation;
|
|
securityContexts = [SecurityContext.NONE];
|
|
}
|
|
}
|
|
// If not a special case, use the full property name
|
|
if (boundPropertyName === null) {
|
|
const mappedPropName = this._schemaRegistry.getMappedPropName(boundProp.name);
|
|
boundPropertyName = mapPropertyName ? mappedPropName : boundProp.name;
|
|
securityContexts = calcPossibleSecurityContexts(this._schemaRegistry, elementSelector, mappedPropName, false);
|
|
bindingType =
|
|
boundProp.type === ParsedPropertyType.TWO_WAY ? BindingType.TwoWay : BindingType.Property;
|
|
if (!skipValidation) {
|
|
this._validatePropertyOrAttributeName(mappedPropName, boundProp.sourceSpan, false);
|
|
}
|
|
}
|
|
return new BoundElementProperty(boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit, boundProp.sourceSpan, boundProp.keySpan, boundProp.valueSpan);
|
|
}
|
|
parseEvent(name, expression, isAssignmentEvent, sourceSpan, handlerSpan, targetMatchableAttrs, targetEvents, keySpan) {
|
|
if (name.length === 0) {
|
|
this._reportError(`Event name is missing in binding`, sourceSpan);
|
|
}
|
|
if (isLegacyAnimationLabel(name)) {
|
|
name = name.slice(1);
|
|
if (keySpan !== undefined) {
|
|
keySpan = moveParseSourceSpan(keySpan, new AbsoluteSourceSpan(keySpan.start.offset + 1, keySpan.end.offset));
|
|
}
|
|
this._parseLegacyAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents, keySpan);
|
|
}
|
|
else {
|
|
this._parseRegularEvent(name, expression, isAssignmentEvent, sourceSpan, handlerSpan, targetMatchableAttrs, targetEvents, keySpan);
|
|
}
|
|
}
|
|
calcPossibleSecurityContexts(selector, propName, isAttribute) {
|
|
const prop = this._schemaRegistry.getMappedPropName(propName);
|
|
return calcPossibleSecurityContexts(this._schemaRegistry, selector, prop, isAttribute);
|
|
}
|
|
parseEventListenerName(rawName) {
|
|
const [target, eventName] = splitAtColon(rawName, [null, rawName]);
|
|
return { eventName: eventName, target };
|
|
}
|
|
parseLegacyAnimationEventName(rawName) {
|
|
const matches = splitAtPeriod(rawName, [rawName, null]);
|
|
return { eventName: matches[0], phase: matches[1] === null ? null : matches[1].toLowerCase() };
|
|
}
|
|
_parseLegacyAnimationEvent(name, expression, sourceSpan, handlerSpan, targetEvents, keySpan) {
|
|
const { eventName, phase } = this.parseLegacyAnimationEventName(name);
|
|
const ast = this._parseAction(expression, handlerSpan);
|
|
targetEvents.push(new ParsedEvent(eventName, phase, ParsedEventType.LegacyAnimation, ast, sourceSpan, handlerSpan, keySpan));
|
|
if (eventName.length === 0) {
|
|
this._reportError(`Animation event name is missing in binding`, sourceSpan);
|
|
}
|
|
if (phase) {
|
|
if (phase !== 'start' && phase !== 'done') {
|
|
this._reportError(`The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`, sourceSpan);
|
|
}
|
|
}
|
|
else {
|
|
this._reportError(`The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`, sourceSpan);
|
|
}
|
|
}
|
|
_parseRegularEvent(name, expression, isAssignmentEvent, sourceSpan, handlerSpan, targetMatchableAttrs, targetEvents, keySpan) {
|
|
// long format: 'target: eventName'
|
|
const { eventName, target } = this.parseEventListenerName(name);
|
|
const prevErrorCount = this.errors.length;
|
|
const ast = this._parseAction(expression, handlerSpan);
|
|
const isValid = this.errors.length === prevErrorCount;
|
|
targetMatchableAttrs.push([name, ast.source]);
|
|
// Don't try to validate assignment events if there were other
|
|
// parsing errors to avoid adding more noise to the error logs.
|
|
if (isAssignmentEvent && isValid && !this._isAllowedAssignmentEvent(ast)) {
|
|
this._reportError('Unsupported expression in a two-way binding', sourceSpan);
|
|
}
|
|
let eventType = ParsedEventType.Regular;
|
|
if (isAssignmentEvent) {
|
|
eventType = ParsedEventType.TwoWay;
|
|
}
|
|
if (name.startsWith(`${ANIMATE_PREFIX}${PROPERTY_PARTS_SEPARATOR}`)) {
|
|
eventType = ParsedEventType.Animation;
|
|
}
|
|
targetEvents.push(new ParsedEvent(eventName, target, eventType, ast, sourceSpan, handlerSpan, keySpan));
|
|
// Don't detect directives for event names for now,
|
|
// so don't add the event name to the matchableAttrs
|
|
}
|
|
_parseAction(value, sourceSpan) {
|
|
const absoluteOffset = sourceSpan && sourceSpan.start ? sourceSpan.start.offset : 0;
|
|
try {
|
|
const ast = this._exprParser.parseAction(value, sourceSpan, absoluteOffset, this._interpolationConfig);
|
|
if (ast) {
|
|
this.errors.push(...ast.errors);
|
|
}
|
|
if (!ast || ast.ast instanceof EmptyExpr$1) {
|
|
this._reportError(`Empty expressions are not allowed`, sourceSpan);
|
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceSpan, absoluteOffset);
|
|
}
|
|
return ast;
|
|
}
|
|
catch (e) {
|
|
this._reportError(`${e}`, sourceSpan);
|
|
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceSpan, absoluteOffset);
|
|
}
|
|
}
|
|
_reportError(message, sourceSpan, level = ParseErrorLevel.ERROR) {
|
|
this.errors.push(new ParseError(sourceSpan, message, level));
|
|
}
|
|
/**
|
|
* @param propName the name of the property / attribute
|
|
* @param sourceSpan
|
|
* @param isAttr true when binding to an attribute
|
|
*/
|
|
_validatePropertyOrAttributeName(propName, sourceSpan, isAttr) {
|
|
const report = isAttr
|
|
? this._schemaRegistry.validateAttribute(propName)
|
|
: this._schemaRegistry.validateProperty(propName);
|
|
if (report.error) {
|
|
this._reportError(report.msg, sourceSpan, ParseErrorLevel.ERROR);
|
|
}
|
|
}
|
|
/**
|
|
* Returns whether a parsed AST is allowed to be used within the event side of a two-way binding.
|
|
* @param ast Parsed AST to be checked.
|
|
*/
|
|
_isAllowedAssignmentEvent(ast) {
|
|
if (ast instanceof ASTWithSource) {
|
|
return this._isAllowedAssignmentEvent(ast.ast);
|
|
}
|
|
if (ast instanceof NonNullAssert) {
|
|
return this._isAllowedAssignmentEvent(ast.expression);
|
|
}
|
|
if (ast instanceof Call &&
|
|
ast.args.length === 1 &&
|
|
ast.receiver instanceof PropertyRead &&
|
|
ast.receiver.name === '$any' &&
|
|
ast.receiver.receiver instanceof ImplicitReceiver &&
|
|
!(ast.receiver.receiver instanceof ThisReceiver)) {
|
|
return this._isAllowedAssignmentEvent(ast.args[0]);
|
|
}
|
|
if (ast instanceof PropertyRead || ast instanceof KeyedRead) {
|
|
if (!hasRecursiveSafeReceiver(ast)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
function hasRecursiveSafeReceiver(ast) {
|
|
if (ast instanceof SafePropertyRead || ast instanceof SafeKeyedRead) {
|
|
return true;
|
|
}
|
|
if (ast instanceof ParenthesizedExpression) {
|
|
return hasRecursiveSafeReceiver(ast.expression);
|
|
}
|
|
if (ast instanceof PropertyRead || ast instanceof KeyedRead || ast instanceof Call) {
|
|
return hasRecursiveSafeReceiver(ast.receiver);
|
|
}
|
|
return false;
|
|
}
|
|
function isLegacyAnimationLabel(name) {
|
|
return name[0] == '@';
|
|
}
|
|
function calcPossibleSecurityContexts(registry, selector, propName, isAttribute) {
|
|
let ctxs;
|
|
const nameToContext = (elName) => registry.securityContext(elName, propName, isAttribute);
|
|
if (selector === null) {
|
|
ctxs = registry.allKnownElementNames().map(nameToContext);
|
|
}
|
|
else {
|
|
ctxs = [];
|
|
CssSelector.parse(selector).forEach((selector) => {
|
|
const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
|
|
const notElementNames = new Set(selector.notSelectors
|
|
.filter((selector) => selector.isElementSelector())
|
|
.map((selector) => selector.element));
|
|
const possibleElementNames = elementNames.filter((elName) => !notElementNames.has(elName));
|
|
ctxs.push(...possibleElementNames.map(nameToContext));
|
|
});
|
|
}
|
|
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
|
|
}
|
|
/**
|
|
* Compute a new ParseSourceSpan based off an original `sourceSpan` by using
|
|
* absolute offsets from the specified `absoluteSpan`.
|
|
*
|
|
* @param sourceSpan original source span
|
|
* @param absoluteSpan absolute source span to move to
|
|
*/
|
|
function moveParseSourceSpan(sourceSpan, absoluteSpan) {
|
|
// The difference of two absolute offsets provide the relative offset
|
|
const startDiff = absoluteSpan.start - sourceSpan.start.offset;
|
|
const endDiff = absoluteSpan.end - sourceSpan.end.offset;
|
|
return new ParseSourceSpan(sourceSpan.start.moveBy(startDiff), sourceSpan.end.moveBy(endDiff), sourceSpan.fullStart.moveBy(startDiff), sourceSpan.details);
|
|
}
|
|
|
|
// Some of the code comes from WebComponents.JS
|
|
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
|
|
function isStyleUrlResolvable(url) {
|
|
if (url == null || url.length === 0 || url[0] == '/')
|
|
return false;
|
|
const schemeMatch = url.match(URL_WITH_SCHEMA_REGEXP);
|
|
return schemeMatch === null || schemeMatch[1] == 'package' || schemeMatch[1] == 'asset';
|
|
}
|
|
const URL_WITH_SCHEMA_REGEXP = /^([^:/?#]+):/;
|
|
|
|
const NG_CONTENT_SELECT_ATTR = 'select';
|
|
const LINK_ELEMENT = 'link';
|
|
const LINK_STYLE_REL_ATTR = 'rel';
|
|
const LINK_STYLE_HREF_ATTR = 'href';
|
|
const LINK_STYLE_REL_VALUE = 'stylesheet';
|
|
const STYLE_ELEMENT = 'style';
|
|
const SCRIPT_ELEMENT = 'script';
|
|
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
|
|
const NG_PROJECT_AS = 'ngProjectAs';
|
|
function preparseElement(ast) {
|
|
let selectAttr = null;
|
|
let hrefAttr = null;
|
|
let relAttr = null;
|
|
let nonBindable = false;
|
|
let projectAs = '';
|
|
ast.attrs.forEach((attr) => {
|
|
const lcAttrName = attr.name.toLowerCase();
|
|
if (lcAttrName == NG_CONTENT_SELECT_ATTR) {
|
|
selectAttr = attr.value;
|
|
}
|
|
else if (lcAttrName == LINK_STYLE_HREF_ATTR) {
|
|
hrefAttr = attr.value;
|
|
}
|
|
else if (lcAttrName == LINK_STYLE_REL_ATTR) {
|
|
relAttr = attr.value;
|
|
}
|
|
else if (attr.name == NG_NON_BINDABLE_ATTR) {
|
|
nonBindable = true;
|
|
}
|
|
else if (attr.name == NG_PROJECT_AS) {
|
|
if (attr.value.length > 0) {
|
|
projectAs = attr.value;
|
|
}
|
|
}
|
|
});
|
|
selectAttr = normalizeNgContentSelect(selectAttr);
|
|
const nodeName = ast.name.toLowerCase();
|
|
let type = PreparsedElementType.OTHER;
|
|
if (isNgContent(nodeName)) {
|
|
type = PreparsedElementType.NG_CONTENT;
|
|
}
|
|
else if (nodeName == STYLE_ELEMENT) {
|
|
type = PreparsedElementType.STYLE;
|
|
}
|
|
else if (nodeName == SCRIPT_ELEMENT) {
|
|
type = PreparsedElementType.SCRIPT;
|
|
}
|
|
else if (nodeName == LINK_ELEMENT && relAttr == LINK_STYLE_REL_VALUE) {
|
|
type = PreparsedElementType.STYLESHEET;
|
|
}
|
|
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable, projectAs);
|
|
}
|
|
var PreparsedElementType;
|
|
(function (PreparsedElementType) {
|
|
PreparsedElementType[PreparsedElementType["NG_CONTENT"] = 0] = "NG_CONTENT";
|
|
PreparsedElementType[PreparsedElementType["STYLE"] = 1] = "STYLE";
|
|
PreparsedElementType[PreparsedElementType["STYLESHEET"] = 2] = "STYLESHEET";
|
|
PreparsedElementType[PreparsedElementType["SCRIPT"] = 3] = "SCRIPT";
|
|
PreparsedElementType[PreparsedElementType["OTHER"] = 4] = "OTHER";
|
|
})(PreparsedElementType || (PreparsedElementType = {}));
|
|
class PreparsedElement {
|
|
type;
|
|
selectAttr;
|
|
hrefAttr;
|
|
nonBindable;
|
|
projectAs;
|
|
constructor(type, selectAttr, hrefAttr, nonBindable, projectAs) {
|
|
this.type = type;
|
|
this.selectAttr = selectAttr;
|
|
this.hrefAttr = hrefAttr;
|
|
this.nonBindable = nonBindable;
|
|
this.projectAs = projectAs;
|
|
}
|
|
}
|
|
function normalizeNgContentSelect(selectAttr) {
|
|
if (selectAttr === null || selectAttr.length === 0) {
|
|
return '*';
|
|
}
|
|
return selectAttr;
|
|
}
|
|
|
|
/** Pattern for the expression in a for loop block. */
|
|
const FOR_LOOP_EXPRESSION_PATTERN = /^\s*([0-9A-Za-z_$]*)\s+of\s+([\S\s]*)/;
|
|
/** Pattern for the tracking expression in a for loop block. */
|
|
const FOR_LOOP_TRACK_PATTERN = /^track\s+([\S\s]*)/;
|
|
/** Pattern for the `as` expression in a conditional block. */
|
|
const CONDITIONAL_ALIAS_PATTERN = /^(as\s+)(.*)/;
|
|
/** Pattern used to identify an `else if` block. */
|
|
const ELSE_IF_PATTERN = /^else[^\S\r\n]+if/;
|
|
/** Pattern used to identify a `let` parameter. */
|
|
const FOR_LOOP_LET_PATTERN = /^let\s+([\S\s]*)/;
|
|
/** Pattern used to validate a JavaScript identifier. */
|
|
const IDENTIFIER_PATTERN = /^[$A-Z_][0-9A-Z_$]*$/i;
|
|
/**
|
|
* Pattern to group a string into leading whitespace, non whitespace, and trailing whitespace.
|
|
* Useful for getting the variable name span when a span can contain leading and trailing space.
|
|
*/
|
|
const CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN = /(\s*)(\S+)(\s*)/;
|
|
/** Names of variables that are allowed to be used in the `let` expression of a `for` loop. */
|
|
const ALLOWED_FOR_LOOP_LET_VARIABLES = new Set([
|
|
'$index',
|
|
'$first',
|
|
'$last',
|
|
'$even',
|
|
'$odd',
|
|
'$count',
|
|
]);
|
|
/**
|
|
* Predicate function that determines if a block with
|
|
* a specific name cam be connected to a `for` block.
|
|
*/
|
|
function isConnectedForLoopBlock(name) {
|
|
return name === 'empty';
|
|
}
|
|
/**
|
|
* Predicate function that determines if a block with
|
|
* a specific name cam be connected to an `if` block.
|
|
*/
|
|
function isConnectedIfLoopBlock(name) {
|
|
return name === 'else' || ELSE_IF_PATTERN.test(name);
|
|
}
|
|
/** Creates an `if` loop block from an HTML AST node. */
|
|
function createIfBlock(ast, connectedBlocks, visitor, bindingParser) {
|
|
const errors = validateIfConnectedBlocks(connectedBlocks);
|
|
const branches = [];
|
|
const mainBlockParams = parseConditionalBlockParameters(ast, errors, bindingParser);
|
|
if (mainBlockParams !== null) {
|
|
branches.push(new IfBlockBranch(mainBlockParams.expression, visitAll(visitor, ast.children, ast.children), mainBlockParams.expressionAlias, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.nameSpan, ast.i18n));
|
|
}
|
|
for (const block of connectedBlocks) {
|
|
if (ELSE_IF_PATTERN.test(block.name)) {
|
|
const params = parseConditionalBlockParameters(block, errors, bindingParser);
|
|
if (params !== null) {
|
|
const children = visitAll(visitor, block.children, block.children);
|
|
branches.push(new IfBlockBranch(params.expression, children, params.expressionAlias, block.sourceSpan, block.startSourceSpan, block.endSourceSpan, block.nameSpan, block.i18n));
|
|
}
|
|
}
|
|
else if (block.name === 'else') {
|
|
const children = visitAll(visitor, block.children, block.children);
|
|
branches.push(new IfBlockBranch(null, children, null, block.sourceSpan, block.startSourceSpan, block.endSourceSpan, block.nameSpan, block.i18n));
|
|
}
|
|
}
|
|
// The outer IfBlock should have a span that encapsulates all branches.
|
|
const ifBlockStartSourceSpan = branches.length > 0 ? branches[0].startSourceSpan : ast.startSourceSpan;
|
|
const ifBlockEndSourceSpan = branches.length > 0 ? branches[branches.length - 1].endSourceSpan : ast.endSourceSpan;
|
|
let wholeSourceSpan = ast.sourceSpan;
|
|
const lastBranch = branches[branches.length - 1];
|
|
if (lastBranch !== undefined) {
|
|
wholeSourceSpan = new ParseSourceSpan(ifBlockStartSourceSpan.start, lastBranch.sourceSpan.end);
|
|
}
|
|
return {
|
|
node: new IfBlock(branches, wholeSourceSpan, ast.startSourceSpan, ifBlockEndSourceSpan, ast.nameSpan),
|
|
errors,
|
|
};
|
|
}
|
|
/** Creates a `for` loop block from an HTML AST node. */
|
|
function createForLoop(ast, connectedBlocks, visitor, bindingParser) {
|
|
const errors = [];
|
|
const params = parseForLoopParameters(ast, errors, bindingParser);
|
|
let node = null;
|
|
let empty = null;
|
|
for (const block of connectedBlocks) {
|
|
if (block.name === 'empty') {
|
|
if (empty !== null) {
|
|
errors.push(new ParseError(block.sourceSpan, '@for loop can only have one @empty block'));
|
|
}
|
|
else if (block.parameters.length > 0) {
|
|
errors.push(new ParseError(block.sourceSpan, '@empty block cannot have parameters'));
|
|
}
|
|
else {
|
|
empty = new ForLoopBlockEmpty(visitAll(visitor, block.children, block.children), block.sourceSpan, block.startSourceSpan, block.endSourceSpan, block.nameSpan, block.i18n);
|
|
}
|
|
}
|
|
else {
|
|
errors.push(new ParseError(block.sourceSpan, `Unrecognized @for loop block "${block.name}"`));
|
|
}
|
|
}
|
|
if (params !== null) {
|
|
if (params.trackBy === null) {
|
|
// TODO: We should not fail here, and instead try to produce some AST for the language
|
|
// service.
|
|
errors.push(new ParseError(ast.startSourceSpan, '@for loop must have a "track" expression'));
|
|
}
|
|
else {
|
|
// The `for` block has a main span that includes the `empty` branch. For only the span of the
|
|
// main `for` body, use `mainSourceSpan`.
|
|
const endSpan = empty?.endSourceSpan ?? ast.endSourceSpan;
|
|
const sourceSpan = new ParseSourceSpan(ast.sourceSpan.start, endSpan?.end ?? ast.sourceSpan.end);
|
|
validateTrackByExpression(params.trackBy.expression, params.trackBy.keywordSpan, errors);
|
|
node = new ForLoopBlock(params.itemName, params.expression, params.trackBy.expression, params.trackBy.keywordSpan, params.context, visitAll(visitor, ast.children, ast.children), empty, sourceSpan, ast.sourceSpan, ast.startSourceSpan, endSpan, ast.nameSpan, ast.i18n);
|
|
}
|
|
}
|
|
return { node, errors };
|
|
}
|
|
/** Creates a switch block from an HTML AST node. */
|
|
function createSwitchBlock(ast, visitor, bindingParser) {
|
|
const errors = validateSwitchBlock(ast);
|
|
const primaryExpression = ast.parameters.length > 0
|
|
? parseBlockParameterToBinding(ast.parameters[0], bindingParser)
|
|
: bindingParser.parseBinding('', false, ast.sourceSpan, 0);
|
|
const cases = [];
|
|
const unknownBlocks = [];
|
|
let defaultCase = null;
|
|
// Here we assume that all the blocks are valid given that we validated them above.
|
|
for (const node of ast.children) {
|
|
if (!(node instanceof Block)) {
|
|
continue;
|
|
}
|
|
if ((node.name !== 'case' || node.parameters.length === 0) && node.name !== 'default') {
|
|
unknownBlocks.push(new UnknownBlock(node.name, node.sourceSpan, node.nameSpan));
|
|
continue;
|
|
}
|
|
const expression = node.name === 'case' ? parseBlockParameterToBinding(node.parameters[0], bindingParser) : null;
|
|
const ast = new SwitchBlockCase(expression, visitAll(visitor, node.children, node.children), node.sourceSpan, node.startSourceSpan, node.endSourceSpan, node.nameSpan, node.i18n);
|
|
if (expression === null) {
|
|
defaultCase = ast;
|
|
}
|
|
else {
|
|
cases.push(ast);
|
|
}
|
|
}
|
|
// Ensure that the default case is last in the array.
|
|
if (defaultCase !== null) {
|
|
cases.push(defaultCase);
|
|
}
|
|
return {
|
|
node: new SwitchBlock(primaryExpression, cases, unknownBlocks, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.nameSpan),
|
|
errors,
|
|
};
|
|
}
|
|
/** Parses the parameters of a `for` loop block. */
|
|
function parseForLoopParameters(block, errors, bindingParser) {
|
|
if (block.parameters.length === 0) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@for loop does not have an expression'));
|
|
return null;
|
|
}
|
|
const [expressionParam, ...secondaryParams] = block.parameters;
|
|
const match = stripOptionalParentheses(expressionParam, errors)?.match(FOR_LOOP_EXPRESSION_PATTERN);
|
|
if (!match || match[2].trim().length === 0) {
|
|
errors.push(new ParseError(expressionParam.sourceSpan, 'Cannot parse expression. @for loop expression must match the pattern "<identifier> of <expression>"'));
|
|
return null;
|
|
}
|
|
const [, itemName, rawExpression] = match;
|
|
if (ALLOWED_FOR_LOOP_LET_VARIABLES.has(itemName)) {
|
|
errors.push(new ParseError(expressionParam.sourceSpan, `@for loop item name cannot be one of ${Array.from(ALLOWED_FOR_LOOP_LET_VARIABLES).join(', ')}.`));
|
|
}
|
|
// `expressionParam.expression` contains the variable declaration and the expression of the
|
|
// for...of statement, i.e. 'user of users' The variable of a ForOfStatement is _only_ the "const
|
|
// user" part and does not include "of x".
|
|
const variableName = expressionParam.expression.split(' ')[0];
|
|
const variableSpan = new ParseSourceSpan(expressionParam.sourceSpan.start, expressionParam.sourceSpan.start.moveBy(variableName.length));
|
|
const result = {
|
|
itemName: new Variable(itemName, '$implicit', variableSpan, variableSpan),
|
|
trackBy: null,
|
|
expression: parseBlockParameterToBinding(expressionParam, bindingParser, rawExpression),
|
|
context: Array.from(ALLOWED_FOR_LOOP_LET_VARIABLES, (variableName) => {
|
|
// Give ambiently-available context variables empty spans at the end of
|
|
// the start of the `for` block, since they are not explicitly defined.
|
|
const emptySpanAfterForBlockStart = new ParseSourceSpan(block.startSourceSpan.end, block.startSourceSpan.end);
|
|
return new Variable(variableName, variableName, emptySpanAfterForBlockStart, emptySpanAfterForBlockStart);
|
|
}),
|
|
};
|
|
for (const param of secondaryParams) {
|
|
const letMatch = param.expression.match(FOR_LOOP_LET_PATTERN);
|
|
if (letMatch !== null) {
|
|
const variablesSpan = new ParseSourceSpan(param.sourceSpan.start.moveBy(letMatch[0].length - letMatch[1].length), param.sourceSpan.end);
|
|
parseLetParameter(param.sourceSpan, letMatch[1], variablesSpan, itemName, result.context, errors);
|
|
continue;
|
|
}
|
|
const trackMatch = param.expression.match(FOR_LOOP_TRACK_PATTERN);
|
|
if (trackMatch !== null) {
|
|
if (result.trackBy !== null) {
|
|
errors.push(new ParseError(param.sourceSpan, '@for loop can only have one "track" expression'));
|
|
}
|
|
else {
|
|
const expression = parseBlockParameterToBinding(param, bindingParser, trackMatch[1]);
|
|
if (expression.ast instanceof EmptyExpr$1) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@for loop must have a "track" expression'));
|
|
}
|
|
const keywordSpan = new ParseSourceSpan(param.sourceSpan.start, param.sourceSpan.start.moveBy('track'.length));
|
|
result.trackBy = { expression, keywordSpan };
|
|
}
|
|
continue;
|
|
}
|
|
errors.push(new ParseError(param.sourceSpan, `Unrecognized @for loop parameter "${param.expression}"`));
|
|
}
|
|
return result;
|
|
}
|
|
function validateTrackByExpression(expression, parseSourceSpan, errors) {
|
|
const visitor = new PipeVisitor();
|
|
expression.ast.visit(visitor);
|
|
if (visitor.hasPipe) {
|
|
errors.push(new ParseError(parseSourceSpan, 'Cannot use pipes in track expressions'));
|
|
}
|
|
}
|
|
/** Parses the `let` parameter of a `for` loop block. */
|
|
function parseLetParameter(sourceSpan, expression, span, loopItemName, context, errors) {
|
|
const parts = expression.split(',');
|
|
let startSpan = span.start;
|
|
for (const part of parts) {
|
|
const expressionParts = part.split('=');
|
|
const name = expressionParts.length === 2 ? expressionParts[0].trim() : '';
|
|
const variableName = expressionParts.length === 2 ? expressionParts[1].trim() : '';
|
|
if (name.length === 0 || variableName.length === 0) {
|
|
errors.push(new ParseError(sourceSpan, `Invalid @for loop "let" parameter. Parameter should match the pattern "<name> = <variable name>"`));
|
|
}
|
|
else if (!ALLOWED_FOR_LOOP_LET_VARIABLES.has(variableName)) {
|
|
errors.push(new ParseError(sourceSpan, `Unknown "let" parameter variable "${variableName}". The allowed variables are: ${Array.from(ALLOWED_FOR_LOOP_LET_VARIABLES).join(', ')}`));
|
|
}
|
|
else if (name === loopItemName) {
|
|
errors.push(new ParseError(sourceSpan, `Invalid @for loop "let" parameter. Variable cannot be called "${loopItemName}"`));
|
|
}
|
|
else if (context.some((v) => v.name === name)) {
|
|
errors.push(new ParseError(sourceSpan, `Duplicate "let" parameter variable "${variableName}"`));
|
|
}
|
|
else {
|
|
const [, keyLeadingWhitespace, keyName] = expressionParts[0].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
|
|
const keySpan = keyLeadingWhitespace !== undefined && expressionParts.length === 2
|
|
? new ParseSourceSpan(
|
|
/* strip leading spaces */
|
|
startSpan.moveBy(keyLeadingWhitespace.length),
|
|
/* advance to end of the variable name */
|
|
startSpan.moveBy(keyLeadingWhitespace.length + keyName.length))
|
|
: span;
|
|
let valueSpan = undefined;
|
|
if (expressionParts.length === 2) {
|
|
const [, valueLeadingWhitespace, implicit] = expressionParts[1].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
|
|
valueSpan =
|
|
valueLeadingWhitespace !== undefined
|
|
? new ParseSourceSpan(startSpan.moveBy(expressionParts[0].length + 1 + valueLeadingWhitespace.length), startSpan.moveBy(expressionParts[0].length + 1 + valueLeadingWhitespace.length + implicit.length))
|
|
: undefined;
|
|
}
|
|
const sourceSpan = new ParseSourceSpan(keySpan.start, valueSpan?.end ?? keySpan.end);
|
|
context.push(new Variable(name, variableName, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
startSpan = startSpan.moveBy(part.length + 1 /* add 1 to move past the comma */);
|
|
}
|
|
}
|
|
/**
|
|
* Checks that the shape of the blocks connected to an
|
|
* `@if` block is correct. Returns an array of errors.
|
|
*/
|
|
function validateIfConnectedBlocks(connectedBlocks) {
|
|
const errors = [];
|
|
let hasElse = false;
|
|
for (let i = 0; i < connectedBlocks.length; i++) {
|
|
const block = connectedBlocks[i];
|
|
if (block.name === 'else') {
|
|
if (hasElse) {
|
|
errors.push(new ParseError(block.startSourceSpan, 'Conditional can only have one @else block'));
|
|
}
|
|
else if (connectedBlocks.length > 1 && i < connectedBlocks.length - 1) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@else block must be last inside the conditional'));
|
|
}
|
|
else if (block.parameters.length > 0) {
|
|
errors.push(new ParseError(block.startSourceSpan, '@else block cannot have parameters'));
|
|
}
|
|
hasElse = true;
|
|
}
|
|
else if (!ELSE_IF_PATTERN.test(block.name)) {
|
|
errors.push(new ParseError(block.startSourceSpan, `Unrecognized conditional block @${block.name}`));
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
/** Checks that the shape of a `switch` block is valid. Returns an array of errors. */
|
|
function validateSwitchBlock(ast) {
|
|
const errors = [];
|
|
let hasDefault = false;
|
|
if (ast.parameters.length !== 1) {
|
|
errors.push(new ParseError(ast.startSourceSpan, '@switch block must have exactly one parameter'));
|
|
return errors;
|
|
}
|
|
for (const node of ast.children) {
|
|
// Skip over comments and empty text nodes inside the switch block.
|
|
// Empty text nodes can be used for formatting while comments don't affect the runtime.
|
|
if (node instanceof Comment ||
|
|
(node instanceof Text && node.value.trim().length === 0)) {
|
|
continue;
|
|
}
|
|
if (!(node instanceof Block) || (node.name !== 'case' && node.name !== 'default')) {
|
|
errors.push(new ParseError(node.sourceSpan, '@switch block can only contain @case and @default blocks'));
|
|
continue;
|
|
}
|
|
if (node.name === 'default') {
|
|
if (hasDefault) {
|
|
errors.push(new ParseError(node.startSourceSpan, '@switch block can only have one @default block'));
|
|
}
|
|
else if (node.parameters.length > 0) {
|
|
errors.push(new ParseError(node.startSourceSpan, '@default block cannot have parameters'));
|
|
}
|
|
hasDefault = true;
|
|
}
|
|
else if (node.name === 'case' && node.parameters.length !== 1) {
|
|
errors.push(new ParseError(node.startSourceSpan, '@case block must have exactly one parameter'));
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
/**
|
|
* Parses a block parameter into a binding AST.
|
|
* @param ast Block parameter that should be parsed.
|
|
* @param bindingParser Parser that the expression should be parsed with.
|
|
* @param part Specific part of the expression that should be parsed.
|
|
*/
|
|
function parseBlockParameterToBinding(ast, bindingParser, part) {
|
|
let start;
|
|
let end;
|
|
if (typeof part === 'string') {
|
|
// Note: `lastIndexOf` here should be enough to know the start index of the expression,
|
|
// because we know that it'll be at the end of the param. Ideally we could use the `d`
|
|
// flag when matching via regex and get the index from `match.indices`, but it's unclear
|
|
// if we can use it yet since it's a relatively new feature. See:
|
|
// https://github.com/tc39/proposal-regexp-match-indices
|
|
start = Math.max(0, ast.expression.lastIndexOf(part));
|
|
end = start + part.length;
|
|
}
|
|
else {
|
|
start = 0;
|
|
end = ast.expression.length;
|
|
}
|
|
return bindingParser.parseBinding(ast.expression.slice(start, end), false, ast.sourceSpan, ast.sourceSpan.start.offset + start);
|
|
}
|
|
/** Parses the parameter of a conditional block (`if` or `else if`). */
|
|
function parseConditionalBlockParameters(block, errors, bindingParser) {
|
|
if (block.parameters.length === 0) {
|
|
errors.push(new ParseError(block.startSourceSpan, 'Conditional block does not have an expression'));
|
|
return null;
|
|
}
|
|
const expression = parseBlockParameterToBinding(block.parameters[0], bindingParser);
|
|
let expressionAlias = null;
|
|
// Start from 1 since we processed the first parameter already.
|
|
for (let i = 1; i < block.parameters.length; i++) {
|
|
const param = block.parameters[i];
|
|
const aliasMatch = param.expression.match(CONDITIONAL_ALIAS_PATTERN);
|
|
// For now conditionals can only have an `as` parameter.
|
|
// We may want to rework this later if we add more.
|
|
if (aliasMatch === null) {
|
|
errors.push(new ParseError(param.sourceSpan, `Unrecognized conditional parameter "${param.expression}"`));
|
|
}
|
|
else if (block.name !== 'if' && !ELSE_IF_PATTERN.test(block.name)) {
|
|
errors.push(new ParseError(param.sourceSpan, '"as" expression is only allowed on `@if` and `@else if` blocks'));
|
|
}
|
|
else if (expressionAlias !== null) {
|
|
errors.push(new ParseError(param.sourceSpan, 'Conditional can only have one "as" expression'));
|
|
}
|
|
else {
|
|
const name = aliasMatch[2].trim();
|
|
if (IDENTIFIER_PATTERN.test(name)) {
|
|
const variableStart = param.sourceSpan.start.moveBy(aliasMatch[1].length);
|
|
const variableSpan = new ParseSourceSpan(variableStart, variableStart.moveBy(name.length));
|
|
expressionAlias = new Variable(name, name, variableSpan, variableSpan);
|
|
}
|
|
else {
|
|
errors.push(new ParseError(param.sourceSpan, '"as" expression must be a valid JavaScript identifier'));
|
|
}
|
|
}
|
|
}
|
|
return { expression, expressionAlias };
|
|
}
|
|
/** Strips optional parentheses around from a control from expression parameter. */
|
|
function stripOptionalParentheses(param, errors) {
|
|
const expression = param.expression;
|
|
const spaceRegex = /^\s$/;
|
|
let openParens = 0;
|
|
let start = 0;
|
|
let end = expression.length - 1;
|
|
for (let i = 0; i < expression.length; i++) {
|
|
const char = expression[i];
|
|
if (char === '(') {
|
|
start = i + 1;
|
|
openParens++;
|
|
}
|
|
else if (spaceRegex.test(char)) {
|
|
continue;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
if (openParens === 0) {
|
|
return expression;
|
|
}
|
|
for (let i = expression.length - 1; i > -1; i--) {
|
|
const char = expression[i];
|
|
if (char === ')') {
|
|
end = i;
|
|
openParens--;
|
|
if (openParens === 0) {
|
|
break;
|
|
}
|
|
}
|
|
else if (spaceRegex.test(char)) {
|
|
continue;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
if (openParens !== 0) {
|
|
errors.push(new ParseError(param.sourceSpan, 'Unclosed parentheses in expression'));
|
|
return null;
|
|
}
|
|
return expression.slice(start, end);
|
|
}
|
|
class PipeVisitor extends RecursiveAstVisitor {
|
|
hasPipe = false;
|
|
visitPipe() {
|
|
this.hasPipe = true;
|
|
}
|
|
}
|
|
|
|
/** Pattern for a timing value in a trigger. */
|
|
const TIME_PATTERN = /^\d+\.?\d*(ms|s)?$/;
|
|
/** Pattern for a separator between keywords in a trigger expression. */
|
|
const SEPARATOR_PATTERN = /^\s$/;
|
|
/** Pairs of characters that form syntax that is comma-delimited. */
|
|
const COMMA_DELIMITED_SYNTAX = new Map([
|
|
[$LBRACE, $RBRACE], // Object literals
|
|
[$LBRACKET, $RBRACKET], // Array literals
|
|
[$LPAREN, $RPAREN], // Function calls
|
|
]);
|
|
/** Possible types of `on` triggers. */
|
|
var OnTriggerType;
|
|
(function (OnTriggerType) {
|
|
OnTriggerType["IDLE"] = "idle";
|
|
OnTriggerType["TIMER"] = "timer";
|
|
OnTriggerType["INTERACTION"] = "interaction";
|
|
OnTriggerType["IMMEDIATE"] = "immediate";
|
|
OnTriggerType["HOVER"] = "hover";
|
|
OnTriggerType["VIEWPORT"] = "viewport";
|
|
OnTriggerType["NEVER"] = "never";
|
|
})(OnTriggerType || (OnTriggerType = {}));
|
|
/** Parses a `when` deferred trigger. */
|
|
function parseNeverTrigger({ expression, sourceSpan }, triggers, errors) {
|
|
const neverIndex = expression.indexOf('never');
|
|
const neverSourceSpan = new ParseSourceSpan(sourceSpan.start.moveBy(neverIndex), sourceSpan.start.moveBy(neverIndex + 'never'.length));
|
|
const prefetchSpan = getPrefetchSpan(expression, sourceSpan);
|
|
const hydrateSpan = getHydrateSpan(expression, sourceSpan);
|
|
// This is here just to be safe, we shouldn't enter this function
|
|
// in the first place if a block doesn't have the "on" keyword.
|
|
if (neverIndex === -1) {
|
|
errors.push(new ParseError(sourceSpan, `Could not find "never" keyword in expression`));
|
|
}
|
|
else {
|
|
trackTrigger('never', triggers, errors, new NeverDeferredTrigger(neverSourceSpan, sourceSpan, prefetchSpan, null, hydrateSpan));
|
|
}
|
|
}
|
|
/** Parses a `when` deferred trigger. */
|
|
function parseWhenTrigger({ expression, sourceSpan }, bindingParser, triggers, errors) {
|
|
const whenIndex = expression.indexOf('when');
|
|
const whenSourceSpan = new ParseSourceSpan(sourceSpan.start.moveBy(whenIndex), sourceSpan.start.moveBy(whenIndex + 'when'.length));
|
|
const prefetchSpan = getPrefetchSpan(expression, sourceSpan);
|
|
const hydrateSpan = getHydrateSpan(expression, sourceSpan);
|
|
// This is here just to be safe, we shouldn't enter this function
|
|
// in the first place if a block doesn't have the "when" keyword.
|
|
if (whenIndex === -1) {
|
|
errors.push(new ParseError(sourceSpan, `Could not find "when" keyword in expression`));
|
|
}
|
|
else {
|
|
const start = getTriggerParametersStart(expression, whenIndex + 1);
|
|
const parsed = bindingParser.parseBinding(expression.slice(start), false, sourceSpan, sourceSpan.start.offset + start);
|
|
trackTrigger('when', triggers, errors, new BoundDeferredTrigger(parsed, sourceSpan, prefetchSpan, whenSourceSpan, hydrateSpan));
|
|
}
|
|
}
|
|
/** Parses an `on` trigger */
|
|
function parseOnTrigger({ expression, sourceSpan }, triggers, errors, placeholder) {
|
|
const onIndex = expression.indexOf('on');
|
|
const onSourceSpan = new ParseSourceSpan(sourceSpan.start.moveBy(onIndex), sourceSpan.start.moveBy(onIndex + 'on'.length));
|
|
const prefetchSpan = getPrefetchSpan(expression, sourceSpan);
|
|
const hydrateSpan = getHydrateSpan(expression, sourceSpan);
|
|
// This is here just to be safe, we shouldn't enter this function
|
|
// in the first place if a block doesn't have the "on" keyword.
|
|
if (onIndex === -1) {
|
|
errors.push(new ParseError(sourceSpan, `Could not find "on" keyword in expression`));
|
|
}
|
|
else {
|
|
const start = getTriggerParametersStart(expression, onIndex + 1);
|
|
const parser = new OnTriggerParser(expression, start, sourceSpan, triggers, errors, expression.startsWith('hydrate')
|
|
? validateHydrateReferenceBasedTrigger
|
|
: validatePlainReferenceBasedTrigger, placeholder, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
parser.parse();
|
|
}
|
|
}
|
|
function getPrefetchSpan(expression, sourceSpan) {
|
|
if (!expression.startsWith('prefetch')) {
|
|
return null;
|
|
}
|
|
return new ParseSourceSpan(sourceSpan.start, sourceSpan.start.moveBy('prefetch'.length));
|
|
}
|
|
function getHydrateSpan(expression, sourceSpan) {
|
|
if (!expression.startsWith('hydrate')) {
|
|
return null;
|
|
}
|
|
return new ParseSourceSpan(sourceSpan.start, sourceSpan.start.moveBy('hydrate'.length));
|
|
}
|
|
class OnTriggerParser {
|
|
expression;
|
|
start;
|
|
span;
|
|
triggers;
|
|
errors;
|
|
validator;
|
|
placeholder;
|
|
prefetchSpan;
|
|
onSourceSpan;
|
|
hydrateSpan;
|
|
index = 0;
|
|
tokens;
|
|
constructor(expression, start, span, triggers, errors, validator, placeholder, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
this.expression = expression;
|
|
this.start = start;
|
|
this.span = span;
|
|
this.triggers = triggers;
|
|
this.errors = errors;
|
|
this.validator = validator;
|
|
this.placeholder = placeholder;
|
|
this.prefetchSpan = prefetchSpan;
|
|
this.onSourceSpan = onSourceSpan;
|
|
this.hydrateSpan = hydrateSpan;
|
|
this.tokens = new Lexer().tokenize(expression.slice(start));
|
|
}
|
|
parse() {
|
|
while (this.tokens.length > 0 && this.index < this.tokens.length) {
|
|
const token = this.token();
|
|
if (!token.isIdentifier()) {
|
|
this.unexpectedToken(token);
|
|
break;
|
|
}
|
|
// An identifier immediately followed by a comma or the end of
|
|
// the expression cannot have parameters so we can exit early.
|
|
if (this.isFollowedByOrLast($COMMA)) {
|
|
this.consumeTrigger(token, []);
|
|
this.advance();
|
|
}
|
|
else if (this.isFollowedByOrLast($LPAREN)) {
|
|
this.advance(); // Advance to the opening paren.
|
|
const prevErrors = this.errors.length;
|
|
const parameters = this.consumeParameters();
|
|
if (this.errors.length !== prevErrors) {
|
|
break;
|
|
}
|
|
this.consumeTrigger(token, parameters);
|
|
this.advance(); // Advance past the closing paren.
|
|
}
|
|
else if (this.index < this.tokens.length - 1) {
|
|
this.unexpectedToken(this.tokens[this.index + 1]);
|
|
}
|
|
this.advance();
|
|
}
|
|
}
|
|
advance() {
|
|
this.index++;
|
|
}
|
|
isFollowedByOrLast(char) {
|
|
if (this.index === this.tokens.length - 1) {
|
|
return true;
|
|
}
|
|
return this.tokens[this.index + 1].isCharacter(char);
|
|
}
|
|
token() {
|
|
return this.tokens[Math.min(this.index, this.tokens.length - 1)];
|
|
}
|
|
consumeTrigger(identifier, parameters) {
|
|
const triggerNameStartSpan = this.span.start.moveBy(this.start + identifier.index - this.tokens[0].index);
|
|
const nameSpan = new ParseSourceSpan(triggerNameStartSpan, triggerNameStartSpan.moveBy(identifier.strValue.length));
|
|
const endSpan = triggerNameStartSpan.moveBy(this.token().end - identifier.index);
|
|
// Put the prefetch and on spans with the first trigger
|
|
// This should maybe be refactored to have something like an outer OnGroup AST
|
|
// Since triggers can be grouped with commas "on hover(x), interaction(y)"
|
|
const isFirstTrigger = identifier.index === 0;
|
|
const onSourceSpan = isFirstTrigger ? this.onSourceSpan : null;
|
|
const prefetchSourceSpan = isFirstTrigger ? this.prefetchSpan : null;
|
|
const hydrateSourceSpan = isFirstTrigger ? this.hydrateSpan : null;
|
|
const sourceSpan = new ParseSourceSpan(isFirstTrigger ? this.span.start : triggerNameStartSpan, endSpan);
|
|
try {
|
|
switch (identifier.toString()) {
|
|
case OnTriggerType.IDLE:
|
|
this.trackTrigger('idle', createIdleTrigger(parameters, nameSpan, sourceSpan, prefetchSourceSpan, onSourceSpan, hydrateSourceSpan));
|
|
break;
|
|
case OnTriggerType.TIMER:
|
|
this.trackTrigger('timer', createTimerTrigger(parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan, this.hydrateSpan));
|
|
break;
|
|
case OnTriggerType.INTERACTION:
|
|
this.trackTrigger('interaction', createInteractionTrigger(parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan, this.hydrateSpan, this.validator));
|
|
break;
|
|
case OnTriggerType.IMMEDIATE:
|
|
this.trackTrigger('immediate', createImmediateTrigger(parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan, this.hydrateSpan));
|
|
break;
|
|
case OnTriggerType.HOVER:
|
|
this.trackTrigger('hover', createHoverTrigger(parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan, this.hydrateSpan, this.placeholder, this.validator));
|
|
break;
|
|
case OnTriggerType.VIEWPORT:
|
|
this.trackTrigger('viewport', createViewportTrigger(parameters, nameSpan, sourceSpan, this.prefetchSpan, this.onSourceSpan, this.hydrateSpan, this.validator));
|
|
break;
|
|
default:
|
|
throw new Error(`Unrecognized trigger type "${identifier}"`);
|
|
}
|
|
}
|
|
catch (e) {
|
|
this.error(identifier, e.message);
|
|
}
|
|
}
|
|
consumeParameters() {
|
|
const parameters = [];
|
|
if (!this.token().isCharacter($LPAREN)) {
|
|
this.unexpectedToken(this.token());
|
|
return parameters;
|
|
}
|
|
this.advance();
|
|
const commaDelimStack = [];
|
|
let current = '';
|
|
while (this.index < this.tokens.length) {
|
|
const token = this.token();
|
|
// Stop parsing if we've hit the end character and we're outside of a comma-delimited syntax.
|
|
// Note that we don't need to account for strings here since the lexer already parsed them
|
|
// into string tokens.
|
|
if (token.isCharacter($RPAREN) && commaDelimStack.length === 0) {
|
|
if (current.length) {
|
|
parameters.push(current);
|
|
}
|
|
break;
|
|
}
|
|
// In the `on` microsyntax "top-level" commas (e.g. ones outside of an parameters) separate
|
|
// the different triggers (e.g. `on idle,timer(500)`). This is problematic, because the
|
|
// function-like syntax also implies that multiple parameters can be passed into the
|
|
// individual trigger (e.g. `on foo(a, b)`). To avoid tripping up the parser with commas that
|
|
// are part of other sorts of syntax (object literals, arrays), we treat anything inside
|
|
// a comma-delimited syntax block as plain text.
|
|
if (token.type === TokenType.Character && COMMA_DELIMITED_SYNTAX.has(token.numValue)) {
|
|
commaDelimStack.push(COMMA_DELIMITED_SYNTAX.get(token.numValue));
|
|
}
|
|
if (commaDelimStack.length > 0 &&
|
|
token.isCharacter(commaDelimStack[commaDelimStack.length - 1])) {
|
|
commaDelimStack.pop();
|
|
}
|
|
// If we hit a comma outside of a comma-delimited syntax, it means
|
|
// that we're at the top level and we're starting a new parameter.
|
|
if (commaDelimStack.length === 0 && token.isCharacter($COMMA) && current.length > 0) {
|
|
parameters.push(current);
|
|
current = '';
|
|
this.advance();
|
|
continue;
|
|
}
|
|
// Otherwise treat the token as a plain text character in the current parameter.
|
|
current += this.tokenText();
|
|
this.advance();
|
|
}
|
|
if (!this.token().isCharacter($RPAREN) || commaDelimStack.length > 0) {
|
|
this.error(this.token(), 'Unexpected end of expression');
|
|
}
|
|
if (this.index < this.tokens.length - 1 &&
|
|
!this.tokens[this.index + 1].isCharacter($COMMA)) {
|
|
this.unexpectedToken(this.tokens[this.index + 1]);
|
|
}
|
|
return parameters;
|
|
}
|
|
tokenText() {
|
|
// Tokens have a toString already which we could use, but for string tokens it omits the quotes.
|
|
// Eventually we could expose this information on the token directly.
|
|
return this.expression.slice(this.start + this.token().index, this.start + this.token().end);
|
|
}
|
|
trackTrigger(name, trigger) {
|
|
trackTrigger(name, this.triggers, this.errors, trigger);
|
|
}
|
|
error(token, message) {
|
|
const newStart = this.span.start.moveBy(this.start + token.index);
|
|
const newEnd = newStart.moveBy(token.end - token.index);
|
|
this.errors.push(new ParseError(new ParseSourceSpan(newStart, newEnd), message));
|
|
}
|
|
unexpectedToken(token) {
|
|
this.error(token, `Unexpected token "${token}"`);
|
|
}
|
|
}
|
|
/** Adds a trigger to a map of triggers. */
|
|
function trackTrigger(name, allTriggers, errors, trigger) {
|
|
if (allTriggers[name]) {
|
|
errors.push(new ParseError(trigger.sourceSpan, `Duplicate "${name}" trigger is not allowed`));
|
|
}
|
|
else {
|
|
allTriggers[name] = trigger;
|
|
}
|
|
}
|
|
function createIdleTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
if (parameters.length > 0) {
|
|
throw new Error(`"${OnTriggerType.IDLE}" trigger cannot have parameters`);
|
|
}
|
|
return new IdleDeferredTrigger(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
function createTimerTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
if (parameters.length !== 1) {
|
|
throw new Error(`"${OnTriggerType.TIMER}" trigger must have exactly one parameter`);
|
|
}
|
|
const delay = parseDeferredTime(parameters[0]);
|
|
if (delay === null) {
|
|
throw new Error(`Could not parse time value of trigger "${OnTriggerType.TIMER}"`);
|
|
}
|
|
return new TimerDeferredTrigger(delay, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
function createImmediateTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan) {
|
|
if (parameters.length > 0) {
|
|
throw new Error(`"${OnTriggerType.IMMEDIATE}" trigger cannot have parameters`);
|
|
}
|
|
return new ImmediateDeferredTrigger(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
function createHoverTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan, placeholder, validator) {
|
|
validator(OnTriggerType.HOVER, parameters);
|
|
return new HoverDeferredTrigger(parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
function createInteractionTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan, validator) {
|
|
validator(OnTriggerType.INTERACTION, parameters);
|
|
return new InteractionDeferredTrigger(parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
function createViewportTrigger(parameters, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan, validator) {
|
|
validator(OnTriggerType.VIEWPORT, parameters);
|
|
return new ViewportDeferredTrigger(parameters[0] ?? null, nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
|
|
}
|
|
/**
|
|
* Checks whether the structure of a non-hydrate reference-based trigger is valid.
|
|
* @param type Type of the trigger being validated.
|
|
* @param parameters Parameters of the trigger.
|
|
*/
|
|
function validatePlainReferenceBasedTrigger(type, parameters) {
|
|
if (parameters.length > 1) {
|
|
throw new Error(`"${type}" trigger can only have zero or one parameters`);
|
|
}
|
|
}
|
|
/**
|
|
* Checks whether the structure of a hydrate trigger is valid.
|
|
* @param type Type of the trigger being validated.
|
|
* @param parameters Parameters of the trigger.
|
|
*/
|
|
function validateHydrateReferenceBasedTrigger(type, parameters) {
|
|
if (parameters.length > 0) {
|
|
throw new Error(`Hydration trigger "${type}" cannot have parameters`);
|
|
}
|
|
}
|
|
/** Gets the index within an expression at which the trigger parameters start. */
|
|
function getTriggerParametersStart(value, startPosition = 0) {
|
|
let hasFoundSeparator = false;
|
|
for (let i = startPosition; i < value.length; i++) {
|
|
if (SEPARATOR_PATTERN.test(value[i])) {
|
|
hasFoundSeparator = true;
|
|
}
|
|
else if (hasFoundSeparator) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
/**
|
|
* Parses a time expression from a deferred trigger to
|
|
* milliseconds. Returns null if it cannot be parsed.
|
|
*/
|
|
function parseDeferredTime(value) {
|
|
const match = value.match(TIME_PATTERN);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
const [time, units] = match;
|
|
return parseFloat(time) * (units === 's' ? 1000 : 1);
|
|
}
|
|
|
|
/** Pattern to identify a `prefetch when` trigger. */
|
|
const PREFETCH_WHEN_PATTERN = /^prefetch\s+when\s/;
|
|
/** Pattern to identify a `prefetch on` trigger. */
|
|
const PREFETCH_ON_PATTERN = /^prefetch\s+on\s/;
|
|
/** Pattern to identify a `hydrate when` trigger. */
|
|
const HYDRATE_WHEN_PATTERN = /^hydrate\s+when\s/;
|
|
/** Pattern to identify a `hydrate on` trigger. */
|
|
const HYDRATE_ON_PATTERN = /^hydrate\s+on\s/;
|
|
/** Pattern to identify a `hydrate never` trigger. */
|
|
const HYDRATE_NEVER_PATTERN = /^hydrate\s+never(\s*)$/;
|
|
/** Pattern to identify a `minimum` parameter in a block. */
|
|
const MINIMUM_PARAMETER_PATTERN = /^minimum\s/;
|
|
/** Pattern to identify a `after` parameter in a block. */
|
|
const AFTER_PARAMETER_PATTERN = /^after\s/;
|
|
/** Pattern to identify a `when` parameter in a block. */
|
|
const WHEN_PARAMETER_PATTERN = /^when\s/;
|
|
/** Pattern to identify a `on` parameter in a block. */
|
|
const ON_PARAMETER_PATTERN = /^on\s/;
|
|
/**
|
|
* Predicate function that determines if a block with
|
|
* a specific name cam be connected to a `defer` block.
|
|
*/
|
|
function isConnectedDeferLoopBlock(name) {
|
|
return name === 'placeholder' || name === 'loading' || name === 'error';
|
|
}
|
|
/** Creates a deferred block from an HTML AST node. */
|
|
function createDeferredBlock(ast, connectedBlocks, visitor, bindingParser) {
|
|
const errors = [];
|
|
const { placeholder, loading, error } = parseConnectedBlocks(connectedBlocks, errors, visitor);
|
|
const { triggers, prefetchTriggers, hydrateTriggers } = parsePrimaryTriggers(ast, bindingParser, errors, placeholder);
|
|
// The `defer` block has a main span encompassing all of the connected branches as well.
|
|
let lastEndSourceSpan = ast.endSourceSpan;
|
|
let endOfLastSourceSpan = ast.sourceSpan.end;
|
|
if (connectedBlocks.length > 0) {
|
|
const lastConnectedBlock = connectedBlocks[connectedBlocks.length - 1];
|
|
lastEndSourceSpan = lastConnectedBlock.endSourceSpan;
|
|
endOfLastSourceSpan = lastConnectedBlock.sourceSpan.end;
|
|
}
|
|
const sourceSpanWithConnectedBlocks = new ParseSourceSpan(ast.sourceSpan.start, endOfLastSourceSpan);
|
|
const node = new DeferredBlock(visitAll(visitor, ast.children, ast.children), triggers, prefetchTriggers, hydrateTriggers, placeholder, loading, error, ast.nameSpan, sourceSpanWithConnectedBlocks, ast.sourceSpan, ast.startSourceSpan, lastEndSourceSpan, ast.i18n);
|
|
return { node, errors };
|
|
}
|
|
function parseConnectedBlocks(connectedBlocks, errors, visitor) {
|
|
let placeholder = null;
|
|
let loading = null;
|
|
let error = null;
|
|
for (const block of connectedBlocks) {
|
|
try {
|
|
if (!isConnectedDeferLoopBlock(block.name)) {
|
|
errors.push(new ParseError(block.startSourceSpan, `Unrecognized block "@${block.name}"`));
|
|
break;
|
|
}
|
|
switch (block.name) {
|
|
case 'placeholder':
|
|
if (placeholder !== null) {
|
|
errors.push(new ParseError(block.startSourceSpan, `@defer block can only have one @placeholder block`));
|
|
}
|
|
else {
|
|
placeholder = parsePlaceholderBlock(block, visitor);
|
|
}
|
|
break;
|
|
case 'loading':
|
|
if (loading !== null) {
|
|
errors.push(new ParseError(block.startSourceSpan, `@defer block can only have one @loading block`));
|
|
}
|
|
else {
|
|
loading = parseLoadingBlock(block, visitor);
|
|
}
|
|
break;
|
|
case 'error':
|
|
if (error !== null) {
|
|
errors.push(new ParseError(block.startSourceSpan, `@defer block can only have one @error block`));
|
|
}
|
|
else {
|
|
error = parseErrorBlock(block, visitor);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
catch (e) {
|
|
errors.push(new ParseError(block.startSourceSpan, e.message));
|
|
}
|
|
}
|
|
return { placeholder, loading, error };
|
|
}
|
|
function parsePlaceholderBlock(ast, visitor) {
|
|
let minimumTime = null;
|
|
for (const param of ast.parameters) {
|
|
if (MINIMUM_PARAMETER_PATTERN.test(param.expression)) {
|
|
if (minimumTime != null) {
|
|
throw new Error(`@placeholder block can only have one "minimum" parameter`);
|
|
}
|
|
const parsedTime = parseDeferredTime(param.expression.slice(getTriggerParametersStart(param.expression)));
|
|
if (parsedTime === null) {
|
|
throw new Error(`Could not parse time value of parameter "minimum"`);
|
|
}
|
|
minimumTime = parsedTime;
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized parameter in @placeholder block: "${param.expression}"`);
|
|
}
|
|
}
|
|
return new DeferredBlockPlaceholder(visitAll(visitor, ast.children, ast.children), minimumTime, ast.nameSpan, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.i18n);
|
|
}
|
|
function parseLoadingBlock(ast, visitor) {
|
|
let afterTime = null;
|
|
let minimumTime = null;
|
|
for (const param of ast.parameters) {
|
|
if (AFTER_PARAMETER_PATTERN.test(param.expression)) {
|
|
if (afterTime != null) {
|
|
throw new Error(`@loading block can only have one "after" parameter`);
|
|
}
|
|
const parsedTime = parseDeferredTime(param.expression.slice(getTriggerParametersStart(param.expression)));
|
|
if (parsedTime === null) {
|
|
throw new Error(`Could not parse time value of parameter "after"`);
|
|
}
|
|
afterTime = parsedTime;
|
|
}
|
|
else if (MINIMUM_PARAMETER_PATTERN.test(param.expression)) {
|
|
if (minimumTime != null) {
|
|
throw new Error(`@loading block can only have one "minimum" parameter`);
|
|
}
|
|
const parsedTime = parseDeferredTime(param.expression.slice(getTriggerParametersStart(param.expression)));
|
|
if (parsedTime === null) {
|
|
throw new Error(`Could not parse time value of parameter "minimum"`);
|
|
}
|
|
minimumTime = parsedTime;
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized parameter in @loading block: "${param.expression}"`);
|
|
}
|
|
}
|
|
return new DeferredBlockLoading(visitAll(visitor, ast.children, ast.children), afterTime, minimumTime, ast.nameSpan, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.i18n);
|
|
}
|
|
function parseErrorBlock(ast, visitor) {
|
|
if (ast.parameters.length > 0) {
|
|
throw new Error(`@error block cannot have parameters`);
|
|
}
|
|
return new DeferredBlockError(visitAll(visitor, ast.children, ast.children), ast.nameSpan, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.i18n);
|
|
}
|
|
function parsePrimaryTriggers(ast, bindingParser, errors, placeholder) {
|
|
const triggers = {};
|
|
const prefetchTriggers = {};
|
|
const hydrateTriggers = {};
|
|
for (const param of ast.parameters) {
|
|
// The lexer ignores the leading spaces so we can assume
|
|
// that the expression starts with a keyword.
|
|
if (WHEN_PARAMETER_PATTERN.test(param.expression)) {
|
|
parseWhenTrigger(param, bindingParser, triggers, errors);
|
|
}
|
|
else if (ON_PARAMETER_PATTERN.test(param.expression)) {
|
|
parseOnTrigger(param, triggers, errors, placeholder);
|
|
}
|
|
else if (PREFETCH_WHEN_PATTERN.test(param.expression)) {
|
|
parseWhenTrigger(param, bindingParser, prefetchTriggers, errors);
|
|
}
|
|
else if (PREFETCH_ON_PATTERN.test(param.expression)) {
|
|
parseOnTrigger(param, prefetchTriggers, errors, placeholder);
|
|
}
|
|
else if (HYDRATE_WHEN_PATTERN.test(param.expression)) {
|
|
parseWhenTrigger(param, bindingParser, hydrateTriggers, errors);
|
|
}
|
|
else if (HYDRATE_ON_PATTERN.test(param.expression)) {
|
|
parseOnTrigger(param, hydrateTriggers, errors, placeholder);
|
|
}
|
|
else if (HYDRATE_NEVER_PATTERN.test(param.expression)) {
|
|
parseNeverTrigger(param, hydrateTriggers, errors);
|
|
}
|
|
else {
|
|
errors.push(new ParseError(param.sourceSpan, 'Unrecognized trigger'));
|
|
}
|
|
}
|
|
if (hydrateTriggers.never && Object.keys(hydrateTriggers).length > 1) {
|
|
errors.push(new ParseError(ast.startSourceSpan, 'Cannot specify additional `hydrate` triggers if `hydrate never` is present'));
|
|
}
|
|
return { triggers, prefetchTriggers, hydrateTriggers };
|
|
}
|
|
|
|
const BIND_NAME_REGEXP = /^(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*)$/;
|
|
// Group 1 = "bind-"
|
|
const KW_BIND_IDX = 1;
|
|
// Group 2 = "let-"
|
|
const KW_LET_IDX = 2;
|
|
// Group 3 = "ref-/#"
|
|
const KW_REF_IDX = 3;
|
|
// Group 4 = "on-"
|
|
const KW_ON_IDX = 4;
|
|
// Group 5 = "bindon-"
|
|
const KW_BINDON_IDX = 5;
|
|
// Group 6 = "@"
|
|
const KW_AT_IDX = 6;
|
|
// Group 7 = the identifier after "bind-", "let-", "ref-/#", "on-", "bindon-" or "@"
|
|
const IDENT_KW_IDX = 7;
|
|
const BINDING_DELIMS = {
|
|
BANANA_BOX: { start: '[(', end: ')]' },
|
|
PROPERTY: { start: '[', end: ']' },
|
|
EVENT: { start: '(', end: ')' },
|
|
};
|
|
const TEMPLATE_ATTR_PREFIX = '*';
|
|
// TODO(crisbeto): any other tag names that shouldn't be allowed here?
|
|
const UNSUPPORTED_SELECTORLESS_TAGS = new Set([
|
|
'link',
|
|
'style',
|
|
'script',
|
|
'ng-template',
|
|
'ng-container',
|
|
'ng-content',
|
|
]);
|
|
// TODO(crisbeto): any other attributes that should not be allowed here?
|
|
const UNSUPPORTED_SELECTORLESS_DIRECTIVE_ATTRS = new Set(['ngProjectAs', 'ngNonBindable']);
|
|
function htmlAstToRender3Ast(htmlNodes, bindingParser, options) {
|
|
const transformer = new HtmlAstToIvyAst(bindingParser, options);
|
|
const ivyNodes = visitAll(transformer, htmlNodes, htmlNodes);
|
|
// Errors might originate in either the binding parser or the html to ivy transformer
|
|
const allErrors = bindingParser.errors.concat(transformer.errors);
|
|
const result = {
|
|
nodes: ivyNodes,
|
|
errors: allErrors,
|
|
styleUrls: transformer.styleUrls,
|
|
styles: transformer.styles,
|
|
ngContentSelectors: transformer.ngContentSelectors,
|
|
};
|
|
if (options.collectCommentNodes) {
|
|
result.commentNodes = transformer.commentNodes;
|
|
}
|
|
return result;
|
|
}
|
|
class HtmlAstToIvyAst {
|
|
bindingParser;
|
|
options;
|
|
errors = [];
|
|
styles = [];
|
|
styleUrls = [];
|
|
ngContentSelectors = [];
|
|
// This array will be populated if `Render3ParseOptions['collectCommentNodes']` is true
|
|
commentNodes = [];
|
|
inI18nBlock = false;
|
|
/**
|
|
* Keeps track of the nodes that have been processed already when previous nodes were visited.
|
|
* These are typically blocks connected to other blocks or text nodes between connected blocks.
|
|
*/
|
|
processedNodes = new Set();
|
|
constructor(bindingParser, options) {
|
|
this.bindingParser = bindingParser;
|
|
this.options = options;
|
|
}
|
|
// HTML visitor
|
|
visitElement(element) {
|
|
const isI18nRootElement = isI18nRootNode(element.i18n);
|
|
if (isI18nRootElement) {
|
|
if (this.inI18nBlock) {
|
|
this.reportError('Cannot mark an element as translatable inside of a translatable section. Please remove the nested i18n marker.', element.sourceSpan);
|
|
}
|
|
this.inI18nBlock = true;
|
|
}
|
|
const preparsedElement = preparseElement(element);
|
|
if (preparsedElement.type === PreparsedElementType.SCRIPT) {
|
|
return null;
|
|
}
|
|
else if (preparsedElement.type === PreparsedElementType.STYLE) {
|
|
const contents = textContents(element);
|
|
if (contents !== null) {
|
|
this.styles.push(contents);
|
|
}
|
|
return null;
|
|
}
|
|
else if (preparsedElement.type === PreparsedElementType.STYLESHEET &&
|
|
isStyleUrlResolvable(preparsedElement.hrefAttr)) {
|
|
this.styleUrls.push(preparsedElement.hrefAttr);
|
|
return null;
|
|
}
|
|
// Whether the element is a `<ng-template>`
|
|
const isTemplateElement = isNgTemplate(element.name);
|
|
const { attributes, boundEvents, references, variables, templateVariables, elementHasInlineTemplate, parsedProperties, templateParsedProperties, i18nAttrsMeta, } = this.prepareAttributes(element.attrs, isTemplateElement);
|
|
const directives = this.extractDirectives(element);
|
|
let children;
|
|
if (preparsedElement.nonBindable) {
|
|
// The `NonBindableVisitor` may need to return an array of nodes for blocks so we need
|
|
// to flatten the array here. Avoid doing this for the `HtmlAstToIvyAst` since `flat` creates
|
|
// a new array.
|
|
children = visitAll(NON_BINDABLE_VISITOR, element.children).flat(Infinity);
|
|
}
|
|
else {
|
|
children = visitAll(this, element.children, element.children);
|
|
}
|
|
let parsedElement;
|
|
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
|
|
const selector = preparsedElement.selectAttr;
|
|
const attrs = element.attrs.map((attr) => this.visitAttribute(attr));
|
|
parsedElement = new Content(selector, attrs, children, element.isSelfClosing, element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
|
|
this.ngContentSelectors.push(selector);
|
|
}
|
|
else if (isTemplateElement) {
|
|
// `<ng-template>`
|
|
const attrs = this.categorizePropertyAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
|
parsedElement = new Template(element.name, attributes, attrs.bound, boundEvents, directives, [
|
|
/* no template attributes */
|
|
], children, references, variables, element.isSelfClosing, element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
|
|
}
|
|
else {
|
|
const attrs = this.categorizePropertyAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
|
if (element.name === 'ng-container') {
|
|
for (const bound of attrs.bound) {
|
|
if (bound.type === BindingType.Attribute) {
|
|
this.reportError(`Attribute bindings are not supported on ng-container. Use property bindings instead.`, bound.sourceSpan);
|
|
}
|
|
}
|
|
}
|
|
parsedElement = new Element$1(element.name, attributes, attrs.bound, boundEvents, directives, children, references, element.isSelfClosing, element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.isVoid, element.i18n);
|
|
}
|
|
if (elementHasInlineTemplate) {
|
|
// If this node is an inline-template (e.g. has *ngFor) then we need to create a template
|
|
// node that contains this node.
|
|
parsedElement = this.wrapInTemplate(parsedElement, templateParsedProperties, templateVariables, i18nAttrsMeta, isTemplateElement, isI18nRootElement);
|
|
}
|
|
if (isI18nRootElement) {
|
|
this.inI18nBlock = false;
|
|
}
|
|
return parsedElement;
|
|
}
|
|
visitAttribute(attribute) {
|
|
return new TextAttribute(attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan, attribute.valueSpan, attribute.i18n);
|
|
}
|
|
visitText(text) {
|
|
return this.processedNodes.has(text)
|
|
? null
|
|
: this._visitTextWithInterpolation(text.value, text.sourceSpan, text.tokens, text.i18n);
|
|
}
|
|
visitExpansion(expansion) {
|
|
if (!expansion.i18n) {
|
|
// do not generate Icu in case it was created
|
|
// outside of i18n block in a template
|
|
return null;
|
|
}
|
|
if (!isI18nRootNode(expansion.i18n)) {
|
|
throw new Error(`Invalid type "${expansion.i18n.constructor}" for "i18n" property of ${expansion.sourceSpan.toString()}. Expected a "Message"`);
|
|
}
|
|
const message = expansion.i18n;
|
|
const vars = {};
|
|
const placeholders = {};
|
|
// extract VARs from ICUs - we process them separately while
|
|
// assembling resulting message via goog.getMsg function, since
|
|
// we need to pass them to top-level goog.getMsg call
|
|
Object.keys(message.placeholders).forEach((key) => {
|
|
const value = message.placeholders[key];
|
|
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
|
// Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g.
|
|
// `{count, select , ...}`), these spaces are also included into the key names in ICU vars
|
|
// (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be
|
|
// converted into `_` symbols while normalizing placeholder names, which might lead to
|
|
// mismatches at runtime (i.e. placeholder will not be replaced with the correct value).
|
|
const formattedKey = key.trim();
|
|
const ast = this.bindingParser.parseInterpolationExpression(value.text, value.sourceSpan);
|
|
vars[formattedKey] = new BoundText(ast, value.sourceSpan);
|
|
}
|
|
else {
|
|
placeholders[key] = this._visitTextWithInterpolation(value.text, value.sourceSpan, null);
|
|
}
|
|
});
|
|
return new Icu$1(vars, placeholders, expansion.sourceSpan, message);
|
|
}
|
|
visitExpansionCase(expansionCase) {
|
|
return null;
|
|
}
|
|
visitComment(comment) {
|
|
if (this.options.collectCommentNodes) {
|
|
this.commentNodes.push(new Comment$1(comment.value || '', comment.sourceSpan));
|
|
}
|
|
return null;
|
|
}
|
|
visitLetDeclaration(decl, context) {
|
|
const value = this.bindingParser.parseBinding(decl.value, false, decl.valueSpan, decl.valueSpan.start.offset);
|
|
if (value.errors.length === 0 && value.ast instanceof EmptyExpr$1) {
|
|
this.reportError('@let declaration value cannot be empty', decl.valueSpan);
|
|
}
|
|
return new LetDeclaration$1(decl.name, value, decl.sourceSpan, decl.nameSpan, decl.valueSpan);
|
|
}
|
|
visitComponent(component) {
|
|
const isI18nRootElement = isI18nRootNode(component.i18n);
|
|
if (isI18nRootElement) {
|
|
if (this.inI18nBlock) {
|
|
this.reportError('Cannot mark a component as translatable inside of a translatable section. Please remove the nested i18n marker.', component.sourceSpan);
|
|
}
|
|
this.inI18nBlock = true;
|
|
}
|
|
if (component.tagName !== null && UNSUPPORTED_SELECTORLESS_TAGS.has(component.tagName)) {
|
|
this.reportError(`Tag name "${component.tagName}" cannot be used as a component tag`, component.startSourceSpan);
|
|
return null;
|
|
}
|
|
const { attributes, boundEvents, references, templateVariables, elementHasInlineTemplate, parsedProperties, templateParsedProperties, i18nAttrsMeta, } = this.prepareAttributes(component.attrs, false);
|
|
this.validateSelectorlessReferences(references);
|
|
const directives = this.extractDirectives(component);
|
|
let children;
|
|
if (component.attrs.find((attr) => attr.name === 'ngNonBindable')) {
|
|
// The `NonBindableVisitor` may need to return an array of nodes for blocks so we need
|
|
// to flatten the array here. Avoid doing this for the `HtmlAstToIvyAst` since `flat` creates
|
|
// a new array.
|
|
children = visitAll(NON_BINDABLE_VISITOR, component.children).flat(Infinity);
|
|
}
|
|
else {
|
|
children = visitAll(this, component.children, component.children);
|
|
}
|
|
const attrs = this.categorizePropertyAttributes(component.tagName, parsedProperties, i18nAttrsMeta);
|
|
let node = new Component$1(component.componentName, component.tagName, component.fullName, attributes, attrs.bound, boundEvents, directives, children, references, component.isSelfClosing, component.sourceSpan, component.startSourceSpan, component.endSourceSpan, component.i18n);
|
|
if (elementHasInlineTemplate) {
|
|
node = this.wrapInTemplate(node, templateParsedProperties, templateVariables, i18nAttrsMeta, false, isI18nRootElement);
|
|
}
|
|
if (isI18nRootElement) {
|
|
this.inI18nBlock = false;
|
|
}
|
|
return node;
|
|
}
|
|
visitDirective() {
|
|
return null;
|
|
}
|
|
visitBlockParameter() {
|
|
return null;
|
|
}
|
|
visitBlock(block, context) {
|
|
const index = Array.isArray(context) ? context.indexOf(block) : -1;
|
|
if (index === -1) {
|
|
throw new Error('Visitor invoked incorrectly. Expecting visitBlock to be invoked siblings array as its context');
|
|
}
|
|
// Connected blocks may have been processed as a part of the previous block.
|
|
if (this.processedNodes.has(block)) {
|
|
return null;
|
|
}
|
|
let result = null;
|
|
switch (block.name) {
|
|
case 'defer':
|
|
result = createDeferredBlock(block, this.findConnectedBlocks(index, context, isConnectedDeferLoopBlock), this, this.bindingParser);
|
|
break;
|
|
case 'switch':
|
|
result = createSwitchBlock(block, this, this.bindingParser);
|
|
break;
|
|
case 'for':
|
|
result = createForLoop(block, this.findConnectedBlocks(index, context, isConnectedForLoopBlock), this, this.bindingParser);
|
|
break;
|
|
case 'if':
|
|
result = createIfBlock(block, this.findConnectedBlocks(index, context, isConnectedIfLoopBlock), this, this.bindingParser);
|
|
break;
|
|
default:
|
|
let errorMessage;
|
|
if (isConnectedDeferLoopBlock(block.name)) {
|
|
errorMessage = `@${block.name} block can only be used after an @defer block.`;
|
|
this.processedNodes.add(block);
|
|
}
|
|
else if (isConnectedForLoopBlock(block.name)) {
|
|
errorMessage = `@${block.name} block can only be used after an @for block.`;
|
|
this.processedNodes.add(block);
|
|
}
|
|
else if (isConnectedIfLoopBlock(block.name)) {
|
|
errorMessage = `@${block.name} block can only be used after an @if or @else if block.`;
|
|
this.processedNodes.add(block);
|
|
}
|
|
else {
|
|
errorMessage = `Unrecognized block @${block.name}.`;
|
|
}
|
|
result = {
|
|
node: new UnknownBlock(block.name, block.sourceSpan, block.nameSpan),
|
|
errors: [new ParseError(block.sourceSpan, errorMessage)],
|
|
};
|
|
break;
|
|
}
|
|
this.errors.push(...result.errors);
|
|
return result.node;
|
|
}
|
|
findConnectedBlocks(primaryBlockIndex, siblings, predicate) {
|
|
const relatedBlocks = [];
|
|
for (let i = primaryBlockIndex + 1; i < siblings.length; i++) {
|
|
const node = siblings[i];
|
|
// Skip over comments.
|
|
if (node instanceof Comment) {
|
|
continue;
|
|
}
|
|
// Ignore empty text nodes between blocks.
|
|
if (node instanceof Text && node.value.trim().length === 0) {
|
|
// Add the text node to the processed nodes since we don't want
|
|
// it to be generated between the connected nodes.
|
|
this.processedNodes.add(node);
|
|
continue;
|
|
}
|
|
// Stop searching as soon as we hit a non-block node or a block that is unrelated.
|
|
if (!(node instanceof Block) || !predicate(node.name)) {
|
|
break;
|
|
}
|
|
relatedBlocks.push(node);
|
|
this.processedNodes.add(node);
|
|
}
|
|
return relatedBlocks;
|
|
}
|
|
/** Splits up the property attributes depending on whether they're static or bound. */
|
|
categorizePropertyAttributes(elementName, properties, i18nPropsMeta) {
|
|
const bound = [];
|
|
const literal = [];
|
|
properties.forEach((prop) => {
|
|
const i18n = i18nPropsMeta[prop.name];
|
|
if (prop.isLiteral) {
|
|
literal.push(new TextAttribute(prop.name, prop.expression.source || '', prop.sourceSpan, prop.keySpan, prop.valueSpan, i18n));
|
|
}
|
|
else {
|
|
// Note that validation is skipped and property mapping is disabled
|
|
// due to the fact that we need to make sure a given prop is not an
|
|
// input of a directive and directive matching happens at runtime.
|
|
const bep = this.bindingParser.createBoundElementProperty(elementName, prop,
|
|
/* skipValidation */ true,
|
|
/* mapPropertyName */ false);
|
|
bound.push(BoundAttribute.fromBoundElementProperty(bep, i18n));
|
|
}
|
|
});
|
|
return { bound, literal };
|
|
}
|
|
prepareAttributes(attrs, isTemplateElement) {
|
|
const parsedProperties = [];
|
|
const boundEvents = [];
|
|
const variables = [];
|
|
const references = [];
|
|
const attributes = [];
|
|
const i18nAttrsMeta = {};
|
|
const templateParsedProperties = [];
|
|
const templateVariables = [];
|
|
// Whether the element has any *-attribute
|
|
let elementHasInlineTemplate = false;
|
|
for (const attribute of attrs) {
|
|
let hasBinding = false;
|
|
const normalizedName = normalizeAttributeName(attribute.name);
|
|
// `*attr` defines template bindings
|
|
let isTemplateBinding = false;
|
|
if (attribute.i18n) {
|
|
i18nAttrsMeta[attribute.name] = attribute.i18n;
|
|
}
|
|
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
|
// *-attributes
|
|
if (elementHasInlineTemplate) {
|
|
this.reportError(`Can't have multiple template bindings on one element. Use only one attribute prefixed with *`, attribute.sourceSpan);
|
|
}
|
|
isTemplateBinding = true;
|
|
elementHasInlineTemplate = true;
|
|
const templateValue = attribute.value;
|
|
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
|
|
const parsedVariables = [];
|
|
const absoluteValueOffset = attribute.valueSpan
|
|
? attribute.valueSpan.fullStart.offset
|
|
: // If there is no value span the attribute does not have a value, like `attr` in
|
|
//`<div attr></div>`. In this case, point to one character beyond the last character of
|
|
// the attribute name.
|
|
attribute.sourceSpan.fullStart.offset + attribute.name.length;
|
|
this.bindingParser.parseInlineTemplateBinding(templateKey, templateValue, attribute.sourceSpan, absoluteValueOffset, [], templateParsedProperties, parsedVariables, true /* isIvyAst */);
|
|
templateVariables.push(...parsedVariables.map((v) => new Variable(v.name, v.value, v.sourceSpan, v.keySpan, v.valueSpan)));
|
|
}
|
|
else {
|
|
// Check for variables, events, property bindings, interpolation
|
|
hasBinding = this.parseAttribute(isTemplateElement, attribute, [], parsedProperties, boundEvents, variables, references);
|
|
}
|
|
if (!hasBinding && !isTemplateBinding) {
|
|
// don't include the bindings as attributes as well in the AST
|
|
attributes.push(this.visitAttribute(attribute));
|
|
}
|
|
}
|
|
return {
|
|
attributes,
|
|
boundEvents,
|
|
references,
|
|
variables,
|
|
templateVariables,
|
|
elementHasInlineTemplate,
|
|
parsedProperties,
|
|
templateParsedProperties,
|
|
i18nAttrsMeta,
|
|
};
|
|
}
|
|
parseAttribute(isTemplateElement, attribute, matchableAttributes, parsedProperties, boundEvents, variables, references) {
|
|
const name = normalizeAttributeName(attribute.name);
|
|
const value = attribute.value;
|
|
const srcSpan = attribute.sourceSpan;
|
|
const absoluteOffset = attribute.valueSpan
|
|
? attribute.valueSpan.fullStart.offset
|
|
: srcSpan.fullStart.offset;
|
|
function createKeySpan(srcSpan, prefix, identifier) {
|
|
// We need to adjust the start location for the keySpan to account for the removed 'data-'
|
|
// prefix from `normalizeAttributeName`.
|
|
const normalizationAdjustment = attribute.name.length - name.length;
|
|
const keySpanStart = srcSpan.start.moveBy(prefix.length + normalizationAdjustment);
|
|
const keySpanEnd = keySpanStart.moveBy(identifier.length);
|
|
return new ParseSourceSpan(keySpanStart, keySpanEnd, keySpanStart, identifier);
|
|
}
|
|
const bindParts = name.match(BIND_NAME_REGEXP);
|
|
if (bindParts) {
|
|
if (bindParts[KW_BIND_IDX] != null) {
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_BIND_IDX], identifier);
|
|
this.bindingParser.parsePropertyBinding(identifier, value, false, false, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
|
}
|
|
else if (bindParts[KW_LET_IDX]) {
|
|
if (isTemplateElement) {
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_LET_IDX], identifier);
|
|
this.parseVariable(identifier, value, srcSpan, keySpan, attribute.valueSpan, variables);
|
|
}
|
|
else {
|
|
this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan);
|
|
}
|
|
}
|
|
else if (bindParts[KW_REF_IDX]) {
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_REF_IDX], identifier);
|
|
this.parseReference(identifier, value, srcSpan, keySpan, attribute.valueSpan, references);
|
|
}
|
|
else if (bindParts[KW_ON_IDX]) {
|
|
const events = [];
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_ON_IDX], identifier);
|
|
this.bindingParser.parseEvent(identifier, value,
|
|
/* isAssignmentEvent */ false, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events, keySpan);
|
|
addEvents(events, boundEvents);
|
|
}
|
|
else if (bindParts[KW_BINDON_IDX]) {
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_BINDON_IDX], identifier);
|
|
this.bindingParser.parsePropertyBinding(identifier, value, false, true, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
|
this.parseAssignmentEvent(identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents, keySpan, absoluteOffset);
|
|
}
|
|
else if (bindParts[KW_AT_IDX]) {
|
|
const keySpan = createKeySpan(srcSpan, '', name);
|
|
this.bindingParser.parseLiteralAttr(name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
|
}
|
|
return true;
|
|
}
|
|
// We didn't see a kw-prefixed property binding, but we have not yet checked
|
|
// for the []/()/[()] syntax.
|
|
let delims = null;
|
|
if (name.startsWith(BINDING_DELIMS.BANANA_BOX.start)) {
|
|
delims = BINDING_DELIMS.BANANA_BOX;
|
|
}
|
|
else if (name.startsWith(BINDING_DELIMS.PROPERTY.start)) {
|
|
delims = BINDING_DELIMS.PROPERTY;
|
|
}
|
|
else if (name.startsWith(BINDING_DELIMS.EVENT.start)) {
|
|
delims = BINDING_DELIMS.EVENT;
|
|
}
|
|
if (delims !== null &&
|
|
// NOTE: older versions of the parser would match a start/end delimited
|
|
// binding iff the property name was terminated by the ending delimiter
|
|
// and the identifier in the binding was non-empty.
|
|
// TODO(ayazhafiz): update this to handle malformed bindings.
|
|
name.endsWith(delims.end) &&
|
|
name.length > delims.start.length + delims.end.length) {
|
|
const identifier = name.substring(delims.start.length, name.length - delims.end.length);
|
|
const keySpan = createKeySpan(srcSpan, delims.start, identifier);
|
|
if (delims.start === BINDING_DELIMS.BANANA_BOX.start) {
|
|
this.bindingParser.parsePropertyBinding(identifier, value, false, true, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
|
this.parseAssignmentEvent(identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents, keySpan, absoluteOffset);
|
|
}
|
|
else if (delims.start === BINDING_DELIMS.PROPERTY.start) {
|
|
this.bindingParser.parsePropertyBinding(identifier, value, false, false, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
|
}
|
|
else {
|
|
const events = [];
|
|
this.bindingParser.parseEvent(identifier, value,
|
|
/* isAssignmentEvent */ false, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events, keySpan);
|
|
addEvents(events, boundEvents);
|
|
}
|
|
return true;
|
|
}
|
|
// No explicit binding found.
|
|
const keySpan = createKeySpan(srcSpan, '' /* prefix */, name);
|
|
const hasBinding = this.bindingParser.parsePropertyInterpolation(name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan, attribute.valueTokens ?? null);
|
|
return hasBinding;
|
|
}
|
|
extractDirectives(node) {
|
|
const elementName = node instanceof Component ? node.tagName : node.name;
|
|
const directives = [];
|
|
const seenDirectives = new Set();
|
|
for (const directive of node.directives) {
|
|
let invalid = false;
|
|
for (const attr of directive.attrs) {
|
|
if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
|
invalid = true;
|
|
this.reportError(`Shorthand template syntax "${attr.name}" is not supported inside a directive context`, attr.sourceSpan);
|
|
}
|
|
else if (UNSUPPORTED_SELECTORLESS_DIRECTIVE_ATTRS.has(attr.name)) {
|
|
invalid = true;
|
|
this.reportError(`Attribute "${attr.name}" is not supported in a directive context`, attr.sourceSpan);
|
|
}
|
|
}
|
|
if (!invalid && seenDirectives.has(directive.name)) {
|
|
invalid = true;
|
|
this.reportError(`Cannot apply directive "${directive.name}" multiple times on the same element`, directive.sourceSpan);
|
|
}
|
|
if (invalid) {
|
|
continue;
|
|
}
|
|
const { attributes, parsedProperties, boundEvents, references, i18nAttrsMeta } = this.prepareAttributes(directive.attrs, false);
|
|
this.validateSelectorlessReferences(references);
|
|
const { bound: inputs } = this.categorizePropertyAttributes(elementName, parsedProperties, i18nAttrsMeta);
|
|
for (const input of inputs) {
|
|
if (input.type !== BindingType.Property && input.type !== BindingType.TwoWay) {
|
|
invalid = true;
|
|
this.reportError('Binding is not supported in a directive context', input.sourceSpan);
|
|
}
|
|
}
|
|
if (invalid) {
|
|
continue;
|
|
}
|
|
seenDirectives.add(directive.name);
|
|
directives.push(new Directive$1(directive.name, attributes, inputs, boundEvents, references, directive.sourceSpan, directive.startSourceSpan, directive.endSourceSpan, undefined));
|
|
}
|
|
return directives;
|
|
}
|
|
filterAnimationAttributes(attributes) {
|
|
return attributes.filter((a) => !a.name.startsWith('animate.'));
|
|
}
|
|
filterAnimationInputs(attributes) {
|
|
return attributes.filter((a) => a.type !== BindingType.Animation);
|
|
}
|
|
wrapInTemplate(node, templateProperties, templateVariables, i18nAttrsMeta, isTemplateElement, isI18nRootElement) {
|
|
// We need to hoist the attributes of the node to the template for content projection purposes.
|
|
const attrs = this.categorizePropertyAttributes('ng-template', templateProperties, i18nAttrsMeta);
|
|
const templateAttrs = [];
|
|
attrs.literal.forEach((attr) => templateAttrs.push(attr));
|
|
attrs.bound.forEach((attr) => templateAttrs.push(attr));
|
|
const hoistedAttrs = {
|
|
attributes: [],
|
|
inputs: [],
|
|
outputs: [],
|
|
};
|
|
if (node instanceof Element$1 || node instanceof Component$1) {
|
|
hoistedAttrs.attributes.push(...this.filterAnimationAttributes(node.attributes));
|
|
hoistedAttrs.inputs.push(...this.filterAnimationInputs(node.inputs));
|
|
hoistedAttrs.outputs.push(...node.outputs);
|
|
}
|
|
// For <ng-template>s with structural directives on them, avoid passing i18n information to
|
|
// the wrapping template to prevent unnecessary i18n instructions from being generated. The
|
|
// necessary i18n meta information will be extracted from child elements.
|
|
const i18n = isTemplateElement && isI18nRootElement ? undefined : node.i18n;
|
|
let name;
|
|
if (node instanceof Component$1) {
|
|
name = node.tagName;
|
|
}
|
|
else if (node instanceof Template) {
|
|
name = null;
|
|
}
|
|
else {
|
|
name = node.name;
|
|
}
|
|
return new Template(name, hoistedAttrs.attributes, hoistedAttrs.inputs, hoistedAttrs.outputs, [
|
|
// Do not copy over the directives.
|
|
], templateAttrs, [node], [
|
|
// Do not copy over the references.
|
|
], templateVariables, false, node.sourceSpan, node.startSourceSpan, node.endSourceSpan, i18n);
|
|
}
|
|
_visitTextWithInterpolation(value, sourceSpan, interpolatedTokens, i18n) {
|
|
const valueNoNgsp = replaceNgsp(value);
|
|
const expr = this.bindingParser.parseInterpolation(valueNoNgsp, sourceSpan, interpolatedTokens);
|
|
return expr ? new BoundText(expr, sourceSpan, i18n) : new Text$3(valueNoNgsp, sourceSpan);
|
|
}
|
|
parseVariable(identifier, value, sourceSpan, keySpan, valueSpan, variables) {
|
|
if (identifier.indexOf('-') > -1) {
|
|
this.reportError(`"-" is not allowed in variable names`, sourceSpan);
|
|
}
|
|
else if (identifier.length === 0) {
|
|
this.reportError(`Variable does not have a name`, sourceSpan);
|
|
}
|
|
variables.push(new Variable(identifier, value, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
parseReference(identifier, value, sourceSpan, keySpan, valueSpan, references) {
|
|
if (identifier.indexOf('-') > -1) {
|
|
this.reportError(`"-" is not allowed in reference names`, sourceSpan);
|
|
}
|
|
else if (identifier.length === 0) {
|
|
this.reportError(`Reference does not have a name`, sourceSpan);
|
|
}
|
|
else if (references.some((reference) => reference.name === identifier)) {
|
|
this.reportError(`Reference "#${identifier}" is defined more than once`, sourceSpan);
|
|
}
|
|
references.push(new Reference(identifier, value, sourceSpan, keySpan, valueSpan));
|
|
}
|
|
parseAssignmentEvent(name, expression, sourceSpan, valueSpan, targetMatchableAttrs, boundEvents, keySpan, absoluteOffset) {
|
|
const events = [];
|
|
this.bindingParser.parseEvent(`${name}Change`, expression,
|
|
/* isAssignmentEvent */ true, sourceSpan, valueSpan || sourceSpan, targetMatchableAttrs, events, keySpan);
|
|
addEvents(events, boundEvents);
|
|
}
|
|
validateSelectorlessReferences(references) {
|
|
if (references.length === 0) {
|
|
return;
|
|
}
|
|
const seenNames = new Set();
|
|
for (const ref of references) {
|
|
if (ref.value.length > 0) {
|
|
this.reportError('Cannot specify a value for a local reference in this context', ref.valueSpan || ref.sourceSpan);
|
|
}
|
|
else if (seenNames.has(ref.name)) {
|
|
this.reportError('Duplicate reference names are not allowed', ref.sourceSpan);
|
|
}
|
|
else {
|
|
seenNames.add(ref.name);
|
|
}
|
|
}
|
|
}
|
|
reportError(message, sourceSpan, level = ParseErrorLevel.ERROR) {
|
|
this.errors.push(new ParseError(sourceSpan, message, level));
|
|
}
|
|
}
|
|
class NonBindableVisitor {
|
|
visitElement(ast) {
|
|
const preparsedElement = preparseElement(ast);
|
|
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
|
|
preparsedElement.type === PreparsedElementType.STYLE ||
|
|
preparsedElement.type === PreparsedElementType.STYLESHEET) {
|
|
// Skipping <script> for security reasons
|
|
// Skipping <style> and stylesheets as we already processed them
|
|
// in the StyleCompiler
|
|
return null;
|
|
}
|
|
const children = visitAll(this, ast.children, null);
|
|
return new Element$1(ast.name, visitAll(this, ast.attrs),
|
|
/* inputs */ [],
|
|
/* outputs */ [],
|
|
/* directives */ [], children,
|
|
/* references */ [], ast.isSelfClosing, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, ast.isVoid);
|
|
}
|
|
visitComment(comment) {
|
|
return null;
|
|
}
|
|
visitAttribute(attribute) {
|
|
return new TextAttribute(attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan, attribute.valueSpan, attribute.i18n);
|
|
}
|
|
visitText(text) {
|
|
return new Text$3(text.value, text.sourceSpan);
|
|
}
|
|
visitExpansion(expansion) {
|
|
return null;
|
|
}
|
|
visitExpansionCase(expansionCase) {
|
|
return null;
|
|
}
|
|
visitBlock(block, context) {
|
|
const nodes = [
|
|
// In an ngNonBindable context we treat the opening/closing tags of block as plain text.
|
|
// This is the as if the `tokenizeBlocks` option was disabled.
|
|
new Text$3(block.startSourceSpan.toString(), block.startSourceSpan),
|
|
...visitAll(this, block.children),
|
|
];
|
|
if (block.endSourceSpan !== null) {
|
|
nodes.push(new Text$3(block.endSourceSpan.toString(), block.endSourceSpan));
|
|
}
|
|
return nodes;
|
|
}
|
|
visitBlockParameter(parameter, context) {
|
|
return null;
|
|
}
|
|
visitLetDeclaration(decl, context) {
|
|
return new Text$3(`@let ${decl.name} = ${decl.value};`, decl.sourceSpan);
|
|
}
|
|
visitComponent(ast, context) {
|
|
const children = visitAll(this, ast.children, null);
|
|
return new Element$1(ast.fullName, visitAll(this, ast.attrs),
|
|
/* inputs */ [],
|
|
/* outputs */ [],
|
|
/* directives */ [], children,
|
|
/* references */ [], ast.isSelfClosing, ast.sourceSpan, ast.startSourceSpan, ast.endSourceSpan, false);
|
|
}
|
|
visitDirective(directive, context) {
|
|
return null;
|
|
}
|
|
}
|
|
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
|
|
function normalizeAttributeName(attrName) {
|
|
return /^data-/i.test(attrName) ? attrName.substring(5) : attrName;
|
|
}
|
|
function addEvents(events, boundEvents) {
|
|
boundEvents.push(...events.map((e) => BoundEvent.fromParsedEvent(e)));
|
|
}
|
|
function textContents(node) {
|
|
if (node.children.length !== 1 || !(node.children[0] instanceof Text)) {
|
|
return null;
|
|
}
|
|
else {
|
|
return node.children[0].value;
|
|
}
|
|
}
|
|
|
|
const LEADING_TRIVIA_CHARS = [' ', '\n', '\r', '\t'];
|
|
/**
|
|
* Parse a template into render3 `Node`s and additional metadata, with no other dependencies.
|
|
*
|
|
* @param template text of the template to parse
|
|
* @param templateUrl URL to use for source mapping of the parsed template
|
|
* @param options options to modify how the template is parsed
|
|
*/
|
|
function parseTemplate(template, templateUrl, options = {}) {
|
|
const { interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat } = options;
|
|
const selectorlessEnabled = options.enableSelectorless ?? false;
|
|
const bindingParser = makeBindingParser(interpolationConfig, selectorlessEnabled);
|
|
const htmlParser = new HtmlParser();
|
|
const parseResult = htmlParser.parse(template, templateUrl, {
|
|
leadingTriviaChars: LEADING_TRIVIA_CHARS,
|
|
...options,
|
|
tokenizeExpansionForms: true,
|
|
tokenizeBlocks: options.enableBlockSyntax ?? true,
|
|
tokenizeLet: options.enableLetSyntax ?? true,
|
|
selectorlessEnabled,
|
|
});
|
|
if (!options.alwaysAttemptHtmlToR3AstConversion &&
|
|
parseResult.errors &&
|
|
parseResult.errors.length > 0) {
|
|
const parsedTemplate = {
|
|
interpolationConfig,
|
|
preserveWhitespaces,
|
|
errors: parseResult.errors,
|
|
nodes: [],
|
|
styleUrls: [],
|
|
styles: [],
|
|
ngContentSelectors: [],
|
|
};
|
|
if (options.collectCommentNodes) {
|
|
parsedTemplate.commentNodes = [];
|
|
}
|
|
return parsedTemplate;
|
|
}
|
|
let rootNodes = parseResult.rootNodes;
|
|
// We need to use the same `retainEmptyTokens` value for both parses to avoid
|
|
// causing a mismatch when reusing source spans, even if the
|
|
// `preserveSignificantWhitespace` behavior is different between the two
|
|
// parses.
|
|
const retainEmptyTokens = !(options.preserveSignificantWhitespace ?? true);
|
|
// process i18n meta information (scan attributes, generate ids)
|
|
// before we run whitespace removal process, because existing i18n
|
|
// extraction process (ng extract-i18n) relies on a raw content to generate
|
|
// message ids
|
|
const i18nMetaVisitor = new I18nMetaVisitor(interpolationConfig,
|
|
/* keepI18nAttrs */ !preserveWhitespaces, enableI18nLegacyMessageIdFormat,
|
|
/* containerBlocks */ undefined, options.preserveSignificantWhitespace, retainEmptyTokens);
|
|
const i18nMetaResult = i18nMetaVisitor.visitAllWithErrors(rootNodes);
|
|
if (!options.alwaysAttemptHtmlToR3AstConversion &&
|
|
i18nMetaResult.errors &&
|
|
i18nMetaResult.errors.length > 0) {
|
|
const parsedTemplate = {
|
|
interpolationConfig,
|
|
preserveWhitespaces,
|
|
errors: i18nMetaResult.errors,
|
|
nodes: [],
|
|
styleUrls: [],
|
|
styles: [],
|
|
ngContentSelectors: [],
|
|
};
|
|
if (options.collectCommentNodes) {
|
|
parsedTemplate.commentNodes = [];
|
|
}
|
|
return parsedTemplate;
|
|
}
|
|
rootNodes = i18nMetaResult.rootNodes;
|
|
if (!preserveWhitespaces) {
|
|
// Always preserve significant whitespace here because this is used to generate the `goog.getMsg`
|
|
// and `$localize` calls which should retain significant whitespace in order to render the
|
|
// correct output. We let this diverge from the message IDs generated earlier which might not
|
|
// have preserved significant whitespace.
|
|
//
|
|
// This should use `visitAllWithSiblings` to set `WhitespaceVisitor` context correctly, however
|
|
// there is an existing bug where significant whitespace is not properly retained in the JS
|
|
// output of leading/trailing whitespace for ICU messages due to the existing lack of context\
|
|
// in `WhitespaceVisitor`. Using `visitAllWithSiblings` here would fix that bug and retain the
|
|
// whitespace, however it would also change the runtime representation which we don't want to do
|
|
// right now.
|
|
rootNodes = visitAll(new WhitespaceVisitor(
|
|
/* preserveSignificantWhitespace */ true,
|
|
/* originalNodeMap */ undefined,
|
|
/* requireContext */ false), rootNodes);
|
|
// run i18n meta visitor again in case whitespaces are removed (because that might affect
|
|
// generated i18n message content) and first pass indicated that i18n content is present in a
|
|
// template. During this pass i18n IDs generated at the first pass will be preserved, so we can
|
|
// mimic existing extraction process (ng extract-i18n)
|
|
if (i18nMetaVisitor.hasI18nMeta) {
|
|
rootNodes = visitAll(new I18nMetaVisitor(interpolationConfig,
|
|
/* keepI18nAttrs */ false,
|
|
/* enableI18nLegacyMessageIdFormat */ undefined,
|
|
/* containerBlocks */ undefined,
|
|
/* preserveSignificantWhitespace */ true, retainEmptyTokens), rootNodes);
|
|
}
|
|
}
|
|
const { nodes, errors, styleUrls, styles, ngContentSelectors, commentNodes } = htmlAstToRender3Ast(rootNodes, bindingParser, { collectCommentNodes: !!options.collectCommentNodes });
|
|
errors.push(...parseResult.errors, ...i18nMetaResult.errors);
|
|
const parsedTemplate = {
|
|
interpolationConfig,
|
|
preserveWhitespaces,
|
|
errors: errors.length > 0 ? errors : null,
|
|
nodes,
|
|
styleUrls,
|
|
styles,
|
|
ngContentSelectors,
|
|
};
|
|
if (options.collectCommentNodes) {
|
|
parsedTemplate.commentNodes = commentNodes;
|
|
}
|
|
return parsedTemplate;
|
|
}
|
|
const elementRegistry = new DomElementSchemaRegistry();
|
|
/**
|
|
* Construct a `BindingParser` with a default configuration.
|
|
*/
|
|
function makeBindingParser(interpolationConfig = DEFAULT_INTERPOLATION_CONFIG, selectorlessEnabled = false) {
|
|
return new BindingParser(new Parser(new Lexer(), selectorlessEnabled), interpolationConfig, elementRegistry, []);
|
|
}
|
|
|
|
const COMPONENT_VARIABLE = '%COMP%';
|
|
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
|
|
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
|
|
function baseDirectiveFields(meta, constantPool, bindingParser) {
|
|
const definitionMap = new DefinitionMap();
|
|
const selectors = parseSelectorToR3Selector(meta.selector);
|
|
// e.g. `type: MyDirective`
|
|
definitionMap.set('type', meta.type.value);
|
|
// e.g. `selectors: [['', 'someDir', '']]`
|
|
if (selectors.length > 0) {
|
|
definitionMap.set('selectors', asLiteral(selectors));
|
|
}
|
|
if (meta.queries.length > 0) {
|
|
// e.g. `contentQueries: (rf, ctx, dirIndex) => { ... }
|
|
definitionMap.set('contentQueries', createContentQueriesFunction(meta.queries, constantPool, meta.name));
|
|
}
|
|
if (meta.viewQueries.length) {
|
|
definitionMap.set('viewQuery', createViewQueriesFunction(meta.viewQueries, constantPool, meta.name));
|
|
}
|
|
// e.g. `hostBindings: (rf, ctx) => { ... }
|
|
definitionMap.set('hostBindings', createHostBindingsFunction(meta.host, meta.typeSourceSpan, bindingParser, constantPool, meta.selector || '', meta.name, definitionMap));
|
|
// e.g 'inputs: {a: 'a'}`
|
|
definitionMap.set('inputs', conditionallyCreateDirectiveBindingLiteral(meta.inputs, true));
|
|
// e.g 'outputs: {a: 'a'}`
|
|
definitionMap.set('outputs', conditionallyCreateDirectiveBindingLiteral(meta.outputs));
|
|
if (meta.exportAs !== null) {
|
|
definitionMap.set('exportAs', literalArr(meta.exportAs.map((e) => literal(e))));
|
|
}
|
|
if (meta.isStandalone === false) {
|
|
definitionMap.set('standalone', literal(false));
|
|
}
|
|
if (meta.isSignal) {
|
|
definitionMap.set('signals', literal(true));
|
|
}
|
|
return definitionMap;
|
|
}
|
|
/**
|
|
* Add features to the definition map.
|
|
*/
|
|
function addFeatures(definitionMap, meta) {
|
|
// e.g. `features: [NgOnChangesFeature]`
|
|
const features = [];
|
|
const providers = meta.providers;
|
|
const viewProviders = meta.viewProviders;
|
|
if (providers || viewProviders) {
|
|
const args = [providers || new LiteralArrayExpr([])];
|
|
if (viewProviders) {
|
|
args.push(viewProviders);
|
|
}
|
|
features.push(importExpr(Identifiers.ProvidersFeature).callFn(args));
|
|
}
|
|
// Note: host directives feature needs to be inserted before the
|
|
// inheritance feature to ensure the correct execution order.
|
|
if (meta.hostDirectives?.length) {
|
|
features.push(importExpr(Identifiers.HostDirectivesFeature)
|
|
.callFn([createHostDirectivesFeatureArg(meta.hostDirectives)]));
|
|
}
|
|
if (meta.usesInheritance) {
|
|
features.push(importExpr(Identifiers.InheritDefinitionFeature));
|
|
}
|
|
if (meta.fullInheritance) {
|
|
features.push(importExpr(Identifiers.CopyDefinitionFeature));
|
|
}
|
|
if (meta.lifecycle.usesOnChanges) {
|
|
features.push(importExpr(Identifiers.NgOnChangesFeature));
|
|
}
|
|
if ('externalStyles' in meta && meta.externalStyles?.length) {
|
|
const externalStyleNodes = meta.externalStyles.map((externalStyle) => literal(externalStyle));
|
|
features.push(importExpr(Identifiers.ExternalStylesFeature).callFn([literalArr(externalStyleNodes)]));
|
|
}
|
|
if (features.length) {
|
|
definitionMap.set('features', literalArr(features));
|
|
}
|
|
}
|
|
/**
|
|
* Compile a directive for the render3 runtime as defined by the `R3DirectiveMetadata`.
|
|
*/
|
|
function compileDirectiveFromMetadata(meta, constantPool, bindingParser) {
|
|
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
|
|
addFeatures(definitionMap, meta);
|
|
const expression = importExpr(Identifiers.defineDirective)
|
|
.callFn([definitionMap.toLiteralMap()], undefined, true);
|
|
const type = createDirectiveType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`.
|
|
*/
|
|
function compileComponentFromMetadata(meta, constantPool, bindingParser) {
|
|
const definitionMap = baseDirectiveFields(meta, constantPool, bindingParser);
|
|
addFeatures(definitionMap, meta);
|
|
const selector = meta.selector && CssSelector.parse(meta.selector);
|
|
const firstSelector = selector && selector[0];
|
|
// e.g. `attr: ["class", ".my.app"]`
|
|
// This is optional an only included if the first selector of a component specifies attributes.
|
|
if (firstSelector) {
|
|
const selectorAttributes = firstSelector.getAttrs();
|
|
if (selectorAttributes.length) {
|
|
definitionMap.set('attrs', constantPool.getConstLiteral(literalArr(selectorAttributes.map((value) => value != null ? literal(value) : literal(undefined))),
|
|
/* forceShared */ true));
|
|
}
|
|
}
|
|
// e.g. `template: function MyComponent_Template(_ctx, _cm) {...}`
|
|
const templateTypeName = meta.name;
|
|
let allDeferrableDepsFn = null;
|
|
if (meta.defer.mode === 1 /* DeferBlockDepsEmitMode.PerComponent */ &&
|
|
meta.defer.dependenciesFn !== null) {
|
|
const fnName = `${templateTypeName}_DeferFn`;
|
|
constantPool.statements.push(new DeclareVarStmt(fnName, meta.defer.dependenciesFn, undefined, StmtModifier.Final));
|
|
allDeferrableDepsFn = variable(fnName);
|
|
}
|
|
const compilationMode = meta.isStandalone && !meta.hasDirectiveDependencies
|
|
? TemplateCompilationMode.DomOnly
|
|
: TemplateCompilationMode.Full;
|
|
// First the template is ingested into IR:
|
|
const tpl = ingestComponent(meta.name, meta.template.nodes, constantPool, compilationMode, meta.relativeContextFilePath, meta.i18nUseExternalIds, meta.defer, allDeferrableDepsFn, meta.relativeTemplatePath, getTemplateSourceLocationsEnabled());
|
|
// Then the IR is transformed to prepare it for cod egeneration.
|
|
transform(tpl, CompilationJobKind.Tmpl);
|
|
// Finally we emit the template function:
|
|
const templateFn = emitTemplateFn(tpl, constantPool);
|
|
if (tpl.contentSelectors !== null) {
|
|
definitionMap.set('ngContentSelectors', tpl.contentSelectors);
|
|
}
|
|
definitionMap.set('decls', literal(tpl.root.decls));
|
|
definitionMap.set('vars', literal(tpl.root.vars));
|
|
if (tpl.consts.length > 0) {
|
|
if (tpl.constsInitializers.length > 0) {
|
|
definitionMap.set('consts', arrowFn([], [...tpl.constsInitializers, new ReturnStatement(literalArr(tpl.consts))]));
|
|
}
|
|
else {
|
|
definitionMap.set('consts', literalArr(tpl.consts));
|
|
}
|
|
}
|
|
definitionMap.set('template', templateFn);
|
|
if (meta.declarationListEmitMode !== 3 /* DeclarationListEmitMode.RuntimeResolved */ &&
|
|
meta.declarations.length > 0) {
|
|
definitionMap.set('dependencies', compileDeclarationList(literalArr(meta.declarations.map((decl) => decl.type)), meta.declarationListEmitMode));
|
|
}
|
|
else if (meta.declarationListEmitMode === 3 /* DeclarationListEmitMode.RuntimeResolved */) {
|
|
const args = [meta.type.value];
|
|
if (meta.rawImports) {
|
|
args.push(meta.rawImports);
|
|
}
|
|
definitionMap.set('dependencies', importExpr(Identifiers.getComponentDepsFactory).callFn(args));
|
|
}
|
|
if (meta.encapsulation === null) {
|
|
meta.encapsulation = ViewEncapsulation$1.Emulated;
|
|
}
|
|
let hasStyles = !!meta.externalStyles?.length;
|
|
// e.g. `styles: [str1, str2]`
|
|
if (meta.styles && meta.styles.length) {
|
|
const styleValues = meta.encapsulation == ViewEncapsulation$1.Emulated
|
|
? compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR)
|
|
: meta.styles;
|
|
const styleNodes = styleValues.reduce((result, style) => {
|
|
if (style.trim().length > 0) {
|
|
result.push(constantPool.getConstLiteral(literal(style)));
|
|
}
|
|
return result;
|
|
}, []);
|
|
if (styleNodes.length > 0) {
|
|
hasStyles = true;
|
|
definitionMap.set('styles', literalArr(styleNodes));
|
|
}
|
|
}
|
|
if (!hasStyles && meta.encapsulation === ViewEncapsulation$1.Emulated) {
|
|
// If there is no style, don't generate css selectors on elements
|
|
meta.encapsulation = ViewEncapsulation$1.None;
|
|
}
|
|
// Only set view encapsulation if it's not the default value
|
|
if (meta.encapsulation !== ViewEncapsulation$1.Emulated) {
|
|
definitionMap.set('encapsulation', literal(meta.encapsulation));
|
|
}
|
|
// e.g. `animation: [trigger('123', [])]`
|
|
if (meta.animations !== null) {
|
|
definitionMap.set('data', literalMap([{ key: 'animation', value: meta.animations, quoted: false }]));
|
|
}
|
|
// Setting change detection flag
|
|
if (meta.changeDetection !== null) {
|
|
if (typeof meta.changeDetection === 'number' &&
|
|
meta.changeDetection !== ChangeDetectionStrategy.Default) {
|
|
// changeDetection is resolved during analysis. Only set it if not the default.
|
|
definitionMap.set('changeDetection', literal(meta.changeDetection));
|
|
}
|
|
else if (typeof meta.changeDetection === 'object') {
|
|
// changeDetection is not resolved during analysis (e.g., we are in local compilation mode).
|
|
// So place it as is.
|
|
definitionMap.set('changeDetection', meta.changeDetection);
|
|
}
|
|
}
|
|
const expression = importExpr(Identifiers.defineComponent)
|
|
.callFn([definitionMap.toLiteralMap()], undefined, true);
|
|
const type = createComponentType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Creates the type specification from the component meta. This type is inserted into .d.ts files
|
|
* to be consumed by upstream compilations.
|
|
*/
|
|
function createComponentType(meta) {
|
|
const typeParams = createBaseDirectiveTypeParams(meta);
|
|
typeParams.push(stringArrayAsType(meta.template.ngContentSelectors));
|
|
typeParams.push(expressionType(literal(meta.isStandalone)));
|
|
typeParams.push(createHostDirectivesType(meta));
|
|
// TODO(signals): Always include this metadata starting with v17. Right
|
|
// now Angular v16.0.x does not support this field and library distributions
|
|
// would then be incompatible with v16.0.x framework users.
|
|
if (meta.isSignal) {
|
|
typeParams.push(expressionType(literal(meta.isSignal)));
|
|
}
|
|
return expressionType(importExpr(Identifiers.ComponentDeclaration, typeParams));
|
|
}
|
|
/**
|
|
* Compiles the array literal of declarations into an expression according to the provided emit
|
|
* mode.
|
|
*/
|
|
function compileDeclarationList(list, mode) {
|
|
switch (mode) {
|
|
case 0 /* DeclarationListEmitMode.Direct */:
|
|
// directives: [MyDir],
|
|
return list;
|
|
case 1 /* DeclarationListEmitMode.Closure */:
|
|
// directives: function () { return [MyDir]; }
|
|
return arrowFn([], list);
|
|
case 2 /* DeclarationListEmitMode.ClosureResolved */:
|
|
// directives: function () { return [MyDir].map(ng.resolveForwardRef); }
|
|
const resolvedList = list.prop('map').callFn([importExpr(Identifiers.resolveForwardRef)]);
|
|
return arrowFn([], resolvedList);
|
|
case 3 /* DeclarationListEmitMode.RuntimeResolved */:
|
|
throw new Error(`Unsupported with an array of pre-resolved dependencies`);
|
|
}
|
|
}
|
|
function stringAsType(str) {
|
|
return expressionType(literal(str));
|
|
}
|
|
function stringMapAsLiteralExpression(map) {
|
|
const mapValues = Object.keys(map).map((key) => {
|
|
const value = Array.isArray(map[key]) ? map[key][0] : map[key];
|
|
return {
|
|
key,
|
|
value: literal(value),
|
|
quoted: true,
|
|
};
|
|
});
|
|
return literalMap(mapValues);
|
|
}
|
|
function stringArrayAsType(arr) {
|
|
return arr.length > 0
|
|
? expressionType(literalArr(arr.map((value) => literal(value))))
|
|
: NONE_TYPE;
|
|
}
|
|
function createBaseDirectiveTypeParams(meta) {
|
|
// On the type side, remove newlines from the selector as it will need to fit into a TypeScript
|
|
// string literal, which must be on one line.
|
|
const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null;
|
|
return [
|
|
typeWithParameters(meta.type.type, meta.typeArgumentCount),
|
|
selectorForType !== null ? stringAsType(selectorForType) : NONE_TYPE,
|
|
meta.exportAs !== null ? stringArrayAsType(meta.exportAs) : NONE_TYPE,
|
|
expressionType(getInputsTypeExpression(meta)),
|
|
expressionType(stringMapAsLiteralExpression(meta.outputs)),
|
|
stringArrayAsType(meta.queries.map((q) => q.propertyName)),
|
|
];
|
|
}
|
|
function getInputsTypeExpression(meta) {
|
|
return literalMap(Object.keys(meta.inputs).map((key) => {
|
|
const value = meta.inputs[key];
|
|
const values = [
|
|
{ key: 'alias', value: literal(value.bindingPropertyName), quoted: true },
|
|
{ key: 'required', value: literal(value.required), quoted: true },
|
|
];
|
|
// TODO(legacy-partial-output-inputs): Consider always emitting this information,
|
|
// or leaving it as is.
|
|
if (value.isSignal) {
|
|
values.push({ key: 'isSignal', value: literal(value.isSignal), quoted: true });
|
|
}
|
|
return { key, value: literalMap(values), quoted: true };
|
|
}));
|
|
}
|
|
/**
|
|
* Creates the type specification from the directive meta. This type is inserted into .d.ts files
|
|
* to be consumed by upstream compilations.
|
|
*/
|
|
function createDirectiveType(meta) {
|
|
const typeParams = createBaseDirectiveTypeParams(meta);
|
|
// Directives have no NgContentSelectors slot, but instead express a `never` type
|
|
// so that future fields align.
|
|
typeParams.push(NONE_TYPE);
|
|
typeParams.push(expressionType(literal(meta.isStandalone)));
|
|
typeParams.push(createHostDirectivesType(meta));
|
|
// TODO(signals): Always include this metadata starting with v17. Right
|
|
// now Angular v16.0.x does not support this field and library distributions
|
|
// would then be incompatible with v16.0.x framework users.
|
|
if (meta.isSignal) {
|
|
typeParams.push(expressionType(literal(meta.isSignal)));
|
|
}
|
|
return expressionType(importExpr(Identifiers.DirectiveDeclaration, typeParams));
|
|
}
|
|
// Return a host binding function or null if one is not necessary.
|
|
function createHostBindingsFunction(hostBindingsMetadata, typeSourceSpan, bindingParser, constantPool, selector, name, definitionMap) {
|
|
const bindings = bindingParser.createBoundHostProperties(hostBindingsMetadata.properties, typeSourceSpan);
|
|
// Calculate host event bindings
|
|
const eventBindings = bindingParser.createDirectiveHostEventAsts(hostBindingsMetadata.listeners, typeSourceSpan);
|
|
// The parser for host bindings treats class and style attributes specially -- they are
|
|
// extracted into these separate fields. This is not the case for templates, so the compiler can
|
|
// actually already handle these special attributes internally. Therefore, we just drop them
|
|
// into the attributes map.
|
|
if (hostBindingsMetadata.specialAttributes.styleAttr) {
|
|
hostBindingsMetadata.attributes['style'] = literal(hostBindingsMetadata.specialAttributes.styleAttr);
|
|
}
|
|
if (hostBindingsMetadata.specialAttributes.classAttr) {
|
|
hostBindingsMetadata.attributes['class'] = literal(hostBindingsMetadata.specialAttributes.classAttr);
|
|
}
|
|
const hostJob = ingestHostBinding({
|
|
componentName: name,
|
|
componentSelector: selector,
|
|
properties: bindings,
|
|
events: eventBindings,
|
|
attributes: hostBindingsMetadata.attributes,
|
|
}, bindingParser, constantPool);
|
|
transform(hostJob, CompilationJobKind.Host);
|
|
definitionMap.set('hostAttrs', hostJob.root.attributes);
|
|
const varCount = hostJob.root.vars;
|
|
if (varCount !== null && varCount > 0) {
|
|
definitionMap.set('hostVars', literal(varCount));
|
|
}
|
|
return emitHostBindingFunction(hostJob);
|
|
}
|
|
const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/;
|
|
function parseHostBindings(host) {
|
|
const attributes = {};
|
|
const listeners = {};
|
|
const properties = {};
|
|
const specialAttributes = {};
|
|
for (const key of Object.keys(host)) {
|
|
const value = host[key];
|
|
const matches = key.match(HOST_REG_EXP);
|
|
if (matches === null) {
|
|
switch (key) {
|
|
case 'class':
|
|
if (typeof value !== 'string') {
|
|
// TODO(alxhub): make this a diagnostic.
|
|
throw new Error(`Class binding must be string`);
|
|
}
|
|
specialAttributes.classAttr = value;
|
|
break;
|
|
case 'style':
|
|
if (typeof value !== 'string') {
|
|
// TODO(alxhub): make this a diagnostic.
|
|
throw new Error(`Style binding must be string`);
|
|
}
|
|
specialAttributes.styleAttr = value;
|
|
break;
|
|
default:
|
|
if (typeof value === 'string') {
|
|
attributes[key] = literal(value);
|
|
}
|
|
else {
|
|
attributes[key] = value;
|
|
}
|
|
}
|
|
}
|
|
else if (matches[1 /* HostBindingGroup.Binding */] != null) {
|
|
if (typeof value !== 'string') {
|
|
// TODO(alxhub): make this a diagnostic.
|
|
throw new Error(`Property binding must be string`);
|
|
}
|
|
// synthetic properties (the ones that have a `@` as a prefix)
|
|
// are still treated the same as regular properties. Therefore
|
|
// there is no point in storing them in a separate map.
|
|
properties[matches[1 /* HostBindingGroup.Binding */]] = value;
|
|
}
|
|
else if (matches[2 /* HostBindingGroup.Event */] != null) {
|
|
if (typeof value !== 'string') {
|
|
// TODO(alxhub): make this a diagnostic.
|
|
throw new Error(`Event binding must be string`);
|
|
}
|
|
listeners[matches[2 /* HostBindingGroup.Event */]] = value;
|
|
}
|
|
}
|
|
return { attributes, listeners, properties, specialAttributes };
|
|
}
|
|
/**
|
|
* Verifies host bindings and returns the list of errors (if any). Empty array indicates that a
|
|
* given set of host bindings has no errors.
|
|
*
|
|
* @param bindings set of host bindings to verify.
|
|
* @param sourceSpan source span where host bindings were defined.
|
|
* @returns array of errors associated with a given set of host bindings.
|
|
*/
|
|
function verifyHostBindings(bindings, sourceSpan) {
|
|
// TODO: abstract out host bindings verification logic and use it instead of
|
|
// creating events and properties ASTs to detect errors (FW-996)
|
|
const bindingParser = makeBindingParser();
|
|
bindingParser.createDirectiveHostEventAsts(bindings.listeners, sourceSpan);
|
|
bindingParser.createBoundHostProperties(bindings.properties, sourceSpan);
|
|
return bindingParser.errors;
|
|
}
|
|
function compileStyles(styles, selector, hostSelector) {
|
|
const shadowCss = new ShadowCss();
|
|
return styles.map((style) => {
|
|
return shadowCss.shimCssText(style, selector, hostSelector);
|
|
});
|
|
}
|
|
/**
|
|
* Encapsulates a CSS stylesheet with emulated view encapsulation.
|
|
* This allows a stylesheet to be used with an Angular component that
|
|
* is using the `ViewEncapsulation.Emulated` mode.
|
|
*
|
|
* @param style The content of a CSS stylesheet.
|
|
* @param componentIdentifier The identifier to use within the CSS rules.
|
|
* @returns The encapsulated content for the style.
|
|
*/
|
|
function encapsulateStyle(style, componentIdentifier) {
|
|
const shadowCss = new ShadowCss();
|
|
const selector = componentIdentifier
|
|
? CONTENT_ATTR.replace(COMPONENT_VARIABLE, componentIdentifier)
|
|
: CONTENT_ATTR;
|
|
const hostSelector = componentIdentifier
|
|
? HOST_ATTR.replace(COMPONENT_VARIABLE, componentIdentifier)
|
|
: HOST_ATTR;
|
|
return shadowCss.shimCssText(style, selector, hostSelector);
|
|
}
|
|
function createHostDirectivesType(meta) {
|
|
if (!meta.hostDirectives?.length) {
|
|
return NONE_TYPE;
|
|
}
|
|
return expressionType(literalArr(meta.hostDirectives.map((hostMeta) => literalMap([
|
|
{ key: 'directive', value: typeofExpr(hostMeta.directive.type), quoted: false },
|
|
{
|
|
key: 'inputs',
|
|
value: stringMapAsLiteralExpression(hostMeta.inputs || {}),
|
|
quoted: false,
|
|
},
|
|
{
|
|
key: 'outputs',
|
|
value: stringMapAsLiteralExpression(hostMeta.outputs || {}),
|
|
quoted: false,
|
|
},
|
|
]))));
|
|
}
|
|
function createHostDirectivesFeatureArg(hostDirectives) {
|
|
const expressions = [];
|
|
let hasForwardRef = false;
|
|
for (const current of hostDirectives) {
|
|
// Use a shorthand if there are no inputs or outputs.
|
|
if (!current.inputs && !current.outputs) {
|
|
expressions.push(current.directive.type);
|
|
}
|
|
else {
|
|
const keys = [{ key: 'directive', value: current.directive.type, quoted: false }];
|
|
if (current.inputs) {
|
|
const inputsLiteral = createHostDirectivesMappingArray(current.inputs);
|
|
if (inputsLiteral) {
|
|
keys.push({ key: 'inputs', value: inputsLiteral, quoted: false });
|
|
}
|
|
}
|
|
if (current.outputs) {
|
|
const outputsLiteral = createHostDirectivesMappingArray(current.outputs);
|
|
if (outputsLiteral) {
|
|
keys.push({ key: 'outputs', value: outputsLiteral, quoted: false });
|
|
}
|
|
}
|
|
expressions.push(literalMap(keys));
|
|
}
|
|
if (current.isForwardReference) {
|
|
hasForwardRef = true;
|
|
}
|
|
}
|
|
// If there's a forward reference, we generate a `function() { return [HostDir] }`,
|
|
// otherwise we can save some bytes by using a plain array, e.g. `[HostDir]`.
|
|
return hasForwardRef
|
|
? new FunctionExpr([], [new ReturnStatement(literalArr(expressions))])
|
|
: literalArr(expressions);
|
|
}
|
|
/**
|
|
* Converts an input/output mapping object literal into an array where the even keys are the
|
|
* public name of the binding and the odd ones are the name it was aliased to. E.g.
|
|
* `{inputOne: 'aliasOne', inputTwo: 'aliasTwo'}` will become
|
|
* `['inputOne', 'aliasOne', 'inputTwo', 'aliasTwo']`.
|
|
*
|
|
* This conversion is necessary, because hosts bind to the public name of the host directive and
|
|
* keeping the mapping in an object literal will break for apps using property renaming.
|
|
*/
|
|
function createHostDirectivesMappingArray(mapping) {
|
|
const elements = [];
|
|
for (const publicName in mapping) {
|
|
if (mapping.hasOwnProperty(publicName)) {
|
|
elements.push(literal(publicName), literal(mapping[publicName]));
|
|
}
|
|
}
|
|
return elements.length > 0 ? literalArr(elements) : null;
|
|
}
|
|
/**
|
|
* Compiles the dependency resolver function for a defer block.
|
|
*/
|
|
function compileDeferResolverFunction(meta) {
|
|
const depExpressions = [];
|
|
if (meta.mode === 0 /* DeferBlockDepsEmitMode.PerBlock */) {
|
|
for (const dep of meta.dependencies) {
|
|
if (dep.isDeferrable) {
|
|
// Callback function, e.g. `m () => m.MyCmp;`.
|
|
const innerFn = arrowFn(
|
|
// Default imports are always accessed through the `default` property.
|
|
[new FnParam('m', DYNAMIC_TYPE)], variable('m').prop(dep.isDefaultImport ? 'default' : dep.symbolName));
|
|
// Dynamic import, e.g. `import('./a').then(...)`.
|
|
const importExpr = new DynamicImportExpr(dep.importPath).prop('then').callFn([innerFn]);
|
|
depExpressions.push(importExpr);
|
|
}
|
|
else {
|
|
// Non-deferrable symbol, just use a reference to the type. Note that it's important to
|
|
// go through `typeReference`, rather than `symbolName` in order to preserve the
|
|
// original reference within the source file.
|
|
depExpressions.push(dep.typeReference);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (const { symbolName, importPath, isDefaultImport } of meta.dependencies) {
|
|
// Callback function, e.g. `m () => m.MyCmp;`.
|
|
const innerFn = arrowFn([new FnParam('m', DYNAMIC_TYPE)], variable('m').prop(isDefaultImport ? 'default' : symbolName));
|
|
// Dynamic import, e.g. `import('./a').then(...)`.
|
|
const importExpr = new DynamicImportExpr(importPath).prop('then').callFn([innerFn]);
|
|
depExpressions.push(importExpr);
|
|
}
|
|
}
|
|
return arrowFn([], literalArr(depExpressions));
|
|
}
|
|
|
|
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
/**
|
|
* Visitor that traverses all template and expression AST nodes in a template.
|
|
* Useful for cases where every single node needs to be visited.
|
|
*/
|
|
class CombinedRecursiveAstVisitor extends RecursiveAstVisitor {
|
|
visit(node) {
|
|
if (node instanceof ASTWithSource) {
|
|
this.visit(node.ast);
|
|
}
|
|
else {
|
|
node.visit(this);
|
|
}
|
|
}
|
|
visitElement(element) {
|
|
this.visitAllTemplateNodes(element.attributes);
|
|
this.visitAllTemplateNodes(element.inputs);
|
|
this.visitAllTemplateNodes(element.outputs);
|
|
this.visitAllTemplateNodes(element.directives);
|
|
this.visitAllTemplateNodes(element.references);
|
|
this.visitAllTemplateNodes(element.children);
|
|
}
|
|
visitTemplate(template) {
|
|
this.visitAllTemplateNodes(template.attributes);
|
|
this.visitAllTemplateNodes(template.inputs);
|
|
this.visitAllTemplateNodes(template.outputs);
|
|
this.visitAllTemplateNodes(template.directives);
|
|
this.visitAllTemplateNodes(template.templateAttrs);
|
|
this.visitAllTemplateNodes(template.variables);
|
|
this.visitAllTemplateNodes(template.references);
|
|
this.visitAllTemplateNodes(template.children);
|
|
}
|
|
visitContent(content) {
|
|
this.visitAllTemplateNodes(content.children);
|
|
}
|
|
visitBoundAttribute(attribute) {
|
|
this.visit(attribute.value);
|
|
}
|
|
visitBoundEvent(attribute) {
|
|
this.visit(attribute.handler);
|
|
}
|
|
visitBoundText(text) {
|
|
this.visit(text.value);
|
|
}
|
|
visitIcu(icu) {
|
|
Object.keys(icu.vars).forEach((key) => this.visit(icu.vars[key]));
|
|
Object.keys(icu.placeholders).forEach((key) => this.visit(icu.placeholders[key]));
|
|
}
|
|
visitDeferredBlock(deferred) {
|
|
deferred.visitAll(this);
|
|
}
|
|
visitDeferredTrigger(trigger) {
|
|
if (trigger instanceof BoundDeferredTrigger) {
|
|
this.visit(trigger.value);
|
|
}
|
|
}
|
|
visitDeferredBlockPlaceholder(block) {
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitDeferredBlockError(block) {
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitDeferredBlockLoading(block) {
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitSwitchBlock(block) {
|
|
this.visit(block.expression);
|
|
this.visitAllTemplateNodes(block.cases);
|
|
}
|
|
visitSwitchBlockCase(block) {
|
|
block.expression && this.visit(block.expression);
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitForLoopBlock(block) {
|
|
block.item.visit(this);
|
|
this.visitAllTemplateNodes(block.contextVariables);
|
|
this.visit(block.expression);
|
|
this.visitAllTemplateNodes(block.children);
|
|
block.empty?.visit(this);
|
|
}
|
|
visitForLoopBlockEmpty(block) {
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitIfBlock(block) {
|
|
this.visitAllTemplateNodes(block.branches);
|
|
}
|
|
visitIfBlockBranch(block) {
|
|
block.expression && this.visit(block.expression);
|
|
block.expressionAlias?.visit(this);
|
|
this.visitAllTemplateNodes(block.children);
|
|
}
|
|
visitLetDeclaration(decl) {
|
|
this.visit(decl.value);
|
|
}
|
|
visitComponent(component) {
|
|
this.visitAllTemplateNodes(component.attributes);
|
|
this.visitAllTemplateNodes(component.inputs);
|
|
this.visitAllTemplateNodes(component.outputs);
|
|
this.visitAllTemplateNodes(component.directives);
|
|
this.visitAllTemplateNodes(component.references);
|
|
this.visitAllTemplateNodes(component.children);
|
|
}
|
|
visitDirective(directive) {
|
|
this.visitAllTemplateNodes(directive.attributes);
|
|
this.visitAllTemplateNodes(directive.inputs);
|
|
this.visitAllTemplateNodes(directive.outputs);
|
|
this.visitAllTemplateNodes(directive.references);
|
|
}
|
|
visitVariable(variable) { }
|
|
visitReference(reference) { }
|
|
visitTextAttribute(attribute) { }
|
|
visitText(text) { }
|
|
visitUnknownBlock(block) { }
|
|
visitAllTemplateNodes(nodes) {
|
|
for (const node of nodes) {
|
|
this.visit(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes a difference between full list (first argument) and
|
|
* list of items that should be excluded from the full list (second
|
|
* argument).
|
|
*/
|
|
function diff(fullList, itemsToExclude) {
|
|
const exclude = new Set(itemsToExclude);
|
|
return fullList.filter((item) => !exclude.has(item));
|
|
}
|
|
/**
|
|
* Given a template string and a set of available directive selectors,
|
|
* computes a list of matching selectors and splits them into 2 buckets:
|
|
* (1) eagerly used in a template and (2) directives used only in defer
|
|
* blocks. Similarly, returns 2 lists of pipes (eager and deferrable).
|
|
*
|
|
* Note: deferrable directives selectors and pipes names used in `@defer`
|
|
* blocks are **candidates** and API caller should make sure that:
|
|
*
|
|
* * A Component where a given template is defined is standalone
|
|
* * Underlying dependency classes are also standalone
|
|
* * Dependency class symbols are not eagerly used in a TS file
|
|
* where a host component (that owns the template) is located
|
|
*/
|
|
function findMatchingDirectivesAndPipes(template, directiveSelectors) {
|
|
const matcher = new SelectorMatcher();
|
|
for (const selector of directiveSelectors) {
|
|
// Create a fake directive instance to account for the logic inside
|
|
// of the `R3TargetBinder` class (which invokes the `hasBindingPropertyName`
|
|
// function internally).
|
|
const fakeDirective = {
|
|
selector,
|
|
exportAs: null,
|
|
inputs: {
|
|
hasBindingPropertyName() {
|
|
return false;
|
|
},
|
|
},
|
|
outputs: {
|
|
hasBindingPropertyName() {
|
|
return false;
|
|
},
|
|
},
|
|
};
|
|
matcher.addSelectables(CssSelector.parse(selector), [fakeDirective]);
|
|
}
|
|
const parsedTemplate = parseTemplate(template, '' /* templateUrl */);
|
|
const binder = new R3TargetBinder(matcher);
|
|
const bound = binder.bind({ template: parsedTemplate.nodes });
|
|
const eagerDirectiveSelectors = bound.getEagerlyUsedDirectives().map((dir) => dir.selector);
|
|
const allMatchedDirectiveSelectors = bound.getUsedDirectives().map((dir) => dir.selector);
|
|
const eagerPipes = bound.getEagerlyUsedPipes();
|
|
return {
|
|
directives: {
|
|
regular: eagerDirectiveSelectors,
|
|
deferCandidates: diff(allMatchedDirectiveSelectors, eagerDirectiveSelectors),
|
|
},
|
|
pipes: {
|
|
regular: eagerPipes,
|
|
deferCandidates: diff(bound.getUsedPipes(), eagerPipes),
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Processes `Target`s with a given set of directives and performs a binding operation, which
|
|
* returns an object similar to TypeScript's `ts.TypeChecker` that contains knowledge about the
|
|
* target.
|
|
*/
|
|
class R3TargetBinder {
|
|
directiveMatcher;
|
|
constructor(directiveMatcher) {
|
|
this.directiveMatcher = directiveMatcher;
|
|
}
|
|
/**
|
|
* Perform a binding operation on the given `Target` and return a `BoundTarget` which contains
|
|
* metadata about the types referenced in the template.
|
|
*/
|
|
bind(target) {
|
|
if (!target.template && !target.host) {
|
|
throw new Error('Empty bound targets are not supported');
|
|
}
|
|
const directives = new Map();
|
|
const eagerDirectives = [];
|
|
const missingDirectives = new Set();
|
|
const bindings = new Map();
|
|
const references = new Map();
|
|
const scopedNodeEntities = new Map();
|
|
const expressions = new Map();
|
|
const symbols = new Map();
|
|
const nestingLevel = new Map();
|
|
const usedPipes = new Set();
|
|
const eagerPipes = new Set();
|
|
const deferBlocks = [];
|
|
if (target.template) {
|
|
// First, parse the template into a `Scope` structure. This operation captures the syntactic
|
|
// scopes in the template and makes them available for later use.
|
|
const scope = Scope.apply(target.template);
|
|
// Use the `Scope` to extract the entities present at every level of the template.
|
|
extractScopedNodeEntities(scope, scopedNodeEntities);
|
|
// Next, perform directive matching on the template using the `DirectiveBinder`. This returns:
|
|
// - directives: Map of nodes (elements & ng-templates) to the directives on them.
|
|
// - bindings: Map of inputs, outputs, and attributes to the directive/element that claims
|
|
// them. TODO(alxhub): handle multiple directives claiming an input/output/etc.
|
|
// - references: Map of #references to their targets.
|
|
DirectiveBinder.apply(target.template, this.directiveMatcher, directives, eagerDirectives, missingDirectives, bindings, references);
|
|
// Finally, run the TemplateBinder to bind references, variables, and other entities within the
|
|
// template. This extracts all the metadata that doesn't depend on directive matching.
|
|
TemplateBinder.applyWithScope(target.template, scope, expressions, symbols, nestingLevel, usedPipes, eagerPipes, deferBlocks);
|
|
}
|
|
// Bind the host element in a separate scope. Note that it only uses the
|
|
// `TemplateBinder` since directives don't apply inside a host context.
|
|
if (target.host) {
|
|
directives.set(target.host.node, target.host.directives);
|
|
TemplateBinder.applyWithScope(target.host.node, Scope.apply(target.host.node), expressions, symbols, nestingLevel, usedPipes, eagerPipes, deferBlocks);
|
|
}
|
|
return new R3BoundTarget(target, directives, eagerDirectives, missingDirectives, bindings, references, expressions, symbols, nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, deferBlocks);
|
|
}
|
|
}
|
|
/**
|
|
* Represents a binding scope within a template.
|
|
*
|
|
* Any variables, references, or other named entities declared within the template will
|
|
* be captured and available by name in `namedEntities`. Additionally, child templates will
|
|
* be analyzed and have their child `Scope`s available in `childScopes`.
|
|
*/
|
|
class Scope {
|
|
parentScope;
|
|
rootNode;
|
|
/**
|
|
* Named members of the `Scope`, such as `Reference`s or `Variable`s.
|
|
*/
|
|
namedEntities = new Map();
|
|
/**
|
|
* Set of element-like nodes that belong to this scope.
|
|
*/
|
|
elementLikeInScope = new Set();
|
|
/**
|
|
* Child `Scope`s for immediately nested `ScopedNode`s.
|
|
*/
|
|
childScopes = new Map();
|
|
/** Whether this scope is deferred or if any of its ancestors are deferred. */
|
|
isDeferred;
|
|
constructor(parentScope, rootNode) {
|
|
this.parentScope = parentScope;
|
|
this.rootNode = rootNode;
|
|
this.isDeferred =
|
|
parentScope !== null && parentScope.isDeferred ? true : rootNode instanceof DeferredBlock;
|
|
}
|
|
static newRootScope() {
|
|
return new Scope(null, null);
|
|
}
|
|
/**
|
|
* Process a template (either as a `Template` sub-template with variables, or a plain array of
|
|
* template `Node`s) and construct its `Scope`.
|
|
*/
|
|
static apply(template) {
|
|
const scope = Scope.newRootScope();
|
|
scope.ingest(template);
|
|
return scope;
|
|
}
|
|
/**
|
|
* Internal method to process the scoped node and populate the `Scope`.
|
|
*/
|
|
ingest(nodeOrNodes) {
|
|
if (nodeOrNodes instanceof Template) {
|
|
// Variables on an <ng-template> are defined in the inner scope.
|
|
nodeOrNodes.variables.forEach((node) => this.visitVariable(node));
|
|
// Process the nodes of the template.
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
}
|
|
else if (nodeOrNodes instanceof IfBlockBranch) {
|
|
if (nodeOrNodes.expressionAlias !== null) {
|
|
this.visitVariable(nodeOrNodes.expressionAlias);
|
|
}
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
}
|
|
else if (nodeOrNodes instanceof ForLoopBlock) {
|
|
this.visitVariable(nodeOrNodes.item);
|
|
nodeOrNodes.contextVariables.forEach((v) => this.visitVariable(v));
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
}
|
|
else if (nodeOrNodes instanceof SwitchBlockCase ||
|
|
nodeOrNodes instanceof ForLoopBlockEmpty ||
|
|
nodeOrNodes instanceof DeferredBlock ||
|
|
nodeOrNodes instanceof DeferredBlockError ||
|
|
nodeOrNodes instanceof DeferredBlockPlaceholder ||
|
|
nodeOrNodes instanceof DeferredBlockLoading ||
|
|
nodeOrNodes instanceof Content) {
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
}
|
|
else if (!(nodeOrNodes instanceof HostElement)) {
|
|
// No overarching `Template` instance, so process the nodes directly.
|
|
nodeOrNodes.forEach((node) => node.visit(this));
|
|
}
|
|
}
|
|
visitElement(element) {
|
|
this.visitElementLike(element);
|
|
}
|
|
visitTemplate(template) {
|
|
template.directives.forEach((node) => node.visit(this));
|
|
// References on a <ng-template> are defined in the outer scope, so capture them before
|
|
// processing the template's child scope.
|
|
template.references.forEach((node) => this.visitReference(node));
|
|
// Next, create an inner scope and process the template within it.
|
|
this.ingestScopedNode(template);
|
|
}
|
|
visitVariable(variable) {
|
|
// Declare the variable if it's not already.
|
|
this.maybeDeclare(variable);
|
|
}
|
|
visitReference(reference) {
|
|
// Declare the variable if it's not already.
|
|
this.maybeDeclare(reference);
|
|
}
|
|
visitDeferredBlock(deferred) {
|
|
this.ingestScopedNode(deferred);
|
|
deferred.placeholder?.visit(this);
|
|
deferred.loading?.visit(this);
|
|
deferred.error?.visit(this);
|
|
}
|
|
visitDeferredBlockPlaceholder(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitDeferredBlockError(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitDeferredBlockLoading(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitSwitchBlock(block) {
|
|
block.cases.forEach((node) => node.visit(this));
|
|
}
|
|
visitSwitchBlockCase(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitForLoopBlock(block) {
|
|
this.ingestScopedNode(block);
|
|
block.empty?.visit(this);
|
|
}
|
|
visitForLoopBlockEmpty(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitIfBlock(block) {
|
|
block.branches.forEach((node) => node.visit(this));
|
|
}
|
|
visitIfBlockBranch(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitContent(content) {
|
|
this.ingestScopedNode(content);
|
|
}
|
|
visitLetDeclaration(decl) {
|
|
this.maybeDeclare(decl);
|
|
}
|
|
visitComponent(component) {
|
|
this.visitElementLike(component);
|
|
}
|
|
visitDirective(directive) {
|
|
directive.references.forEach((current) => this.visitReference(current));
|
|
}
|
|
// Unused visitors.
|
|
visitBoundAttribute(attr) { }
|
|
visitBoundEvent(event) { }
|
|
visitBoundText(text) { }
|
|
visitText(text) { }
|
|
visitTextAttribute(attr) { }
|
|
visitIcu(icu) { }
|
|
visitDeferredTrigger(trigger) { }
|
|
visitUnknownBlock(block) { }
|
|
visitElementLike(node) {
|
|
node.directives.forEach((current) => current.visit(this));
|
|
node.references.forEach((current) => this.visitReference(current));
|
|
node.children.forEach((current) => current.visit(this));
|
|
this.elementLikeInScope.add(node);
|
|
}
|
|
maybeDeclare(thing) {
|
|
// Declare something with a name, as long as that name isn't taken.
|
|
if (!this.namedEntities.has(thing.name)) {
|
|
this.namedEntities.set(thing.name, thing);
|
|
}
|
|
}
|
|
/**
|
|
* Look up a variable within this `Scope`.
|
|
*
|
|
* This can recurse into a parent `Scope` if it's available.
|
|
*/
|
|
lookup(name) {
|
|
if (this.namedEntities.has(name)) {
|
|
// Found in the local scope.
|
|
return this.namedEntities.get(name);
|
|
}
|
|
else if (this.parentScope !== null) {
|
|
// Not in the local scope, but there's a parent scope so check there.
|
|
return this.parentScope.lookup(name);
|
|
}
|
|
else {
|
|
// At the top level and it wasn't found.
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Get the child scope for a `ScopedNode`.
|
|
*
|
|
* This should always be defined.
|
|
*/
|
|
getChildScope(node) {
|
|
const res = this.childScopes.get(node);
|
|
if (res === undefined) {
|
|
throw new Error(`Assertion error: child scope for ${node} not found`);
|
|
}
|
|
return res;
|
|
}
|
|
ingestScopedNode(node) {
|
|
const scope = new Scope(this, node);
|
|
scope.ingest(node);
|
|
this.childScopes.set(node, scope);
|
|
}
|
|
}
|
|
/**
|
|
* Processes a template and matches directives on nodes (elements and templates).
|
|
*
|
|
* Usually used via the static `apply()` method.
|
|
*/
|
|
class DirectiveBinder {
|
|
directiveMatcher;
|
|
directives;
|
|
eagerDirectives;
|
|
missingDirectives;
|
|
bindings;
|
|
references;
|
|
// Indicates whether we are visiting elements within a `defer` block
|
|
isInDeferBlock = false;
|
|
constructor(directiveMatcher, directives, eagerDirectives, missingDirectives, bindings, references) {
|
|
this.directiveMatcher = directiveMatcher;
|
|
this.directives = directives;
|
|
this.eagerDirectives = eagerDirectives;
|
|
this.missingDirectives = missingDirectives;
|
|
this.bindings = bindings;
|
|
this.references = references;
|
|
}
|
|
/**
|
|
* Process a template (list of `Node`s) and perform directive matching against each node.
|
|
*
|
|
* @param template the list of template `Node`s to match (recursively).
|
|
* @param selectorMatcher a `SelectorMatcher` containing the directives that are in scope for
|
|
* this template.
|
|
* @returns three maps which contain information about directives in the template: the
|
|
* `directives` map which lists directives matched on each node, the `bindings` map which
|
|
* indicates which directives claimed which bindings (inputs, outputs, etc), and the `references`
|
|
* map which resolves #references (`Reference`s) within the template to the named directive or
|
|
* template node.
|
|
*/
|
|
static apply(template, directiveMatcher, directives, eagerDirectives, missingDirectives, bindings, references) {
|
|
const matcher = new DirectiveBinder(directiveMatcher, directives, eagerDirectives, missingDirectives, bindings, references);
|
|
matcher.ingest(template);
|
|
}
|
|
ingest(template) {
|
|
template.forEach((node) => node.visit(this));
|
|
}
|
|
visitElement(element) {
|
|
this.visitElementOrTemplate(element);
|
|
}
|
|
visitTemplate(template) {
|
|
this.visitElementOrTemplate(template);
|
|
}
|
|
visitDeferredBlock(deferred) {
|
|
const wasInDeferBlock = this.isInDeferBlock;
|
|
this.isInDeferBlock = true;
|
|
deferred.children.forEach((child) => child.visit(this));
|
|
this.isInDeferBlock = wasInDeferBlock;
|
|
deferred.placeholder?.visit(this);
|
|
deferred.loading?.visit(this);
|
|
deferred.error?.visit(this);
|
|
}
|
|
visitDeferredBlockPlaceholder(block) {
|
|
block.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitDeferredBlockError(block) {
|
|
block.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitDeferredBlockLoading(block) {
|
|
block.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitSwitchBlock(block) {
|
|
block.cases.forEach((node) => node.visit(this));
|
|
}
|
|
visitSwitchBlockCase(block) {
|
|
block.children.forEach((node) => node.visit(this));
|
|
}
|
|
visitForLoopBlock(block) {
|
|
block.item.visit(this);
|
|
block.contextVariables.forEach((v) => v.visit(this));
|
|
block.children.forEach((node) => node.visit(this));
|
|
block.empty?.visit(this);
|
|
}
|
|
visitForLoopBlockEmpty(block) {
|
|
block.children.forEach((node) => node.visit(this));
|
|
}
|
|
visitIfBlock(block) {
|
|
block.branches.forEach((node) => node.visit(this));
|
|
}
|
|
visitIfBlockBranch(block) {
|
|
block.expressionAlias?.visit(this);
|
|
block.children.forEach((node) => node.visit(this));
|
|
}
|
|
visitContent(content) {
|
|
content.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitComponent(node) {
|
|
if (this.directiveMatcher instanceof SelectorlessMatcher) {
|
|
const componentMatches = this.directiveMatcher.match(node.componentName);
|
|
if (componentMatches.length > 0) {
|
|
this.trackSelectorlessMatchesAndDirectives(node, componentMatches);
|
|
}
|
|
else {
|
|
this.missingDirectives.add(node.componentName);
|
|
}
|
|
}
|
|
node.directives.forEach((directive) => directive.visit(this));
|
|
node.children.forEach((child) => child.visit(this));
|
|
}
|
|
visitDirective(node) {
|
|
if (this.directiveMatcher instanceof SelectorlessMatcher) {
|
|
const directives = this.directiveMatcher.match(node.name);
|
|
if (directives.length > 0) {
|
|
this.trackSelectorlessMatchesAndDirectives(node, directives);
|
|
}
|
|
else {
|
|
this.missingDirectives.add(node.name);
|
|
}
|
|
}
|
|
}
|
|
visitElementOrTemplate(node) {
|
|
if (this.directiveMatcher instanceof SelectorMatcher) {
|
|
const directives = [];
|
|
const cssSelector = createCssSelectorFromNode(node);
|
|
this.directiveMatcher.match(cssSelector, (_, results) => directives.push(...results));
|
|
this.trackSelectorBasedBindingsAndDirectives(node, directives);
|
|
}
|
|
else {
|
|
node.references.forEach((ref) => {
|
|
if (ref.value.trim() === '') {
|
|
this.references.set(ref, node);
|
|
}
|
|
});
|
|
}
|
|
node.directives.forEach((directive) => directive.visit(this));
|
|
node.children.forEach((child) => child.visit(this));
|
|
}
|
|
trackMatchedDirectives(node, directives) {
|
|
if (directives.length > 0) {
|
|
this.directives.set(node, directives);
|
|
if (!this.isInDeferBlock) {
|
|
this.eagerDirectives.push(...directives);
|
|
}
|
|
}
|
|
}
|
|
trackSelectorlessMatchesAndDirectives(node, directives) {
|
|
if (directives.length === 0) {
|
|
return;
|
|
}
|
|
this.trackMatchedDirectives(node, directives);
|
|
const setBinding = (meta, attribute, ioType) => {
|
|
if (meta[ioType].hasBindingPropertyName(attribute.name)) {
|
|
this.bindings.set(attribute, meta);
|
|
}
|
|
};
|
|
for (const directive of directives) {
|
|
node.inputs.forEach((input) => setBinding(directive, input, 'inputs'));
|
|
node.attributes.forEach((attr) => setBinding(directive, attr, 'inputs'));
|
|
node.outputs.forEach((output) => setBinding(directive, output, 'outputs'));
|
|
}
|
|
// TODO(crisbeto): currently it's unclear how references should behave under selectorless,
|
|
// given that there's one named class which can bring in multiple host directives.
|
|
// For the time being only register the first directive as the reference target.
|
|
node.references.forEach((ref) => this.references.set(ref, { directive: directives[0], node: node }));
|
|
}
|
|
trackSelectorBasedBindingsAndDirectives(node, directives) {
|
|
this.trackMatchedDirectives(node, directives);
|
|
// Resolve any references that are created on this node.
|
|
node.references.forEach((ref) => {
|
|
let dirTarget = null;
|
|
// If the reference expression is empty, then it matches the "primary" directive on the node
|
|
// (if there is one). Otherwise it matches the host node itself (either an element or
|
|
// <ng-template> node).
|
|
if (ref.value.trim() === '') {
|
|
// This could be a reference to a component if there is one.
|
|
dirTarget = directives.find((dir) => dir.isComponent) || null;
|
|
}
|
|
else {
|
|
// This should be a reference to a directive exported via exportAs.
|
|
dirTarget =
|
|
directives.find((dir) => dir.exportAs !== null && dir.exportAs.some((value) => value === ref.value)) || null;
|
|
// Check if a matching directive was found.
|
|
if (dirTarget === null) {
|
|
// No matching directive was found - this reference points to an unknown target. Leave it
|
|
// unmapped.
|
|
return;
|
|
}
|
|
}
|
|
if (dirTarget !== null) {
|
|
// This reference points to a directive.
|
|
this.references.set(ref, { directive: dirTarget, node });
|
|
}
|
|
else {
|
|
// This reference points to the node itself.
|
|
this.references.set(ref, node);
|
|
}
|
|
});
|
|
// Associate attributes/bindings on the node with directives or with the node itself.
|
|
const setAttributeBinding = (attribute, ioType) => {
|
|
const dir = directives.find((dir) => dir[ioType].hasBindingPropertyName(attribute.name));
|
|
const binding = dir !== undefined ? dir : node;
|
|
this.bindings.set(attribute, binding);
|
|
};
|
|
// Node inputs (bound attributes) and text attributes can be bound to an
|
|
// input on a directive.
|
|
node.inputs.forEach((input) => setAttributeBinding(input, 'inputs'));
|
|
node.attributes.forEach((attr) => setAttributeBinding(attr, 'inputs'));
|
|
if (node instanceof Template) {
|
|
node.templateAttrs.forEach((attr) => setAttributeBinding(attr, 'inputs'));
|
|
}
|
|
// Node outputs (bound events) can be bound to an output on a directive.
|
|
node.outputs.forEach((output) => setAttributeBinding(output, 'outputs'));
|
|
}
|
|
// Unused visitors.
|
|
visitVariable(variable) { }
|
|
visitReference(reference) { }
|
|
visitTextAttribute(attribute) { }
|
|
visitBoundAttribute(attribute) { }
|
|
visitBoundEvent(attribute) { }
|
|
visitBoundAttributeOrEvent(node) { }
|
|
visitText(text) { }
|
|
visitBoundText(text) { }
|
|
visitIcu(icu) { }
|
|
visitDeferredTrigger(trigger) { }
|
|
visitUnknownBlock(block) { }
|
|
visitLetDeclaration(decl) { }
|
|
}
|
|
/**
|
|
* Processes a template and extract metadata about expressions and symbols within.
|
|
*
|
|
* This is a companion to the `DirectiveBinder` that doesn't require knowledge of directives matched
|
|
* within the template in order to operate.
|
|
*
|
|
* Expressions are visited by the superclass `RecursiveAstVisitor`, with custom logic provided
|
|
* by overridden methods from that visitor.
|
|
*/
|
|
class TemplateBinder extends CombinedRecursiveAstVisitor {
|
|
bindings;
|
|
symbols;
|
|
usedPipes;
|
|
eagerPipes;
|
|
deferBlocks;
|
|
nestingLevel;
|
|
scope;
|
|
rootNode;
|
|
level;
|
|
visitNode = (node) => node.visit(this);
|
|
constructor(bindings, symbols, usedPipes, eagerPipes, deferBlocks, nestingLevel, scope, rootNode, level) {
|
|
super();
|
|
this.bindings = bindings;
|
|
this.symbols = symbols;
|
|
this.usedPipes = usedPipes;
|
|
this.eagerPipes = eagerPipes;
|
|
this.deferBlocks = deferBlocks;
|
|
this.nestingLevel = nestingLevel;
|
|
this.scope = scope;
|
|
this.rootNode = rootNode;
|
|
this.level = level;
|
|
}
|
|
/**
|
|
* Process a template and extract metadata about expressions and symbols within.
|
|
*
|
|
* @param nodeOrNodes the nodes of the template to process
|
|
* @param scope the `Scope` of the template being processed.
|
|
* @returns three maps which contain metadata about the template: `expressions` which interprets
|
|
* special `AST` nodes in expressions as pointing to references or variables declared within the
|
|
* template, `symbols` which maps those variables and references to the nested `Template` which
|
|
* declares them, if any, and `nestingLevel` which associates each `Template` with a integer
|
|
* nesting level (how many levels deep within the template structure the `Template` is), starting
|
|
* at 1.
|
|
*/
|
|
static applyWithScope(nodeOrNodes, scope, expressions, symbols, nestingLevel, usedPipes, eagerPipes, deferBlocks) {
|
|
const template = nodeOrNodes instanceof Template ? nodeOrNodes : null;
|
|
// The top-level template has nesting level 0.
|
|
const binder = new TemplateBinder(expressions, symbols, usedPipes, eagerPipes, deferBlocks, nestingLevel, scope, template, 0);
|
|
binder.ingest(nodeOrNodes);
|
|
}
|
|
ingest(nodeOrNodes) {
|
|
if (nodeOrNodes instanceof Template) {
|
|
// For <ng-template>s, process only variables and child nodes. Inputs, outputs, templateAttrs,
|
|
// and references were all processed in the scope of the containing template.
|
|
nodeOrNodes.variables.forEach(this.visitNode);
|
|
nodeOrNodes.children.forEach(this.visitNode);
|
|
// Set the nesting level.
|
|
this.nestingLevel.set(nodeOrNodes, this.level);
|
|
}
|
|
else if (nodeOrNodes instanceof IfBlockBranch) {
|
|
if (nodeOrNodes.expressionAlias !== null) {
|
|
this.visitNode(nodeOrNodes.expressionAlias);
|
|
}
|
|
nodeOrNodes.children.forEach(this.visitNode);
|
|
this.nestingLevel.set(nodeOrNodes, this.level);
|
|
}
|
|
else if (nodeOrNodes instanceof ForLoopBlock) {
|
|
this.visitNode(nodeOrNodes.item);
|
|
nodeOrNodes.contextVariables.forEach((v) => this.visitNode(v));
|
|
nodeOrNodes.trackBy.visit(this);
|
|
nodeOrNodes.children.forEach(this.visitNode);
|
|
this.nestingLevel.set(nodeOrNodes, this.level);
|
|
}
|
|
else if (nodeOrNodes instanceof DeferredBlock) {
|
|
if (this.scope.rootNode !== nodeOrNodes) {
|
|
throw new Error(`Assertion error: resolved incorrect scope for deferred block ${nodeOrNodes}`);
|
|
}
|
|
this.deferBlocks.push([nodeOrNodes, this.scope]);
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
this.nestingLevel.set(nodeOrNodes, this.level);
|
|
}
|
|
else if (nodeOrNodes instanceof SwitchBlockCase ||
|
|
nodeOrNodes instanceof ForLoopBlockEmpty ||
|
|
nodeOrNodes instanceof DeferredBlockError ||
|
|
nodeOrNodes instanceof DeferredBlockPlaceholder ||
|
|
nodeOrNodes instanceof DeferredBlockLoading ||
|
|
nodeOrNodes instanceof Content) {
|
|
nodeOrNodes.children.forEach((node) => node.visit(this));
|
|
this.nestingLevel.set(nodeOrNodes, this.level);
|
|
}
|
|
else if (nodeOrNodes instanceof HostElement) {
|
|
// Host elements are always at the top level.
|
|
this.nestingLevel.set(nodeOrNodes, 0);
|
|
}
|
|
else {
|
|
// Visit each node from the top-level template.
|
|
nodeOrNodes.forEach(this.visitNode);
|
|
}
|
|
}
|
|
visitTemplate(template) {
|
|
// First, visit inputs, outputs and template attributes of the template node.
|
|
template.inputs.forEach(this.visitNode);
|
|
template.outputs.forEach(this.visitNode);
|
|
template.directives.forEach(this.visitNode);
|
|
template.templateAttrs.forEach(this.visitNode);
|
|
template.references.forEach(this.visitNode);
|
|
// Next, recurse into the template.
|
|
this.ingestScopedNode(template);
|
|
}
|
|
visitVariable(variable) {
|
|
// Register the `Variable` as a symbol in the current `Template`.
|
|
if (this.rootNode !== null) {
|
|
this.symbols.set(variable, this.rootNode);
|
|
}
|
|
}
|
|
visitReference(reference) {
|
|
// Register the `Reference` as a symbol in the current `Template`.
|
|
if (this.rootNode !== null) {
|
|
this.symbols.set(reference, this.rootNode);
|
|
}
|
|
}
|
|
visitDeferredBlock(deferred) {
|
|
this.ingestScopedNode(deferred);
|
|
deferred.triggers.when?.value.visit(this);
|
|
deferred.prefetchTriggers.when?.value.visit(this);
|
|
deferred.hydrateTriggers.when?.value.visit(this);
|
|
deferred.hydrateTriggers.never?.visit(this);
|
|
deferred.placeholder && this.visitNode(deferred.placeholder);
|
|
deferred.loading && this.visitNode(deferred.loading);
|
|
deferred.error && this.visitNode(deferred.error);
|
|
}
|
|
visitDeferredBlockPlaceholder(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitDeferredBlockError(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitDeferredBlockLoading(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitSwitchBlockCase(block) {
|
|
block.expression?.visit(this);
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitForLoopBlock(block) {
|
|
block.expression.visit(this);
|
|
this.ingestScopedNode(block);
|
|
block.empty?.visit(this);
|
|
}
|
|
visitForLoopBlockEmpty(block) {
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitIfBlockBranch(block) {
|
|
block.expression?.visit(this);
|
|
this.ingestScopedNode(block);
|
|
}
|
|
visitContent(content) {
|
|
this.ingestScopedNode(content);
|
|
}
|
|
visitLetDeclaration(decl) {
|
|
super.visitLetDeclaration(decl);
|
|
if (this.rootNode !== null) {
|
|
this.symbols.set(decl, this.rootNode);
|
|
}
|
|
}
|
|
visitPipe(ast, context) {
|
|
this.usedPipes.add(ast.name);
|
|
if (!this.scope.isDeferred) {
|
|
this.eagerPipes.add(ast.name);
|
|
}
|
|
return super.visitPipe(ast, context);
|
|
}
|
|
// These five types of AST expressions can refer to expression roots, which could be variables
|
|
// or references in the current scope.
|
|
visitPropertyRead(ast, context) {
|
|
this.maybeMap(ast, ast.name);
|
|
return super.visitPropertyRead(ast, context);
|
|
}
|
|
visitSafePropertyRead(ast, context) {
|
|
this.maybeMap(ast, ast.name);
|
|
return super.visitSafePropertyRead(ast, context);
|
|
}
|
|
ingestScopedNode(node) {
|
|
const childScope = this.scope.getChildScope(node);
|
|
const binder = new TemplateBinder(this.bindings, this.symbols, this.usedPipes, this.eagerPipes, this.deferBlocks, this.nestingLevel, childScope, node, this.level + 1);
|
|
binder.ingest(node);
|
|
}
|
|
maybeMap(ast, name) {
|
|
// If the receiver of the expression isn't the `ImplicitReceiver`, this isn't the root of an
|
|
// `AST` expression that maps to a `Variable` or `Reference`.
|
|
if (!(ast.receiver instanceof ImplicitReceiver) || ast.receiver instanceof ThisReceiver) {
|
|
return;
|
|
}
|
|
// Check whether the name exists in the current scope. If so, map it. Otherwise, the name is
|
|
// probably a property on the top-level component context.
|
|
const target = this.scope.lookup(name);
|
|
if (target !== null) {
|
|
this.bindings.set(ast, target);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Metadata container for a `Target` that allows queries for specific bits of metadata.
|
|
*
|
|
* See `BoundTarget` for documentation on the individual methods.
|
|
*/
|
|
class R3BoundTarget {
|
|
target;
|
|
directives;
|
|
eagerDirectives;
|
|
missingDirectives;
|
|
bindings;
|
|
references;
|
|
exprTargets;
|
|
symbols;
|
|
nestingLevel;
|
|
scopedNodeEntities;
|
|
usedPipes;
|
|
eagerPipes;
|
|
/** Deferred blocks, ordered as they appear in the template. */
|
|
deferredBlocks;
|
|
/** Map of deferred blocks to their scope. */
|
|
deferredScopes;
|
|
constructor(target, directives, eagerDirectives, missingDirectives, bindings, references, exprTargets, symbols, nestingLevel, scopedNodeEntities, usedPipes, eagerPipes, rawDeferred) {
|
|
this.target = target;
|
|
this.directives = directives;
|
|
this.eagerDirectives = eagerDirectives;
|
|
this.missingDirectives = missingDirectives;
|
|
this.bindings = bindings;
|
|
this.references = references;
|
|
this.exprTargets = exprTargets;
|
|
this.symbols = symbols;
|
|
this.nestingLevel = nestingLevel;
|
|
this.scopedNodeEntities = scopedNodeEntities;
|
|
this.usedPipes = usedPipes;
|
|
this.eagerPipes = eagerPipes;
|
|
this.deferredBlocks = rawDeferred.map((current) => current[0]);
|
|
this.deferredScopes = new Map(rawDeferred);
|
|
}
|
|
getEntitiesInScope(node) {
|
|
return this.scopedNodeEntities.get(node) ?? new Set();
|
|
}
|
|
getDirectivesOfNode(node) {
|
|
return this.directives.get(node) || null;
|
|
}
|
|
getReferenceTarget(ref) {
|
|
return this.references.get(ref) || null;
|
|
}
|
|
getConsumerOfBinding(binding) {
|
|
return this.bindings.get(binding) || null;
|
|
}
|
|
getExpressionTarget(expr) {
|
|
return this.exprTargets.get(expr) || null;
|
|
}
|
|
getDefinitionNodeOfSymbol(symbol) {
|
|
return this.symbols.get(symbol) || null;
|
|
}
|
|
getNestingLevel(node) {
|
|
return this.nestingLevel.get(node) || 0;
|
|
}
|
|
getUsedDirectives() {
|
|
const set = new Set();
|
|
this.directives.forEach((dirs) => dirs.forEach((dir) => set.add(dir)));
|
|
return Array.from(set.values());
|
|
}
|
|
getEagerlyUsedDirectives() {
|
|
const set = new Set(this.eagerDirectives);
|
|
return Array.from(set.values());
|
|
}
|
|
getUsedPipes() {
|
|
return Array.from(this.usedPipes);
|
|
}
|
|
getEagerlyUsedPipes() {
|
|
return Array.from(this.eagerPipes);
|
|
}
|
|
getDeferBlocks() {
|
|
return this.deferredBlocks;
|
|
}
|
|
getDeferredTriggerTarget(block, trigger) {
|
|
// Only triggers that refer to DOM nodes can be resolved.
|
|
if (!(trigger instanceof InteractionDeferredTrigger) &&
|
|
!(trigger instanceof ViewportDeferredTrigger) &&
|
|
!(trigger instanceof HoverDeferredTrigger)) {
|
|
return null;
|
|
}
|
|
const name = trigger.reference;
|
|
if (name === null) {
|
|
let target = null;
|
|
if (block.placeholder !== null) {
|
|
for (const child of block.placeholder.children) {
|
|
// Skip over comment nodes. Currently by default the template parser doesn't capture
|
|
// comments, but we have a safeguard here just in case since it can be enabled.
|
|
if (child instanceof Comment$1) {
|
|
continue;
|
|
}
|
|
// We can only infer the trigger if there's one root element node. Any other
|
|
// nodes at the root make it so that we can't infer the trigger anymore.
|
|
if (target !== null) {
|
|
return null;
|
|
}
|
|
if (child instanceof Element$1) {
|
|
target = child;
|
|
}
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
const outsideRef = this.findEntityInScope(block, name);
|
|
// First try to resolve the target in the scope of the main deferred block. Note that we
|
|
// skip triggers defined inside the main block itself, because they might not exist yet.
|
|
if (outsideRef instanceof Reference && this.getDefinitionNodeOfSymbol(outsideRef) !== block) {
|
|
const target = this.getReferenceTarget(outsideRef);
|
|
if (target !== null) {
|
|
return this.referenceTargetToElement(target);
|
|
}
|
|
}
|
|
// If the trigger couldn't be found in the main block, check the
|
|
// placeholder block which is shown before the main block has loaded.
|
|
if (block.placeholder !== null) {
|
|
const refInPlaceholder = this.findEntityInScope(block.placeholder, name);
|
|
const targetInPlaceholder = refInPlaceholder instanceof Reference ? this.getReferenceTarget(refInPlaceholder) : null;
|
|
if (targetInPlaceholder !== null) {
|
|
return this.referenceTargetToElement(targetInPlaceholder);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
isDeferred(element) {
|
|
for (const block of this.deferredBlocks) {
|
|
if (!this.deferredScopes.has(block)) {
|
|
continue;
|
|
}
|
|
const stack = [this.deferredScopes.get(block)];
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
if (current.elementLikeInScope.has(element)) {
|
|
return true;
|
|
}
|
|
stack.push(...current.childScopes.values());
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
referencedDirectiveExists(name) {
|
|
return !this.missingDirectives.has(name);
|
|
}
|
|
/**
|
|
* Finds an entity with a specific name in a scope.
|
|
* @param rootNode Root node of the scope.
|
|
* @param name Name of the entity.
|
|
*/
|
|
findEntityInScope(rootNode, name) {
|
|
const entities = this.getEntitiesInScope(rootNode);
|
|
for (const entity of entities) {
|
|
if (entity.name === name) {
|
|
return entity;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/** Coerces a `ReferenceTarget` to an `Element`, if possible. */
|
|
referenceTargetToElement(target) {
|
|
if (target instanceof Element$1) {
|
|
return target;
|
|
}
|
|
if (target instanceof Template ||
|
|
target.node instanceof Component$1 ||
|
|
target.node instanceof Directive$1 ||
|
|
target.node instanceof HostElement) {
|
|
return null;
|
|
}
|
|
return this.referenceTargetToElement(target.node);
|
|
}
|
|
}
|
|
function extractScopedNodeEntities(rootScope, templateEntities) {
|
|
const entityMap = new Map();
|
|
function extractScopeEntities(scope) {
|
|
if (entityMap.has(scope.rootNode)) {
|
|
return entityMap.get(scope.rootNode);
|
|
}
|
|
const currentEntities = scope.namedEntities;
|
|
let entities;
|
|
if (scope.parentScope !== null) {
|
|
entities = new Map([...extractScopeEntities(scope.parentScope), ...currentEntities]);
|
|
}
|
|
else {
|
|
entities = new Map(currentEntities);
|
|
}
|
|
entityMap.set(scope.rootNode, entities);
|
|
return entities;
|
|
}
|
|
const scopesToProcess = [rootScope];
|
|
while (scopesToProcess.length > 0) {
|
|
const scope = scopesToProcess.pop();
|
|
for (const childScope of scope.childScopes.values()) {
|
|
scopesToProcess.push(childScope);
|
|
}
|
|
extractScopeEntities(scope);
|
|
}
|
|
for (const [template, entities] of entityMap) {
|
|
templateEntities.set(template, new Set(entities.values()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An interface for retrieving documents by URL that the compiler uses to
|
|
* load templates.
|
|
*
|
|
* This is an abstract class, rather than an interface, so that it can be used
|
|
* as injection token.
|
|
*/
|
|
class ResourceLoader {
|
|
}
|
|
|
|
class CompilerFacadeImpl {
|
|
jitEvaluator;
|
|
FactoryTarget = FactoryTarget;
|
|
ResourceLoader = ResourceLoader;
|
|
elementSchemaRegistry = new DomElementSchemaRegistry();
|
|
constructor(jitEvaluator = new JitEvaluator()) {
|
|
this.jitEvaluator = jitEvaluator;
|
|
}
|
|
compilePipe(angularCoreEnv, sourceMapUrl, facade) {
|
|
const metadata = {
|
|
name: facade.name,
|
|
type: wrapReference(facade.type),
|
|
typeArgumentCount: 0,
|
|
deps: null,
|
|
pipeName: facade.pipeName,
|
|
pure: facade.pure,
|
|
isStandalone: facade.isStandalone,
|
|
};
|
|
const res = compilePipeFromMetadata(metadata);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compilePipeDeclaration(angularCoreEnv, sourceMapUrl, declaration) {
|
|
const meta = convertDeclarePipeFacadeToMetadata(declaration);
|
|
const res = compilePipeFromMetadata(meta);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compileInjectable(angularCoreEnv, sourceMapUrl, facade) {
|
|
const { expression, statements } = compileInjectable({
|
|
name: facade.name,
|
|
type: wrapReference(facade.type),
|
|
typeArgumentCount: facade.typeArgumentCount,
|
|
providedIn: computeProvidedIn(facade.providedIn),
|
|
useClass: convertToProviderExpression(facade, 'useClass'),
|
|
useFactory: wrapExpression(facade, 'useFactory'),
|
|
useValue: convertToProviderExpression(facade, 'useValue'),
|
|
useExisting: convertToProviderExpression(facade, 'useExisting'),
|
|
deps: facade.deps?.map(convertR3DependencyMetadata),
|
|
},
|
|
/* resolveForwardRefs */ true);
|
|
return this.jitExpression(expression, angularCoreEnv, sourceMapUrl, statements);
|
|
}
|
|
compileInjectableDeclaration(angularCoreEnv, sourceMapUrl, facade) {
|
|
const { expression, statements } = compileInjectable({
|
|
name: facade.type.name,
|
|
type: wrapReference(facade.type),
|
|
typeArgumentCount: 0,
|
|
providedIn: computeProvidedIn(facade.providedIn),
|
|
useClass: convertToProviderExpression(facade, 'useClass'),
|
|
useFactory: wrapExpression(facade, 'useFactory'),
|
|
useValue: convertToProviderExpression(facade, 'useValue'),
|
|
useExisting: convertToProviderExpression(facade, 'useExisting'),
|
|
deps: facade.deps?.map(convertR3DeclareDependencyMetadata),
|
|
},
|
|
/* resolveForwardRefs */ true);
|
|
return this.jitExpression(expression, angularCoreEnv, sourceMapUrl, statements);
|
|
}
|
|
compileInjector(angularCoreEnv, sourceMapUrl, facade) {
|
|
const meta = {
|
|
name: facade.name,
|
|
type: wrapReference(facade.type),
|
|
providers: facade.providers && facade.providers.length > 0
|
|
? new WrappedNodeExpr(facade.providers)
|
|
: null,
|
|
imports: facade.imports.map((i) => new WrappedNodeExpr(i)),
|
|
};
|
|
const res = compileInjector(meta);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compileInjectorDeclaration(angularCoreEnv, sourceMapUrl, declaration) {
|
|
const meta = convertDeclareInjectorFacadeToMetadata(declaration);
|
|
const res = compileInjector(meta);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compileNgModule(angularCoreEnv, sourceMapUrl, facade) {
|
|
const meta = {
|
|
kind: R3NgModuleMetadataKind.Global,
|
|
type: wrapReference(facade.type),
|
|
bootstrap: facade.bootstrap.map(wrapReference),
|
|
declarations: facade.declarations.map(wrapReference),
|
|
publicDeclarationTypes: null, // only needed for types in AOT
|
|
imports: facade.imports.map(wrapReference),
|
|
includeImportTypes: true,
|
|
exports: facade.exports.map(wrapReference),
|
|
selectorScopeMode: R3SelectorScopeMode.Inline,
|
|
containsForwardDecls: false,
|
|
schemas: facade.schemas ? facade.schemas.map(wrapReference) : null,
|
|
id: facade.id ? new WrappedNodeExpr(facade.id) : null,
|
|
};
|
|
const res = compileNgModule(meta);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compileNgModuleDeclaration(angularCoreEnv, sourceMapUrl, declaration) {
|
|
const expression = compileNgModuleDeclarationExpression(declaration);
|
|
return this.jitExpression(expression, angularCoreEnv, sourceMapUrl, []);
|
|
}
|
|
compileDirective(angularCoreEnv, sourceMapUrl, facade) {
|
|
const meta = convertDirectiveFacadeToMetadata(facade);
|
|
return this.compileDirectiveFromMeta(angularCoreEnv, sourceMapUrl, meta);
|
|
}
|
|
compileDirectiveDeclaration(angularCoreEnv, sourceMapUrl, declaration) {
|
|
const typeSourceSpan = this.createParseSourceSpan('Directive', declaration.type.name, sourceMapUrl);
|
|
const meta = convertDeclareDirectiveFacadeToMetadata(declaration, typeSourceSpan);
|
|
return this.compileDirectiveFromMeta(angularCoreEnv, sourceMapUrl, meta);
|
|
}
|
|
compileDirectiveFromMeta(angularCoreEnv, sourceMapUrl, meta) {
|
|
const constantPool = new ConstantPool();
|
|
const bindingParser = makeBindingParser();
|
|
const res = compileDirectiveFromMetadata(meta, constantPool, bindingParser);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
|
|
}
|
|
compileComponent(angularCoreEnv, sourceMapUrl, facade) {
|
|
// Parse the template and check for errors.
|
|
const { template, interpolation, defer } = parseJitTemplate(facade.template, facade.name, sourceMapUrl, facade.preserveWhitespaces, facade.interpolation, undefined);
|
|
// Compile the component metadata, including template, into an expression.
|
|
const meta = {
|
|
...facade,
|
|
...convertDirectiveFacadeToMetadata(facade),
|
|
selector: facade.selector || this.elementSchemaRegistry.getDefaultComponentElementName(),
|
|
template,
|
|
declarations: facade.declarations.map(convertDeclarationFacadeToMetadata),
|
|
declarationListEmitMode: 0 /* DeclarationListEmitMode.Direct */,
|
|
defer,
|
|
styles: [...facade.styles, ...template.styles],
|
|
encapsulation: facade.encapsulation,
|
|
interpolation,
|
|
changeDetection: facade.changeDetection ?? null,
|
|
animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null,
|
|
viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) : null,
|
|
relativeContextFilePath: '',
|
|
i18nUseExternalIds: true,
|
|
relativeTemplatePath: null,
|
|
};
|
|
const jitExpressionSourceMap = `ng:///${facade.name}.js`;
|
|
return this.compileComponentFromMeta(angularCoreEnv, jitExpressionSourceMap, meta);
|
|
}
|
|
compileComponentDeclaration(angularCoreEnv, sourceMapUrl, declaration) {
|
|
const typeSourceSpan = this.createParseSourceSpan('Component', declaration.type.name, sourceMapUrl);
|
|
const meta = convertDeclareComponentFacadeToMetadata(declaration, typeSourceSpan, sourceMapUrl);
|
|
return this.compileComponentFromMeta(angularCoreEnv, sourceMapUrl, meta);
|
|
}
|
|
compileComponentFromMeta(angularCoreEnv, sourceMapUrl, meta) {
|
|
const constantPool = new ConstantPool();
|
|
const bindingParser = makeBindingParser(meta.interpolation);
|
|
const res = compileComponentFromMetadata(meta, constantPool, bindingParser);
|
|
return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
|
|
}
|
|
compileFactory(angularCoreEnv, sourceMapUrl, meta) {
|
|
const factoryRes = compileFactoryFunction({
|
|
name: meta.name,
|
|
type: wrapReference(meta.type),
|
|
typeArgumentCount: meta.typeArgumentCount,
|
|
deps: convertR3DependencyMetadataArray(meta.deps),
|
|
target: meta.target,
|
|
});
|
|
return this.jitExpression(factoryRes.expression, angularCoreEnv, sourceMapUrl, factoryRes.statements);
|
|
}
|
|
compileFactoryDeclaration(angularCoreEnv, sourceMapUrl, meta) {
|
|
const factoryRes = compileFactoryFunction({
|
|
name: meta.type.name,
|
|
type: wrapReference(meta.type),
|
|
typeArgumentCount: 0,
|
|
deps: Array.isArray(meta.deps)
|
|
? meta.deps.map(convertR3DeclareDependencyMetadata)
|
|
: meta.deps,
|
|
target: meta.target,
|
|
});
|
|
return this.jitExpression(factoryRes.expression, angularCoreEnv, sourceMapUrl, factoryRes.statements);
|
|
}
|
|
createParseSourceSpan(kind, typeName, sourceUrl) {
|
|
return r3JitTypeSourceSpan(kind, typeName, sourceUrl);
|
|
}
|
|
/**
|
|
* JIT compiles an expression and returns the result of executing that expression.
|
|
*
|
|
* @param def the definition which will be compiled and executed to get the value to patch
|
|
* @param context an object map of @angular/core symbol names to symbols which will be available
|
|
* in the context of the compiled expression
|
|
* @param sourceUrl a URL to use for the source map of the compiled expression
|
|
* @param preStatements a collection of statements that should be evaluated before the expression.
|
|
*/
|
|
jitExpression(def, context, sourceUrl, preStatements) {
|
|
// The ConstantPool may contain Statements which declare variables used in the final expression.
|
|
// Therefore, its statements need to precede the actual JIT operation. The final statement is a
|
|
// declaration of $def which is set to the expression being compiled.
|
|
const statements = [
|
|
...preStatements,
|
|
new DeclareVarStmt('$def', def, undefined, StmtModifier.Exported),
|
|
];
|
|
const res = this.jitEvaluator.evaluateStatements(sourceUrl, statements, new R3JitReflector(context),
|
|
/* enableSourceMaps */ true);
|
|
return res['$def'];
|
|
}
|
|
}
|
|
function convertToR3QueryMetadata(facade) {
|
|
return {
|
|
...facade,
|
|
isSignal: facade.isSignal,
|
|
predicate: convertQueryPredicate(facade.predicate),
|
|
read: facade.read ? new WrappedNodeExpr(facade.read) : null,
|
|
static: facade.static,
|
|
emitDistinctChangesOnly: facade.emitDistinctChangesOnly,
|
|
};
|
|
}
|
|
function convertQueryDeclarationToMetadata(declaration) {
|
|
return {
|
|
propertyName: declaration.propertyName,
|
|
first: declaration.first ?? false,
|
|
predicate: convertQueryPredicate(declaration.predicate),
|
|
descendants: declaration.descendants ?? false,
|
|
read: declaration.read ? new WrappedNodeExpr(declaration.read) : null,
|
|
static: declaration.static ?? false,
|
|
emitDistinctChangesOnly: declaration.emitDistinctChangesOnly ?? true,
|
|
isSignal: !!declaration.isSignal,
|
|
};
|
|
}
|
|
function convertQueryPredicate(predicate) {
|
|
return Array.isArray(predicate)
|
|
? // The predicate is an array of strings so pass it through.
|
|
predicate
|
|
: // The predicate is a type - assume that we will need to unwrap any `forwardRef()` calls.
|
|
createMayBeForwardRefExpression(new WrappedNodeExpr(predicate), 1 /* ForwardRefHandling.Wrapped */);
|
|
}
|
|
function convertDirectiveFacadeToMetadata(facade) {
|
|
const inputsFromMetadata = parseInputsArray(facade.inputs || []);
|
|
const outputsFromMetadata = parseMappingStringArray(facade.outputs || []);
|
|
const propMetadata = facade.propMetadata;
|
|
const inputsFromType = {};
|
|
const outputsFromType = {};
|
|
for (const field in propMetadata) {
|
|
if (propMetadata.hasOwnProperty(field)) {
|
|
propMetadata[field].forEach((ann) => {
|
|
if (isInput(ann)) {
|
|
inputsFromType[field] = {
|
|
bindingPropertyName: ann.alias || field,
|
|
classPropertyName: field,
|
|
required: ann.required || false,
|
|
// For JIT, decorators are used to declare signal inputs. That is because of
|
|
// a technical limitation where it's not possible to statically reflect class
|
|
// members of a directive/component at runtime before instantiating the class.
|
|
isSignal: !!ann.isSignal,
|
|
transformFunction: ann.transform != null ? new WrappedNodeExpr(ann.transform) : null,
|
|
};
|
|
}
|
|
else if (isOutput(ann)) {
|
|
outputsFromType[field] = ann.alias || field;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
const hostDirectives = facade.hostDirectives?.length
|
|
? facade.hostDirectives.map((hostDirective) => {
|
|
return typeof hostDirective === 'function'
|
|
? {
|
|
directive: wrapReference(hostDirective),
|
|
inputs: null,
|
|
outputs: null,
|
|
isForwardReference: false,
|
|
}
|
|
: {
|
|
directive: wrapReference(hostDirective.directive),
|
|
isForwardReference: false,
|
|
inputs: hostDirective.inputs ? parseMappingStringArray(hostDirective.inputs) : null,
|
|
outputs: hostDirective.outputs
|
|
? parseMappingStringArray(hostDirective.outputs)
|
|
: null,
|
|
};
|
|
})
|
|
: null;
|
|
return {
|
|
...facade,
|
|
typeArgumentCount: 0,
|
|
typeSourceSpan: facade.typeSourceSpan,
|
|
type: wrapReference(facade.type),
|
|
deps: null,
|
|
host: {
|
|
...extractHostBindings(facade.propMetadata, facade.typeSourceSpan, facade.host),
|
|
},
|
|
inputs: { ...inputsFromMetadata, ...inputsFromType },
|
|
outputs: { ...outputsFromMetadata, ...outputsFromType },
|
|
queries: facade.queries.map(convertToR3QueryMetadata),
|
|
providers: facade.providers != null ? new WrappedNodeExpr(facade.providers) : null,
|
|
viewQueries: facade.viewQueries.map(convertToR3QueryMetadata),
|
|
fullInheritance: false,
|
|
hostDirectives,
|
|
};
|
|
}
|
|
function convertDeclareDirectiveFacadeToMetadata(declaration, typeSourceSpan) {
|
|
const hostDirectives = declaration.hostDirectives?.length
|
|
? declaration.hostDirectives.map((dir) => ({
|
|
directive: wrapReference(dir.directive),
|
|
isForwardReference: false,
|
|
inputs: dir.inputs ? getHostDirectiveBindingMapping(dir.inputs) : null,
|
|
outputs: dir.outputs ? getHostDirectiveBindingMapping(dir.outputs) : null,
|
|
}))
|
|
: null;
|
|
return {
|
|
name: declaration.type.name,
|
|
type: wrapReference(declaration.type),
|
|
typeSourceSpan,
|
|
selector: declaration.selector ?? null,
|
|
inputs: declaration.inputs ? inputsPartialMetadataToInputMetadata(declaration.inputs) : {},
|
|
outputs: declaration.outputs ?? {},
|
|
host: convertHostDeclarationToMetadata(declaration.host),
|
|
queries: (declaration.queries ?? []).map(convertQueryDeclarationToMetadata),
|
|
viewQueries: (declaration.viewQueries ?? []).map(convertQueryDeclarationToMetadata),
|
|
providers: declaration.providers !== undefined ? new WrappedNodeExpr(declaration.providers) : null,
|
|
exportAs: declaration.exportAs ?? null,
|
|
usesInheritance: declaration.usesInheritance ?? false,
|
|
lifecycle: { usesOnChanges: declaration.usesOnChanges ?? false },
|
|
deps: null,
|
|
typeArgumentCount: 0,
|
|
fullInheritance: false,
|
|
isStandalone: declaration.isStandalone ?? getJitStandaloneDefaultForVersion(declaration.version),
|
|
isSignal: declaration.isSignal ?? false,
|
|
hostDirectives,
|
|
};
|
|
}
|
|
function convertHostDeclarationToMetadata(host = {}) {
|
|
return {
|
|
attributes: convertOpaqueValuesToExpressions(host.attributes ?? {}),
|
|
listeners: host.listeners ?? {},
|
|
properties: host.properties ?? {},
|
|
specialAttributes: {
|
|
classAttr: host.classAttribute,
|
|
styleAttr: host.styleAttribute,
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Parses a host directive mapping where each odd array key is the name of an input/output
|
|
* and each even key is its public name, e.g. `['one', 'oneAlias', 'two', 'two']`.
|
|
*/
|
|
function getHostDirectiveBindingMapping(array) {
|
|
let result = null;
|
|
for (let i = 1; i < array.length; i += 2) {
|
|
result = result || {};
|
|
result[array[i - 1]] = array[i];
|
|
}
|
|
return result;
|
|
}
|
|
function convertOpaqueValuesToExpressions(obj) {
|
|
const result = {};
|
|
for (const key of Object.keys(obj)) {
|
|
result[key] = new WrappedNodeExpr(obj[key]);
|
|
}
|
|
return result;
|
|
}
|
|
function convertDeclareComponentFacadeToMetadata(decl, typeSourceSpan, sourceMapUrl) {
|
|
const { template, interpolation, defer } = parseJitTemplate(decl.template, decl.type.name, sourceMapUrl, decl.preserveWhitespaces ?? false, decl.interpolation, decl.deferBlockDependencies);
|
|
const declarations = [];
|
|
if (decl.dependencies) {
|
|
for (const innerDep of decl.dependencies) {
|
|
switch (innerDep.kind) {
|
|
case 'directive':
|
|
case 'component':
|
|
declarations.push(convertDirectiveDeclarationToMetadata(innerDep));
|
|
break;
|
|
case 'pipe':
|
|
declarations.push(convertPipeDeclarationToMetadata(innerDep));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (decl.components || decl.directives || decl.pipes) {
|
|
// Existing declarations on NPM may not be using the new `dependencies` merged field, and may
|
|
// have separate fields for dependencies instead. Unify them for JIT compilation.
|
|
decl.components &&
|
|
declarations.push(...decl.components.map((dir) => convertDirectiveDeclarationToMetadata(dir, /* isComponent */ true)));
|
|
decl.directives &&
|
|
declarations.push(...decl.directives.map((dir) => convertDirectiveDeclarationToMetadata(dir)));
|
|
decl.pipes && declarations.push(...convertPipeMapToMetadata(decl.pipes));
|
|
}
|
|
const hasDirectiveDependencies = declarations.some(({ kind }) => kind === R3TemplateDependencyKind.Directive || kind === R3TemplateDependencyKind.NgModule);
|
|
return {
|
|
...convertDeclareDirectiveFacadeToMetadata(decl, typeSourceSpan),
|
|
template,
|
|
styles: decl.styles ?? [],
|
|
declarations,
|
|
viewProviders: decl.viewProviders !== undefined ? new WrappedNodeExpr(decl.viewProviders) : null,
|
|
animations: decl.animations !== undefined ? new WrappedNodeExpr(decl.animations) : null,
|
|
defer,
|
|
changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.Default,
|
|
encapsulation: decl.encapsulation ?? ViewEncapsulation$1.Emulated,
|
|
interpolation,
|
|
declarationListEmitMode: 2 /* DeclarationListEmitMode.ClosureResolved */,
|
|
relativeContextFilePath: '',
|
|
i18nUseExternalIds: true,
|
|
relativeTemplatePath: null,
|
|
hasDirectiveDependencies,
|
|
};
|
|
}
|
|
function convertDeclarationFacadeToMetadata(declaration) {
|
|
return {
|
|
...declaration,
|
|
type: new WrappedNodeExpr(declaration.type),
|
|
};
|
|
}
|
|
function convertDirectiveDeclarationToMetadata(declaration, isComponent = null) {
|
|
return {
|
|
kind: R3TemplateDependencyKind.Directive,
|
|
isComponent: isComponent || declaration.kind === 'component',
|
|
selector: declaration.selector,
|
|
type: new WrappedNodeExpr(declaration.type),
|
|
inputs: declaration.inputs ?? [],
|
|
outputs: declaration.outputs ?? [],
|
|
exportAs: declaration.exportAs ?? null,
|
|
};
|
|
}
|
|
function convertPipeMapToMetadata(pipes) {
|
|
if (!pipes) {
|
|
return [];
|
|
}
|
|
return Object.keys(pipes).map((name) => {
|
|
return {
|
|
kind: R3TemplateDependencyKind.Pipe,
|
|
name,
|
|
type: new WrappedNodeExpr(pipes[name]),
|
|
};
|
|
});
|
|
}
|
|
function convertPipeDeclarationToMetadata(pipe) {
|
|
return {
|
|
kind: R3TemplateDependencyKind.Pipe,
|
|
name: pipe.name,
|
|
type: new WrappedNodeExpr(pipe.type),
|
|
};
|
|
}
|
|
function parseJitTemplate(template, typeName, sourceMapUrl, preserveWhitespaces, interpolation, deferBlockDependencies) {
|
|
const interpolationConfig = interpolation
|
|
? InterpolationConfig.fromArray(interpolation)
|
|
: DEFAULT_INTERPOLATION_CONFIG;
|
|
// Parse the template and check for errors.
|
|
const parsed = parseTemplate(template, sourceMapUrl, {
|
|
preserveWhitespaces,
|
|
interpolationConfig,
|
|
});
|
|
if (parsed.errors !== null) {
|
|
const errors = parsed.errors.map((err) => err.toString()).join(', ');
|
|
throw new Error(`Errors during JIT compilation of template for ${typeName}: ${errors}`);
|
|
}
|
|
const binder = new R3TargetBinder(null);
|
|
const boundTarget = binder.bind({ template: parsed.nodes });
|
|
return {
|
|
template: parsed,
|
|
interpolation: interpolationConfig,
|
|
defer: createR3ComponentDeferMetadata(boundTarget, deferBlockDependencies),
|
|
};
|
|
}
|
|
/**
|
|
* Convert the expression, if present to an `R3ProviderExpression`.
|
|
*
|
|
* In JIT mode we do not want the compiler to wrap the expression in a `forwardRef()` call because,
|
|
* if it is referencing a type that has not yet been defined, it will have already been wrapped in
|
|
* a `forwardRef()` - either by the application developer or during partial-compilation. Thus we can
|
|
* use `ForwardRefHandling.None`.
|
|
*/
|
|
function convertToProviderExpression(obj, property) {
|
|
if (obj.hasOwnProperty(property)) {
|
|
return createMayBeForwardRefExpression(new WrappedNodeExpr(obj[property]), 0 /* ForwardRefHandling.None */);
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
function wrapExpression(obj, property) {
|
|
if (obj.hasOwnProperty(property)) {
|
|
return new WrappedNodeExpr(obj[property]);
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
function computeProvidedIn(providedIn) {
|
|
const expression = typeof providedIn === 'function'
|
|
? new WrappedNodeExpr(providedIn)
|
|
: new LiteralExpr(providedIn ?? null);
|
|
// See `convertToProviderExpression()` for why this uses `ForwardRefHandling.None`.
|
|
return createMayBeForwardRefExpression(expression, 0 /* ForwardRefHandling.None */);
|
|
}
|
|
function convertR3DependencyMetadataArray(facades) {
|
|
return facades == null ? null : facades.map(convertR3DependencyMetadata);
|
|
}
|
|
function convertR3DependencyMetadata(facade) {
|
|
const isAttributeDep = facade.attribute != null; // both `null` and `undefined`
|
|
const rawToken = facade.token === null ? null : new WrappedNodeExpr(facade.token);
|
|
// In JIT mode, if the dep is an `@Attribute()` then we use the attribute name given in
|
|
// `attribute` rather than the `token`.
|
|
const token = isAttributeDep ? new WrappedNodeExpr(facade.attribute) : rawToken;
|
|
return createR3DependencyMetadata(token, isAttributeDep, facade.host, facade.optional, facade.self, facade.skipSelf);
|
|
}
|
|
function convertR3DeclareDependencyMetadata(facade) {
|
|
const isAttributeDep = facade.attribute ?? false;
|
|
const token = facade.token === null ? null : new WrappedNodeExpr(facade.token);
|
|
return createR3DependencyMetadata(token, isAttributeDep, facade.host ?? false, facade.optional ?? false, facade.self ?? false, facade.skipSelf ?? false);
|
|
}
|
|
function createR3DependencyMetadata(token, isAttributeDep, host, optional, self, skipSelf) {
|
|
// If the dep is an `@Attribute()` the `attributeNameType` ought to be the `unknown` type.
|
|
// But types are not available at runtime so we just use a literal `"<unknown>"` string as a dummy
|
|
// marker.
|
|
const attributeNameType = isAttributeDep ? literal('unknown') : null;
|
|
return { token, attributeNameType, host, optional, self, skipSelf };
|
|
}
|
|
function createR3ComponentDeferMetadata(boundTarget, deferBlockDependencies) {
|
|
const deferredBlocks = boundTarget.getDeferBlocks();
|
|
const blocks = new Map();
|
|
for (let i = 0; i < deferredBlocks.length; i++) {
|
|
const dependencyFn = deferBlockDependencies?.[i];
|
|
blocks.set(deferredBlocks[i], dependencyFn ? new WrappedNodeExpr(dependencyFn) : null);
|
|
}
|
|
return { mode: 0 /* DeferBlockDepsEmitMode.PerBlock */, blocks };
|
|
}
|
|
function extractHostBindings(propMetadata, sourceSpan, host) {
|
|
// First parse the declarations from the metadata.
|
|
const bindings = parseHostBindings(host || {});
|
|
// After that check host bindings for errors
|
|
const errors = verifyHostBindings(bindings, sourceSpan);
|
|
if (errors.length) {
|
|
throw new Error(errors.map((error) => error.msg).join('\n'));
|
|
}
|
|
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
|
|
for (const field in propMetadata) {
|
|
if (propMetadata.hasOwnProperty(field)) {
|
|
propMetadata[field].forEach((ann) => {
|
|
if (isHostBinding(ann)) {
|
|
// Since this is a decorator, we know that the value is a class member. Always access it
|
|
// through `this` so that further down the line it can't be confused for a literal value
|
|
// (e.g. if there's a property called `true`).
|
|
bindings.properties[ann.hostPropertyName || field] = getSafePropertyAccessString('this', field);
|
|
}
|
|
else if (isHostListener(ann)) {
|
|
bindings.listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return bindings;
|
|
}
|
|
function isHostBinding(value) {
|
|
return value.ngMetadataName === 'HostBinding';
|
|
}
|
|
function isHostListener(value) {
|
|
return value.ngMetadataName === 'HostListener';
|
|
}
|
|
function isInput(value) {
|
|
return value.ngMetadataName === 'Input';
|
|
}
|
|
function isOutput(value) {
|
|
return value.ngMetadataName === 'Output';
|
|
}
|
|
function inputsPartialMetadataToInputMetadata(inputs) {
|
|
return Object.keys(inputs).reduce((result, minifiedClassName) => {
|
|
const value = inputs[minifiedClassName];
|
|
// Handle legacy partial input output.
|
|
if (typeof value === 'string' || Array.isArray(value)) {
|
|
result[minifiedClassName] = parseLegacyInputPartialOutput(value);
|
|
}
|
|
else {
|
|
result[minifiedClassName] = {
|
|
bindingPropertyName: value.publicName,
|
|
classPropertyName: minifiedClassName,
|
|
transformFunction: value.transformFunction !== null ? new WrappedNodeExpr(value.transformFunction) : null,
|
|
required: value.isRequired,
|
|
isSignal: value.isSignal,
|
|
};
|
|
}
|
|
return result;
|
|
}, {});
|
|
}
|
|
/**
|
|
* Parses the legacy input partial output. For more details see `partial/directive.ts`.
|
|
* TODO(legacy-partial-output-inputs): Remove in v18.
|
|
*/
|
|
function parseLegacyInputPartialOutput(value) {
|
|
if (typeof value === 'string') {
|
|
return {
|
|
bindingPropertyName: value,
|
|
classPropertyName: value,
|
|
transformFunction: null,
|
|
required: false,
|
|
// legacy partial output does not capture signal inputs.
|
|
isSignal: false,
|
|
};
|
|
}
|
|
return {
|
|
bindingPropertyName: value[0],
|
|
classPropertyName: value[1],
|
|
transformFunction: value[2] ? new WrappedNodeExpr(value[2]) : null,
|
|
required: false,
|
|
// legacy partial output does not capture signal inputs.
|
|
isSignal: false,
|
|
};
|
|
}
|
|
function parseInputsArray(values) {
|
|
return values.reduce((results, value) => {
|
|
if (typeof value === 'string') {
|
|
const [bindingPropertyName, classPropertyName] = parseMappingString(value);
|
|
results[classPropertyName] = {
|
|
bindingPropertyName,
|
|
classPropertyName,
|
|
required: false,
|
|
// Signal inputs not supported for the inputs array.
|
|
isSignal: false,
|
|
transformFunction: null,
|
|
};
|
|
}
|
|
else {
|
|
results[value.name] = {
|
|
bindingPropertyName: value.alias || value.name,
|
|
classPropertyName: value.name,
|
|
required: value.required || false,
|
|
// Signal inputs not supported for the inputs array.
|
|
isSignal: false,
|
|
transformFunction: value.transform != null ? new WrappedNodeExpr(value.transform) : null,
|
|
};
|
|
}
|
|
return results;
|
|
}, {});
|
|
}
|
|
function parseMappingStringArray(values) {
|
|
return values.reduce((results, value) => {
|
|
const [alias, fieldName] = parseMappingString(value);
|
|
results[fieldName] = alias;
|
|
return results;
|
|
}, {});
|
|
}
|
|
function parseMappingString(value) {
|
|
// Either the value is 'field' or 'field: property'. In the first case, `property` will
|
|
// be undefined, in which case the field name should also be used as the property name.
|
|
const [fieldName, bindingPropertyName] = value.split(':', 2).map((str) => str.trim());
|
|
return [bindingPropertyName ?? fieldName, fieldName];
|
|
}
|
|
function convertDeclarePipeFacadeToMetadata(declaration) {
|
|
return {
|
|
name: declaration.type.name,
|
|
type: wrapReference(declaration.type),
|
|
typeArgumentCount: 0,
|
|
pipeName: declaration.name,
|
|
deps: null,
|
|
pure: declaration.pure ?? true,
|
|
isStandalone: declaration.isStandalone ?? getJitStandaloneDefaultForVersion(declaration.version),
|
|
};
|
|
}
|
|
function convertDeclareInjectorFacadeToMetadata(declaration) {
|
|
return {
|
|
name: declaration.type.name,
|
|
type: wrapReference(declaration.type),
|
|
providers: declaration.providers !== undefined && declaration.providers.length > 0
|
|
? new WrappedNodeExpr(declaration.providers)
|
|
: null,
|
|
imports: declaration.imports !== undefined
|
|
? declaration.imports.map((i) => new WrappedNodeExpr(i))
|
|
: [],
|
|
};
|
|
}
|
|
function publishFacade(global) {
|
|
const ng = global.ng || (global.ng = {});
|
|
ng.ɵcompilerFacade = new CompilerFacadeImpl();
|
|
}
|
|
|
|
class CompilerConfig {
|
|
defaultEncapsulation;
|
|
preserveWhitespaces;
|
|
strictInjectionParameters;
|
|
constructor({ defaultEncapsulation = ViewEncapsulation$1.Emulated, preserveWhitespaces, strictInjectionParameters, } = {}) {
|
|
this.defaultEncapsulation = defaultEncapsulation;
|
|
this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces));
|
|
this.strictInjectionParameters = strictInjectionParameters === true;
|
|
}
|
|
}
|
|
function preserveWhitespacesDefault(preserveWhitespacesOption, defaultSetting = false) {
|
|
return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption;
|
|
}
|
|
|
|
const _I18N_ATTR = 'i18n';
|
|
const _I18N_ATTR_PREFIX = 'i18n-';
|
|
const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/;
|
|
const MEANING_SEPARATOR = '|';
|
|
const ID_SEPARATOR = '@@';
|
|
let i18nCommentsWarned = false;
|
|
/**
|
|
* Extract translatable messages from an html AST
|
|
*/
|
|
function extractMessages(nodes, interpolationConfig, implicitTags, implicitAttrs, preserveSignificantWhitespace) {
|
|
const visitor = new _Visitor(implicitTags, implicitAttrs, preserveSignificantWhitespace);
|
|
return visitor.extract(nodes, interpolationConfig);
|
|
}
|
|
function mergeTranslations(nodes, translations, interpolationConfig, implicitTags, implicitAttrs) {
|
|
const visitor = new _Visitor(implicitTags, implicitAttrs);
|
|
return visitor.merge(nodes, translations, interpolationConfig);
|
|
}
|
|
class ExtractionResult {
|
|
messages;
|
|
errors;
|
|
constructor(messages, errors) {
|
|
this.messages = messages;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
var _VisitorMode;
|
|
(function (_VisitorMode) {
|
|
_VisitorMode[_VisitorMode["Extract"] = 0] = "Extract";
|
|
_VisitorMode[_VisitorMode["Merge"] = 1] = "Merge";
|
|
})(_VisitorMode || (_VisitorMode = {}));
|
|
/**
|
|
* This Visitor is used:
|
|
* 1. to extract all the translatable strings from an html AST (see `extract()`),
|
|
* 2. to replace the translatable strings with the actual translations (see `merge()`)
|
|
*
|
|
* @internal
|
|
*/
|
|
class _Visitor {
|
|
_implicitTags;
|
|
_implicitAttrs;
|
|
_preserveSignificantWhitespace;
|
|
// Using non-null assertions because all variables are (re)set in init()
|
|
_depth;
|
|
// <el i18n>...</el>
|
|
_inI18nNode;
|
|
_inImplicitNode;
|
|
// <!--i18n-->...<!--/i18n-->
|
|
_inI18nBlock;
|
|
_blockMeaningAndDesc;
|
|
_blockChildren;
|
|
_blockStartDepth;
|
|
// {<icu message>}
|
|
_inIcu;
|
|
// set to void 0 when not in a section
|
|
_msgCountAtSectionStart;
|
|
_errors;
|
|
_mode;
|
|
// _VisitorMode.Extract only
|
|
_messages;
|
|
// _VisitorMode.Merge only
|
|
_translations;
|
|
_createI18nMessage;
|
|
constructor(_implicitTags, _implicitAttrs, _preserveSignificantWhitespace = true) {
|
|
this._implicitTags = _implicitTags;
|
|
this._implicitAttrs = _implicitAttrs;
|
|
this._preserveSignificantWhitespace = _preserveSignificantWhitespace;
|
|
}
|
|
/**
|
|
* Extracts the messages from the tree
|
|
*/
|
|
extract(nodes, interpolationConfig) {
|
|
this._init(_VisitorMode.Extract, interpolationConfig);
|
|
nodes.forEach((node) => node.visit(this, null));
|
|
if (this._inI18nBlock) {
|
|
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
|
|
}
|
|
return new ExtractionResult(this._messages, this._errors);
|
|
}
|
|
/**
|
|
* Returns a tree where all translatable nodes are translated
|
|
*/
|
|
merge(nodes, translations, interpolationConfig) {
|
|
this._init(_VisitorMode.Merge, interpolationConfig);
|
|
this._translations = translations;
|
|
// Construct a single fake root element
|
|
const wrapper = new Element('wrapper', [], [], nodes, false, undefined, undefined, undefined, false);
|
|
const translatedNode = wrapper.visit(this, null);
|
|
if (this._inI18nBlock) {
|
|
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
|
|
}
|
|
return new ParseTreeResult(translatedNode.children, this._errors);
|
|
}
|
|
visitExpansionCase(icuCase, context) {
|
|
// Parse cases for translatable html attributes
|
|
const expression = visitAll(this, icuCase.expression, context);
|
|
if (this._mode === _VisitorMode.Merge) {
|
|
return new ExpansionCase(icuCase.value, expression, icuCase.sourceSpan, icuCase.valueSourceSpan, icuCase.expSourceSpan);
|
|
}
|
|
}
|
|
visitExpansion(icu, context) {
|
|
this._mayBeAddBlockChildren(icu);
|
|
const wasInIcu = this._inIcu;
|
|
if (!this._inIcu) {
|
|
// nested ICU messages should not be extracted but top-level translated as a whole
|
|
if (this._isInTranslatableSection) {
|
|
this._addMessage([icu]);
|
|
}
|
|
this._inIcu = true;
|
|
}
|
|
const cases = visitAll(this, icu.cases, context);
|
|
if (this._mode === _VisitorMode.Merge) {
|
|
icu = new Expansion(icu.switchValue, icu.type, cases, icu.sourceSpan, icu.switchValueSourceSpan);
|
|
}
|
|
this._inIcu = wasInIcu;
|
|
return icu;
|
|
}
|
|
visitComment(comment, context) {
|
|
const isOpening = _isOpeningComment(comment);
|
|
if (isOpening && this._isInTranslatableSection) {
|
|
this._reportError(comment, 'Could not start a block inside a translatable section');
|
|
return;
|
|
}
|
|
const isClosing = _isClosingComment(comment);
|
|
if (isClosing && !this._inI18nBlock) {
|
|
this._reportError(comment, 'Trying to close an unopened block');
|
|
return;
|
|
}
|
|
if (!this._inI18nNode && !this._inIcu) {
|
|
if (!this._inI18nBlock) {
|
|
if (isOpening) {
|
|
// deprecated from v5 you should use <ng-container i18n> instead of i18n comments
|
|
if (!i18nCommentsWarned && console && console.warn) {
|
|
i18nCommentsWarned = true;
|
|
const details = comment.sourceSpan.details ? `, ${comment.sourceSpan.details}` : '';
|
|
// TODO(ocombe): use a log service once there is a public one available
|
|
console.warn(`I18n comments are deprecated, use an <ng-container> element instead (${comment.sourceSpan.start}${details})`);
|
|
}
|
|
this._inI18nBlock = true;
|
|
this._blockStartDepth = this._depth;
|
|
this._blockChildren = [];
|
|
this._blockMeaningAndDesc = comment
|
|
.value.replace(_I18N_COMMENT_PREFIX_REGEXP, '')
|
|
.trim();
|
|
this._openTranslatableSection(comment);
|
|
}
|
|
}
|
|
else {
|
|
if (isClosing) {
|
|
if (this._depth == this._blockStartDepth) {
|
|
this._closeTranslatableSection(comment, this._blockChildren);
|
|
this._inI18nBlock = false;
|
|
const message = this._addMessage(this._blockChildren, this._blockMeaningAndDesc);
|
|
// merge attributes in sections
|
|
const nodes = this._translateMessage(comment, message);
|
|
return visitAll(this, nodes);
|
|
}
|
|
else {
|
|
this._reportError(comment, 'I18N blocks should not cross element boundaries');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
visitText(text, context) {
|
|
if (this._isInTranslatableSection) {
|
|
this._mayBeAddBlockChildren(text);
|
|
}
|
|
return text;
|
|
}
|
|
visitElement(el, context) {
|
|
return this._visitElementLike(el, context);
|
|
}
|
|
visitAttribute(attribute, context) {
|
|
throw new Error('unreachable code');
|
|
}
|
|
visitBlock(block, context) {
|
|
visitAll(this, block.children, context);
|
|
}
|
|
visitBlockParameter(parameter, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
return this._visitElementLike(component, context);
|
|
}
|
|
visitDirective(directive, context) {
|
|
throw new Error('unreachable code');
|
|
}
|
|
_init(mode, interpolationConfig) {
|
|
this._mode = mode;
|
|
this._inI18nBlock = false;
|
|
this._inI18nNode = false;
|
|
this._depth = 0;
|
|
this._inIcu = false;
|
|
this._msgCountAtSectionStart = undefined;
|
|
this._errors = [];
|
|
this._messages = [];
|
|
this._inImplicitNode = false;
|
|
this._createI18nMessage = createI18nMessageFactory(interpolationConfig, DEFAULT_CONTAINER_BLOCKS,
|
|
// When dropping significant whitespace we need to retain whitespace tokens or
|
|
// else we won't be able to reuse source spans because empty tokens would be
|
|
// removed and cause a mismatch.
|
|
/* retainEmptyTokens */ !this._preserveSignificantWhitespace,
|
|
/* preserveExpressionWhitespace */ this._preserveSignificantWhitespace);
|
|
}
|
|
_visitElementLike(node, context) {
|
|
this._mayBeAddBlockChildren(node);
|
|
this._depth++;
|
|
const wasInI18nNode = this._inI18nNode;
|
|
const wasInImplicitNode = this._inImplicitNode;
|
|
let childNodes = [];
|
|
let translatedChildNodes = undefined;
|
|
// Extract:
|
|
// - top level nodes with the (implicit) "i18n" attribute if not already in a section
|
|
// - ICU messages
|
|
const nodeName = node instanceof Component ? node.tagName : node.name;
|
|
const i18nAttr = _getI18nAttr(node);
|
|
const i18nMeta = i18nAttr ? i18nAttr.value : '';
|
|
const isImplicit = this._implicitTags.some((tag) => nodeName === tag) &&
|
|
!this._inIcu &&
|
|
!this._isInTranslatableSection;
|
|
const isTopLevelImplicit = !wasInImplicitNode && isImplicit;
|
|
this._inImplicitNode = wasInImplicitNode || isImplicit;
|
|
if (!this._isInTranslatableSection && !this._inIcu) {
|
|
if (i18nAttr || isTopLevelImplicit) {
|
|
this._inI18nNode = true;
|
|
const message = this._addMessage(node.children, i18nMeta);
|
|
translatedChildNodes = this._translateMessage(node, message);
|
|
}
|
|
if (this._mode == _VisitorMode.Extract) {
|
|
const isTranslatable = i18nAttr || isTopLevelImplicit;
|
|
if (isTranslatable)
|
|
this._openTranslatableSection(node);
|
|
visitAll(this, node.children);
|
|
if (isTranslatable)
|
|
this._closeTranslatableSection(node, node.children);
|
|
}
|
|
}
|
|
else {
|
|
if (i18nAttr || isTopLevelImplicit) {
|
|
this._reportError(node, 'Could not mark an element as translatable inside a translatable section');
|
|
}
|
|
if (this._mode == _VisitorMode.Extract) {
|
|
// Descend into child nodes for extraction
|
|
visitAll(this, node.children);
|
|
}
|
|
}
|
|
if (this._mode === _VisitorMode.Merge) {
|
|
const visitNodes = translatedChildNodes || node.children;
|
|
visitNodes.forEach((child) => {
|
|
const visited = child.visit(this, context);
|
|
if (visited && !this._isInTranslatableSection) {
|
|
// Do not add the children from translatable sections (= i18n blocks here)
|
|
// They will be added later in this loop when the block closes (i.e. on `<!-- /i18n -->`)
|
|
childNodes = childNodes.concat(visited);
|
|
}
|
|
});
|
|
}
|
|
this._visitAttributesOf(node);
|
|
this._depth--;
|
|
this._inI18nNode = wasInI18nNode;
|
|
this._inImplicitNode = wasInImplicitNode;
|
|
if (this._mode === _VisitorMode.Merge) {
|
|
if (node instanceof Element) {
|
|
return new Element(node.name, this._translateAttributes(node), this._translateDirectives(node), childNodes, node.isSelfClosing, node.sourceSpan, node.startSourceSpan, node.endSourceSpan, node.isVoid);
|
|
}
|
|
else {
|
|
return new Component(node.componentName, node.tagName, node.fullName, this._translateAttributes(node), this._translateDirectives(node), childNodes, node.isSelfClosing, node.sourceSpan, node.startSourceSpan, node.endSourceSpan);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
// looks for translatable attributes
|
|
_visitAttributesOf(el) {
|
|
const explicitAttrNameToValue = {};
|
|
const implicitAttrNames = this._implicitAttrs[el instanceof Component ? el.tagName || '' : el.name] || [];
|
|
el.attrs
|
|
.filter((attr) => attr instanceof Attribute && attr.name.startsWith(_I18N_ATTR_PREFIX))
|
|
.forEach((attr) => {
|
|
explicitAttrNameToValue[attr.name.slice(_I18N_ATTR_PREFIX.length)] = attr.value;
|
|
});
|
|
el.attrs.forEach((attr) => {
|
|
if (attr.name in explicitAttrNameToValue) {
|
|
this._addMessage([attr], explicitAttrNameToValue[attr.name]);
|
|
}
|
|
else if (implicitAttrNames.some((name) => attr.name === name)) {
|
|
this._addMessage([attr]);
|
|
}
|
|
});
|
|
}
|
|
// add a translatable message
|
|
_addMessage(ast, msgMeta) {
|
|
if (ast.length == 0 ||
|
|
this._isEmptyAttributeValue(ast) ||
|
|
this._isPlaceholderOnlyAttributeValue(ast) ||
|
|
this._isPlaceholderOnlyMessage(ast)) {
|
|
// Do not create empty messages
|
|
return null;
|
|
}
|
|
const { meaning, description, id } = _parseMessageMeta(msgMeta);
|
|
const message = this._createI18nMessage(ast, meaning, description, id);
|
|
this._messages.push(message);
|
|
return message;
|
|
}
|
|
// Check for cases like `<div i18n-title title="">`.
|
|
_isEmptyAttributeValue(ast) {
|
|
if (!isAttrNode(ast))
|
|
return false;
|
|
const node = ast[0];
|
|
return node.value.trim() === '';
|
|
}
|
|
// Check for cases like `<div i18n-title title="{{ name }}">`.
|
|
_isPlaceholderOnlyAttributeValue(ast) {
|
|
if (!isAttrNode(ast))
|
|
return false;
|
|
const tokens = ast[0].valueTokens ?? [];
|
|
const interpolations = tokens.filter((token) => token.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */);
|
|
const plainText = tokens
|
|
.filter((token) => token.type === 16 /* TokenType.ATTR_VALUE_TEXT */)
|
|
// `AttributeValueTextToken` always has exactly one part per its type.
|
|
.map((token) => token.parts[0].trim())
|
|
.join('');
|
|
// Check if there is a single interpolation and all text around it is empty.
|
|
return interpolations.length === 1 && plainText === '';
|
|
}
|
|
// Check for cases like `<div i18n>{{ name }}</div>`.
|
|
_isPlaceholderOnlyMessage(ast) {
|
|
if (!isTextNode(ast))
|
|
return false;
|
|
const tokens = ast[0].tokens;
|
|
const interpolations = tokens.filter((token) => token.type === 8 /* TokenType.INTERPOLATION */);
|
|
const plainText = tokens
|
|
.filter((token) => token.type === 5 /* TokenType.TEXT */)
|
|
// `TextToken` always has exactly one part per its type.
|
|
.map((token) => token.parts[0].trim())
|
|
.join('');
|
|
// Check if there is a single interpolation and all text around it is empty.
|
|
return interpolations.length === 1 && plainText === '';
|
|
}
|
|
// Translates the given message given the `TranslationBundle`
|
|
// This is used for translating elements / blocks - see `_translateAttributes` for attributes
|
|
// no-op when called in extraction mode (returns [])
|
|
_translateMessage(el, message) {
|
|
if (message && this._mode === _VisitorMode.Merge) {
|
|
const nodes = this._translations.get(message);
|
|
if (nodes) {
|
|
return nodes;
|
|
}
|
|
this._reportError(el, `Translation unavailable for message id="${this._translations.digest(message)}"`);
|
|
}
|
|
return [];
|
|
}
|
|
// translate the attributes of an element and remove i18n specific attributes
|
|
_translateAttributes(node) {
|
|
const i18nParsedMessageMeta = {};
|
|
const translatedAttributes = [];
|
|
node.attrs.forEach((attr) => {
|
|
if (attr.name.startsWith(_I18N_ATTR_PREFIX)) {
|
|
i18nParsedMessageMeta[attr.name.slice(_I18N_ATTR_PREFIX.length)] = _parseMessageMeta(attr.value);
|
|
}
|
|
});
|
|
node.attrs.forEach((attr) => {
|
|
if (attr.name === _I18N_ATTR || attr.name.startsWith(_I18N_ATTR_PREFIX)) {
|
|
// strip i18n specific attributes
|
|
return;
|
|
}
|
|
if (attr.value && attr.value != '' && i18nParsedMessageMeta.hasOwnProperty(attr.name)) {
|
|
const { meaning, description, id } = i18nParsedMessageMeta[attr.name];
|
|
const message = this._createI18nMessage([attr], meaning, description, id);
|
|
const nodes = this._translations.get(message);
|
|
if (nodes) {
|
|
if (nodes.length == 0) {
|
|
translatedAttributes.push(new Attribute(attr.name, '', attr.sourceSpan, undefined /* keySpan */, undefined /* valueSpan */, undefined /* valueTokens */, undefined /* i18n */));
|
|
}
|
|
else if (nodes[0] instanceof Text) {
|
|
const value = nodes[0].value;
|
|
translatedAttributes.push(new Attribute(attr.name, value, attr.sourceSpan, undefined /* keySpan */, undefined /* valueSpan */, undefined /* valueTokens */, undefined /* i18n */));
|
|
}
|
|
else {
|
|
this._reportError(node, `Unexpected translation for attribute "${attr.name}" (id="${id || this._translations.digest(message)}")`);
|
|
}
|
|
}
|
|
else {
|
|
this._reportError(node, `Translation unavailable for attribute "${attr.name}" (id="${id || this._translations.digest(message)}")`);
|
|
}
|
|
}
|
|
else {
|
|
translatedAttributes.push(attr);
|
|
}
|
|
});
|
|
return translatedAttributes;
|
|
}
|
|
_translateDirectives(node) {
|
|
return node.directives.map((dir) => new Directive(dir.name, this._translateAttributes(dir), dir.sourceSpan, dir.startSourceSpan, dir.endSourceSpan));
|
|
}
|
|
/**
|
|
* Add the node as a child of the block when:
|
|
* - we are in a block,
|
|
* - we are not inside a ICU message (those are handled separately),
|
|
* - the node is a "direct child" of the block
|
|
*/
|
|
_mayBeAddBlockChildren(node) {
|
|
if (this._inI18nBlock && !this._inIcu && this._depth == this._blockStartDepth) {
|
|
this._blockChildren.push(node);
|
|
}
|
|
}
|
|
/**
|
|
* Marks the start of a section, see `_closeTranslatableSection`
|
|
*/
|
|
_openTranslatableSection(node) {
|
|
if (this._isInTranslatableSection) {
|
|
this._reportError(node, 'Unexpected section start');
|
|
}
|
|
else {
|
|
this._msgCountAtSectionStart = this._messages.length;
|
|
}
|
|
}
|
|
/**
|
|
* A translatable section could be:
|
|
* - the content of translatable element,
|
|
* - nodes between `<!-- i18n -->` and `<!-- /i18n -->` comments
|
|
*/
|
|
get _isInTranslatableSection() {
|
|
return this._msgCountAtSectionStart !== void 0;
|
|
}
|
|
/**
|
|
* Terminates a section.
|
|
*
|
|
* If a section has only one significant children (comments not significant) then we should not
|
|
* keep the message from this children:
|
|
*
|
|
* `<p i18n="meaning|description">{ICU message}</p>` would produce two messages:
|
|
* - one for the <p> content with meaning and description,
|
|
* - another one for the ICU message.
|
|
*
|
|
* In this case the last message is discarded as it contains less information (the AST is
|
|
* otherwise identical).
|
|
*
|
|
* Note that we should still keep messages extracted from attributes inside the section (ie in the
|
|
* ICU message here)
|
|
*/
|
|
_closeTranslatableSection(node, directChildren) {
|
|
if (!this._isInTranslatableSection) {
|
|
this._reportError(node, 'Unexpected section end');
|
|
return;
|
|
}
|
|
const startIndex = this._msgCountAtSectionStart;
|
|
const significantChildren = directChildren.reduce((count, node) => count + (node instanceof Comment ? 0 : 1), 0);
|
|
if (significantChildren == 1) {
|
|
for (let i = this._messages.length - 1; i >= startIndex; i--) {
|
|
const ast = this._messages[i].nodes;
|
|
if (!(ast.length == 1 && ast[0] instanceof Text$2)) {
|
|
this._messages.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this._msgCountAtSectionStart = undefined;
|
|
}
|
|
_reportError(node, msg) {
|
|
this._errors.push(new ParseError(node.sourceSpan, msg));
|
|
}
|
|
}
|
|
function _isOpeningComment(n) {
|
|
return !!(n instanceof Comment && n.value && n.value.startsWith('i18n'));
|
|
}
|
|
function _isClosingComment(n) {
|
|
return !!(n instanceof Comment && n.value && n.value === '/i18n');
|
|
}
|
|
function _getI18nAttr(p) {
|
|
return (p.attrs.find((attr) => attr instanceof Attribute && attr.name === _I18N_ATTR) || null);
|
|
}
|
|
function _parseMessageMeta(i18n) {
|
|
if (!i18n)
|
|
return { meaning: '', description: '', id: '' };
|
|
const idIndex = i18n.indexOf(ID_SEPARATOR);
|
|
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
|
|
const [meaningAndDesc, id] = idIndex > -1 ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
|
|
const [meaning, description] = descIndex > -1
|
|
? [meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)]
|
|
: ['', meaningAndDesc];
|
|
return { meaning, description, id: id.trim() };
|
|
}
|
|
function isTextNode(ast) {
|
|
return ast.length === 1 && ast[0] instanceof Text;
|
|
}
|
|
function isAttrNode(ast) {
|
|
return ast.length === 1 && ast[0] instanceof Attribute;
|
|
}
|
|
|
|
class XmlTagDefinition {
|
|
closedByParent = false;
|
|
implicitNamespacePrefix = null;
|
|
isVoid = false;
|
|
ignoreFirstLf = false;
|
|
canSelfClose = true;
|
|
preventNamespaceInheritance = false;
|
|
requireExtraParent(currentParent) {
|
|
return false;
|
|
}
|
|
isClosedByChild(name) {
|
|
return false;
|
|
}
|
|
getContentType() {
|
|
return TagContentType.PARSABLE_DATA;
|
|
}
|
|
}
|
|
const _TAG_DEFINITION = new XmlTagDefinition();
|
|
function getXmlTagDefinition(tagName) {
|
|
return _TAG_DEFINITION;
|
|
}
|
|
|
|
class XmlParser extends Parser$1 {
|
|
constructor() {
|
|
super(getXmlTagDefinition);
|
|
}
|
|
parse(source, url, options = {}) {
|
|
// Blocks and let declarations aren't supported in an XML context.
|
|
return super.parse(source, url, {
|
|
...options,
|
|
tokenizeBlocks: false,
|
|
tokenizeLet: false,
|
|
selectorlessEnabled: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
const _VERSION$1 = '1.2';
|
|
const _XMLNS$1 = 'urn:oasis:names:tc:xliff:document:1.2';
|
|
// TODO(vicb): make this a param (s/_/-/)
|
|
const _DEFAULT_SOURCE_LANG$1 = 'en';
|
|
const _PLACEHOLDER_TAG$2 = 'x';
|
|
const _MARKER_TAG$1 = 'mrk';
|
|
const _FILE_TAG = 'file';
|
|
const _SOURCE_TAG$1 = 'source';
|
|
const _SEGMENT_SOURCE_TAG = 'seg-source';
|
|
const _ALT_TRANS_TAG = 'alt-trans';
|
|
const _TARGET_TAG$1 = 'target';
|
|
const _UNIT_TAG$1 = 'trans-unit';
|
|
const _CONTEXT_GROUP_TAG = 'context-group';
|
|
const _CONTEXT_TAG = 'context';
|
|
// https://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
|
|
// https://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
|
|
class Xliff extends Serializer {
|
|
write(messages, locale) {
|
|
const visitor = new _WriteVisitor$1();
|
|
const transUnits = [];
|
|
messages.forEach((message) => {
|
|
let contextTags = [];
|
|
message.sources.forEach((source) => {
|
|
let contextGroupTag = new Tag(_CONTEXT_GROUP_TAG, { purpose: 'location' });
|
|
contextGroupTag.children.push(new CR(10), new Tag(_CONTEXT_TAG, { 'context-type': 'sourcefile' }, [
|
|
new Text$1(source.filePath),
|
|
]), new CR(10), new Tag(_CONTEXT_TAG, { 'context-type': 'linenumber' }, [
|
|
new Text$1(`${source.startLine}`),
|
|
]), new CR(8));
|
|
contextTags.push(new CR(8), contextGroupTag);
|
|
});
|
|
const transUnit = new Tag(_UNIT_TAG$1, { id: message.id, datatype: 'html' });
|
|
transUnit.children.push(new CR(8), new Tag(_SOURCE_TAG$1, {}, visitor.serialize(message.nodes)), ...contextTags);
|
|
if (message.description) {
|
|
transUnit.children.push(new CR(8), new Tag('note', { priority: '1', from: 'description' }, [
|
|
new Text$1(message.description),
|
|
]));
|
|
}
|
|
if (message.meaning) {
|
|
transUnit.children.push(new CR(8), new Tag('note', { priority: '1', from: 'meaning' }, [new Text$1(message.meaning)]));
|
|
}
|
|
transUnit.children.push(new CR(6));
|
|
transUnits.push(new CR(6), transUnit);
|
|
});
|
|
const body = new Tag('body', {}, [...transUnits, new CR(4)]);
|
|
const file = new Tag('file', {
|
|
'source-language': locale || _DEFAULT_SOURCE_LANG$1,
|
|
datatype: 'plaintext',
|
|
original: 'ng2.template',
|
|
}, [new CR(4), body, new CR(2)]);
|
|
const xliff = new Tag('xliff', { version: _VERSION$1, xmlns: _XMLNS$1 }, [
|
|
new CR(2),
|
|
file,
|
|
new CR(),
|
|
]);
|
|
return serialize$1([
|
|
new Declaration({ version: '1.0', encoding: 'UTF-8' }),
|
|
new CR(),
|
|
xliff,
|
|
new CR(),
|
|
]);
|
|
}
|
|
load(content, url) {
|
|
// xliff to xml nodes
|
|
const xliffParser = new XliffParser();
|
|
const { locale, msgIdToHtml, errors } = xliffParser.parse(content, url);
|
|
// xml nodes to i18n nodes
|
|
const i18nNodesByMsgId = {};
|
|
const converter = new XmlToI18n$2();
|
|
Object.keys(msgIdToHtml).forEach((msgId) => {
|
|
const { i18nNodes, errors: e } = converter.convert(msgIdToHtml[msgId], url);
|
|
errors.push(...e);
|
|
i18nNodesByMsgId[msgId] = i18nNodes;
|
|
});
|
|
if (errors.length) {
|
|
throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
|
|
}
|
|
return { locale: locale, i18nNodesByMsgId };
|
|
}
|
|
digest(message) {
|
|
return digest$1(message);
|
|
}
|
|
}
|
|
let _WriteVisitor$1 = class _WriteVisitor {
|
|
visitText(text, context) {
|
|
return [new Text$1(text.value)];
|
|
}
|
|
visitContainer(container, context) {
|
|
const nodes = [];
|
|
container.children.forEach((node) => nodes.push(...node.visit(this)));
|
|
return nodes;
|
|
}
|
|
visitIcu(icu, context) {
|
|
const nodes = [new Text$1(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
|
|
Object.keys(icu.cases).forEach((c) => {
|
|
nodes.push(new Text$1(`${c} {`), ...icu.cases[c].visit(this), new Text$1(`} `));
|
|
});
|
|
nodes.push(new Text$1(`}`));
|
|
return nodes;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
const ctype = getCtypeForTag(ph.tag);
|
|
if (ph.isVoid) {
|
|
// void tags have no children nor closing tags
|
|
return [
|
|
new Tag(_PLACEHOLDER_TAG$2, { id: ph.startName, ctype, 'equiv-text': `<${ph.tag}/>` }),
|
|
];
|
|
}
|
|
const startTagPh = new Tag(_PLACEHOLDER_TAG$2, {
|
|
id: ph.startName,
|
|
ctype,
|
|
'equiv-text': `<${ph.tag}>`,
|
|
});
|
|
const closeTagPh = new Tag(_PLACEHOLDER_TAG$2, {
|
|
id: ph.closeName,
|
|
ctype,
|
|
'equiv-text': `</${ph.tag}>`,
|
|
});
|
|
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
return [new Tag(_PLACEHOLDER_TAG$2, { id: ph.name, 'equiv-text': `{{${ph.value}}}` })];
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
const ctype = `x-${ph.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
const startTagPh = new Tag(_PLACEHOLDER_TAG$2, {
|
|
id: ph.startName,
|
|
ctype,
|
|
'equiv-text': `@${ph.name}`,
|
|
});
|
|
const closeTagPh = new Tag(_PLACEHOLDER_TAG$2, { id: ph.closeName, ctype, 'equiv-text': `}` });
|
|
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
const equivText = `{${ph.value.expression}, ${ph.value.type}, ${Object.keys(ph.value.cases)
|
|
.map((value) => value + ' {...}')
|
|
.join(' ')}}`;
|
|
return [new Tag(_PLACEHOLDER_TAG$2, { id: ph.name, 'equiv-text': equivText })];
|
|
}
|
|
serialize(nodes) {
|
|
return [].concat(...nodes.map((node) => node.visit(this)));
|
|
}
|
|
};
|
|
// TODO(vicb): add error management (structure)
|
|
// Extract messages as xml nodes from the xliff file
|
|
class XliffParser {
|
|
// using non-null assertions because they're re(set) by parse()
|
|
_unitMlString;
|
|
_errors;
|
|
_msgIdToHtml;
|
|
_locale = null;
|
|
parse(xliff, url) {
|
|
this._unitMlString = null;
|
|
this._msgIdToHtml = {};
|
|
const xml = new XmlParser().parse(xliff, url);
|
|
this._errors = xml.errors;
|
|
visitAll(this, xml.rootNodes, null);
|
|
return {
|
|
msgIdToHtml: this._msgIdToHtml,
|
|
errors: this._errors,
|
|
locale: this._locale,
|
|
};
|
|
}
|
|
visitElement(element, context) {
|
|
switch (element.name) {
|
|
case _UNIT_TAG$1:
|
|
this._unitMlString = null;
|
|
const idAttr = element.attrs.find((attr) => attr.name === 'id');
|
|
if (!idAttr) {
|
|
this._addError(element, `<${_UNIT_TAG$1}> misses the "id" attribute`);
|
|
}
|
|
else {
|
|
const id = idAttr.value;
|
|
if (this._msgIdToHtml.hasOwnProperty(id)) {
|
|
this._addError(element, `Duplicated translations for msg ${id}`);
|
|
}
|
|
else {
|
|
visitAll(this, element.children, null);
|
|
if (typeof this._unitMlString === 'string') {
|
|
this._msgIdToHtml[id] = this._unitMlString;
|
|
}
|
|
else {
|
|
this._addError(element, `Message ${id} misses a translation`);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
// ignore those tags
|
|
case _SOURCE_TAG$1:
|
|
case _SEGMENT_SOURCE_TAG:
|
|
case _ALT_TRANS_TAG:
|
|
break;
|
|
case _TARGET_TAG$1:
|
|
const innerTextStart = element.startSourceSpan.end.offset;
|
|
const innerTextEnd = element.endSourceSpan.start.offset;
|
|
const content = element.startSourceSpan.start.file.content;
|
|
const innerText = content.slice(innerTextStart, innerTextEnd);
|
|
this._unitMlString = innerText;
|
|
break;
|
|
case _FILE_TAG:
|
|
const localeAttr = element.attrs.find((attr) => attr.name === 'target-language');
|
|
if (localeAttr) {
|
|
this._locale = localeAttr.value;
|
|
}
|
|
visitAll(this, element.children, null);
|
|
break;
|
|
default:
|
|
// TODO(vicb): assert file structure, xliff version
|
|
// For now only recurse on unhandled nodes
|
|
visitAll(this, element.children, null);
|
|
}
|
|
}
|
|
visitAttribute(attribute, context) { }
|
|
visitText(text, context) { }
|
|
visitComment(comment, context) { }
|
|
visitExpansion(expansion, context) { }
|
|
visitExpansionCase(expansionCase, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(parameter, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) { }
|
|
visitDirective(directive, context) { }
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
}
|
|
// Convert ml nodes (xliff syntax) to i18n nodes
|
|
let XmlToI18n$2 = class XmlToI18n {
|
|
// using non-null assertion because it's re(set) by convert()
|
|
_errors;
|
|
convert(message, url) {
|
|
const xmlIcu = new XmlParser().parse(message, url, { tokenizeExpansionForms: true });
|
|
this._errors = xmlIcu.errors;
|
|
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0
|
|
? []
|
|
: [].concat(...visitAll(this, xmlIcu.rootNodes));
|
|
return {
|
|
i18nNodes: i18nNodes,
|
|
errors: this._errors,
|
|
};
|
|
}
|
|
visitText(text, context) {
|
|
return new Text$2(text.value, text.sourceSpan);
|
|
}
|
|
visitElement(el, context) {
|
|
if (el.name === _PLACEHOLDER_TAG$2) {
|
|
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
|
|
if (nameAttr) {
|
|
return new Placeholder('', nameAttr.value, el.sourceSpan);
|
|
}
|
|
this._addError(el, `<${_PLACEHOLDER_TAG$2}> misses the "id" attribute`);
|
|
return null;
|
|
}
|
|
if (el.name === _MARKER_TAG$1) {
|
|
return [].concat(...visitAll(this, el.children));
|
|
}
|
|
this._addError(el, `Unexpected tag`);
|
|
return null;
|
|
}
|
|
visitExpansion(icu, context) {
|
|
const caseMap = {};
|
|
visitAll(this, icu.cases).forEach((c) => {
|
|
caseMap[c.value] = new Container(c.nodes, icu.sourceSpan);
|
|
});
|
|
return new Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
|
|
}
|
|
visitExpansionCase(icuCase, context) {
|
|
return {
|
|
value: icuCase.value,
|
|
nodes: visitAll(this, icuCase.expression),
|
|
};
|
|
}
|
|
visitComment(comment, context) { }
|
|
visitAttribute(attribute, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(parameter, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
this._addError(component, 'Unexpected node');
|
|
}
|
|
visitDirective(directive, context) {
|
|
this._addError(directive, 'Unexpected node');
|
|
}
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
};
|
|
function getCtypeForTag(tag) {
|
|
switch (tag.toLowerCase()) {
|
|
case 'br':
|
|
return 'lb';
|
|
case 'img':
|
|
return 'image';
|
|
default:
|
|
return `x-${tag}`;
|
|
}
|
|
}
|
|
|
|
const _VERSION = '2.0';
|
|
const _XMLNS = 'urn:oasis:names:tc:xliff:document:2.0';
|
|
// TODO(vicb): make this a param (s/_/-/)
|
|
const _DEFAULT_SOURCE_LANG = 'en';
|
|
const _PLACEHOLDER_TAG$1 = 'ph';
|
|
const _PLACEHOLDER_SPANNING_TAG = 'pc';
|
|
const _MARKER_TAG = 'mrk';
|
|
const _XLIFF_TAG = 'xliff';
|
|
const _SOURCE_TAG = 'source';
|
|
const _TARGET_TAG = 'target';
|
|
const _UNIT_TAG = 'unit';
|
|
// https://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
|
|
class Xliff2 extends Serializer {
|
|
write(messages, locale) {
|
|
const visitor = new _WriteVisitor();
|
|
const units = [];
|
|
messages.forEach((message) => {
|
|
const unit = new Tag(_UNIT_TAG, { id: message.id });
|
|
const notes = new Tag('notes');
|
|
if (message.description || message.meaning) {
|
|
if (message.description) {
|
|
notes.children.push(new CR(8), new Tag('note', { category: 'description' }, [new Text$1(message.description)]));
|
|
}
|
|
if (message.meaning) {
|
|
notes.children.push(new CR(8), new Tag('note', { category: 'meaning' }, [new Text$1(message.meaning)]));
|
|
}
|
|
}
|
|
message.sources.forEach((source) => {
|
|
notes.children.push(new CR(8), new Tag('note', { category: 'location' }, [
|
|
new Text$1(`${source.filePath}:${source.startLine}${source.endLine !== source.startLine ? ',' + source.endLine : ''}`),
|
|
]));
|
|
});
|
|
notes.children.push(new CR(6));
|
|
unit.children.push(new CR(6), notes);
|
|
const segment = new Tag('segment');
|
|
segment.children.push(new CR(8), new Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)), new CR(6));
|
|
unit.children.push(new CR(6), segment, new CR(4));
|
|
units.push(new CR(4), unit);
|
|
});
|
|
const file = new Tag('file', { 'original': 'ng.template', id: 'ngi18n' }, [
|
|
...units,
|
|
new CR(2),
|
|
]);
|
|
const xliff = new Tag(_XLIFF_TAG, { version: _VERSION, xmlns: _XMLNS, srcLang: locale || _DEFAULT_SOURCE_LANG }, [new CR(2), file, new CR()]);
|
|
return serialize$1([
|
|
new Declaration({ version: '1.0', encoding: 'UTF-8' }),
|
|
new CR(),
|
|
xliff,
|
|
new CR(),
|
|
]);
|
|
}
|
|
load(content, url) {
|
|
// xliff to xml nodes
|
|
const xliff2Parser = new Xliff2Parser();
|
|
const { locale, msgIdToHtml, errors } = xliff2Parser.parse(content, url);
|
|
// xml nodes to i18n nodes
|
|
const i18nNodesByMsgId = {};
|
|
const converter = new XmlToI18n$1();
|
|
Object.keys(msgIdToHtml).forEach((msgId) => {
|
|
const { i18nNodes, errors: e } = converter.convert(msgIdToHtml[msgId], url);
|
|
errors.push(...e);
|
|
i18nNodesByMsgId[msgId] = i18nNodes;
|
|
});
|
|
if (errors.length) {
|
|
throw new Error(`xliff2 parse errors:\n${errors.join('\n')}`);
|
|
}
|
|
return { locale: locale, i18nNodesByMsgId };
|
|
}
|
|
digest(message) {
|
|
return decimalDigest(message);
|
|
}
|
|
}
|
|
class _WriteVisitor {
|
|
_nextPlaceholderId = 0;
|
|
visitText(text, context) {
|
|
return [new Text$1(text.value)];
|
|
}
|
|
visitContainer(container, context) {
|
|
const nodes = [];
|
|
container.children.forEach((node) => nodes.push(...node.visit(this)));
|
|
return nodes;
|
|
}
|
|
visitIcu(icu, context) {
|
|
const nodes = [new Text$1(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
|
|
Object.keys(icu.cases).forEach((c) => {
|
|
nodes.push(new Text$1(`${c} {`), ...icu.cases[c].visit(this), new Text$1(`} `));
|
|
});
|
|
nodes.push(new Text$1(`}`));
|
|
return nodes;
|
|
}
|
|
visitTagPlaceholder(ph, context) {
|
|
const type = getTypeForTag(ph.tag);
|
|
if (ph.isVoid) {
|
|
const tagPh = new Tag(_PLACEHOLDER_TAG$1, {
|
|
id: (this._nextPlaceholderId++).toString(),
|
|
equiv: ph.startName,
|
|
type: type,
|
|
disp: `<${ph.tag}/>`,
|
|
});
|
|
return [tagPh];
|
|
}
|
|
const tagPc = new Tag(_PLACEHOLDER_SPANNING_TAG, {
|
|
id: (this._nextPlaceholderId++).toString(),
|
|
equivStart: ph.startName,
|
|
equivEnd: ph.closeName,
|
|
type: type,
|
|
dispStart: `<${ph.tag}>`,
|
|
dispEnd: `</${ph.tag}>`,
|
|
});
|
|
const nodes = [].concat(...ph.children.map((node) => node.visit(this)));
|
|
if (nodes.length) {
|
|
nodes.forEach((node) => tagPc.children.push(node));
|
|
}
|
|
else {
|
|
tagPc.children.push(new Text$1(''));
|
|
}
|
|
return [tagPc];
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
const idStr = (this._nextPlaceholderId++).toString();
|
|
return [
|
|
new Tag(_PLACEHOLDER_TAG$1, {
|
|
id: idStr,
|
|
equiv: ph.name,
|
|
disp: `{{${ph.value}}}`,
|
|
}),
|
|
];
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
const tagPc = new Tag(_PLACEHOLDER_SPANNING_TAG, {
|
|
id: (this._nextPlaceholderId++).toString(),
|
|
equivStart: ph.startName,
|
|
equivEnd: ph.closeName,
|
|
type: 'other',
|
|
dispStart: `@${ph.name}`,
|
|
dispEnd: `}`,
|
|
});
|
|
const nodes = [].concat(...ph.children.map((node) => node.visit(this)));
|
|
if (nodes.length) {
|
|
nodes.forEach((node) => tagPc.children.push(node));
|
|
}
|
|
else {
|
|
tagPc.children.push(new Text$1(''));
|
|
}
|
|
return [tagPc];
|
|
}
|
|
visitIcuPlaceholder(ph, context) {
|
|
const cases = Object.keys(ph.value.cases)
|
|
.map((value) => value + ' {...}')
|
|
.join(' ');
|
|
const idStr = (this._nextPlaceholderId++).toString();
|
|
return [
|
|
new Tag(_PLACEHOLDER_TAG$1, {
|
|
id: idStr,
|
|
equiv: ph.name,
|
|
disp: `{${ph.value.expression}, ${ph.value.type}, ${cases}}`,
|
|
}),
|
|
];
|
|
}
|
|
serialize(nodes) {
|
|
this._nextPlaceholderId = 0;
|
|
return [].concat(...nodes.map((node) => node.visit(this)));
|
|
}
|
|
}
|
|
// Extract messages as xml nodes from the xliff file
|
|
class Xliff2Parser {
|
|
// using non-null assertions because they're all (re)set by parse()
|
|
_unitMlString;
|
|
_errors;
|
|
_msgIdToHtml;
|
|
_locale = null;
|
|
parse(xliff, url) {
|
|
this._unitMlString = null;
|
|
this._msgIdToHtml = {};
|
|
const xml = new XmlParser().parse(xliff, url);
|
|
this._errors = xml.errors;
|
|
visitAll(this, xml.rootNodes, null);
|
|
return {
|
|
msgIdToHtml: this._msgIdToHtml,
|
|
errors: this._errors,
|
|
locale: this._locale,
|
|
};
|
|
}
|
|
visitElement(element, context) {
|
|
switch (element.name) {
|
|
case _UNIT_TAG:
|
|
this._unitMlString = null;
|
|
const idAttr = element.attrs.find((attr) => attr.name === 'id');
|
|
if (!idAttr) {
|
|
this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`);
|
|
}
|
|
else {
|
|
const id = idAttr.value;
|
|
if (this._msgIdToHtml.hasOwnProperty(id)) {
|
|
this._addError(element, `Duplicated translations for msg ${id}`);
|
|
}
|
|
else {
|
|
visitAll(this, element.children, null);
|
|
if (typeof this._unitMlString === 'string') {
|
|
this._msgIdToHtml[id] = this._unitMlString;
|
|
}
|
|
else {
|
|
this._addError(element, `Message ${id} misses a translation`);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case _SOURCE_TAG:
|
|
// ignore source message
|
|
break;
|
|
case _TARGET_TAG:
|
|
const innerTextStart = element.startSourceSpan.end.offset;
|
|
const innerTextEnd = element.endSourceSpan.start.offset;
|
|
const content = element.startSourceSpan.start.file.content;
|
|
const innerText = content.slice(innerTextStart, innerTextEnd);
|
|
this._unitMlString = innerText;
|
|
break;
|
|
case _XLIFF_TAG:
|
|
const localeAttr = element.attrs.find((attr) => attr.name === 'trgLang');
|
|
if (localeAttr) {
|
|
this._locale = localeAttr.value;
|
|
}
|
|
const versionAttr = element.attrs.find((attr) => attr.name === 'version');
|
|
if (versionAttr) {
|
|
const version = versionAttr.value;
|
|
if (version !== '2.0') {
|
|
this._addError(element, `The XLIFF file version ${version} is not compatible with XLIFF 2.0 serializer`);
|
|
}
|
|
else {
|
|
visitAll(this, element.children, null);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
visitAll(this, element.children, null);
|
|
}
|
|
}
|
|
visitAttribute(attribute, context) { }
|
|
visitText(text, context) { }
|
|
visitComment(comment, context) { }
|
|
visitExpansion(expansion, context) { }
|
|
visitExpansionCase(expansionCase, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(parameter, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) { }
|
|
visitDirective(directive, context) { }
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
}
|
|
// Convert ml nodes (xliff syntax) to i18n nodes
|
|
let XmlToI18n$1 = class XmlToI18n {
|
|
// using non-null assertion because re(set) by convert()
|
|
_errors;
|
|
convert(message, url) {
|
|
const xmlIcu = new XmlParser().parse(message, url, { tokenizeExpansionForms: true });
|
|
this._errors = xmlIcu.errors;
|
|
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0
|
|
? []
|
|
: [].concat(...visitAll(this, xmlIcu.rootNodes));
|
|
return {
|
|
i18nNodes,
|
|
errors: this._errors,
|
|
};
|
|
}
|
|
visitText(text, context) {
|
|
return new Text$2(text.value, text.sourceSpan);
|
|
}
|
|
visitElement(el, context) {
|
|
switch (el.name) {
|
|
case _PLACEHOLDER_TAG$1:
|
|
const nameAttr = el.attrs.find((attr) => attr.name === 'equiv');
|
|
if (nameAttr) {
|
|
return [new Placeholder('', nameAttr.value, el.sourceSpan)];
|
|
}
|
|
this._addError(el, `<${_PLACEHOLDER_TAG$1}> misses the "equiv" attribute`);
|
|
break;
|
|
case _PLACEHOLDER_SPANNING_TAG:
|
|
const startAttr = el.attrs.find((attr) => attr.name === 'equivStart');
|
|
const endAttr = el.attrs.find((attr) => attr.name === 'equivEnd');
|
|
if (!startAttr) {
|
|
this._addError(el, `<${_PLACEHOLDER_TAG$1}> misses the "equivStart" attribute`);
|
|
}
|
|
else if (!endAttr) {
|
|
this._addError(el, `<${_PLACEHOLDER_TAG$1}> misses the "equivEnd" attribute`);
|
|
}
|
|
else {
|
|
const startId = startAttr.value;
|
|
const endId = endAttr.value;
|
|
const nodes = [];
|
|
return nodes.concat(new Placeholder('', startId, el.sourceSpan), ...el.children.map((node) => node.visit(this, null)), new Placeholder('', endId, el.sourceSpan));
|
|
}
|
|
break;
|
|
case _MARKER_TAG:
|
|
return [].concat(...visitAll(this, el.children));
|
|
default:
|
|
this._addError(el, `Unexpected tag`);
|
|
}
|
|
return null;
|
|
}
|
|
visitExpansion(icu, context) {
|
|
const caseMap = {};
|
|
visitAll(this, icu.cases).forEach((c) => {
|
|
caseMap[c.value] = new Container(c.nodes, icu.sourceSpan);
|
|
});
|
|
return new Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
|
|
}
|
|
visitExpansionCase(icuCase, context) {
|
|
return {
|
|
value: icuCase.value,
|
|
nodes: [].concat(...visitAll(this, icuCase.expression)),
|
|
};
|
|
}
|
|
visitComment(comment, context) { }
|
|
visitAttribute(attribute, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(parameter, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
this._addError(component, 'Unexpected node');
|
|
}
|
|
visitDirective(directive, context) {
|
|
this._addError(directive, 'Unexpected node');
|
|
}
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
};
|
|
function getTypeForTag(tag) {
|
|
switch (tag.toLowerCase()) {
|
|
case 'br':
|
|
case 'b':
|
|
case 'i':
|
|
case 'u':
|
|
return 'fmt';
|
|
case 'img':
|
|
return 'image';
|
|
case 'a':
|
|
return 'link';
|
|
default:
|
|
return 'other';
|
|
}
|
|
}
|
|
|
|
const _TRANSLATIONS_TAG = 'translationbundle';
|
|
const _TRANSLATION_TAG = 'translation';
|
|
const _PLACEHOLDER_TAG = 'ph';
|
|
class Xtb extends Serializer {
|
|
write(messages, locale) {
|
|
throw new Error('Unsupported');
|
|
}
|
|
load(content, url) {
|
|
// xtb to xml nodes
|
|
const xtbParser = new XtbParser();
|
|
const { locale, msgIdToHtml, errors } = xtbParser.parse(content, url);
|
|
// xml nodes to i18n nodes
|
|
const i18nNodesByMsgId = {};
|
|
const converter = new XmlToI18n();
|
|
// Because we should be able to load xtb files that rely on features not supported by angular,
|
|
// we need to delay the conversion of html to i18n nodes so that non angular messages are not
|
|
// converted
|
|
Object.keys(msgIdToHtml).forEach((msgId) => {
|
|
const valueFn = function () {
|
|
const { i18nNodes, errors } = converter.convert(msgIdToHtml[msgId], url);
|
|
if (errors.length) {
|
|
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
|
}
|
|
return i18nNodes;
|
|
};
|
|
createLazyProperty(i18nNodesByMsgId, msgId, valueFn);
|
|
});
|
|
if (errors.length) {
|
|
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
|
}
|
|
return { locale: locale, i18nNodesByMsgId };
|
|
}
|
|
digest(message) {
|
|
return digest(message);
|
|
}
|
|
createNameMapper(message) {
|
|
return new SimplePlaceholderMapper(message, toPublicName);
|
|
}
|
|
}
|
|
function createLazyProperty(messages, id, valueFn) {
|
|
Object.defineProperty(messages, id, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function () {
|
|
const value = valueFn();
|
|
Object.defineProperty(messages, id, { enumerable: true, value });
|
|
return value;
|
|
},
|
|
set: (_) => {
|
|
throw new Error('Could not overwrite an XTB translation');
|
|
},
|
|
});
|
|
}
|
|
// Extract messages as xml nodes from the xtb file
|
|
class XtbParser {
|
|
// using non-null assertions because they're (re)set by parse()
|
|
_bundleDepth;
|
|
_errors;
|
|
_msgIdToHtml;
|
|
_locale = null;
|
|
parse(xtb, url) {
|
|
this._bundleDepth = 0;
|
|
this._msgIdToHtml = {};
|
|
// We can not parse the ICU messages at this point as some messages might not originate
|
|
// from Angular that could not be lex'd.
|
|
const xml = new XmlParser().parse(xtb, url);
|
|
this._errors = xml.errors;
|
|
visitAll(this, xml.rootNodes);
|
|
return {
|
|
msgIdToHtml: this._msgIdToHtml,
|
|
errors: this._errors,
|
|
locale: this._locale,
|
|
};
|
|
}
|
|
visitElement(element, context) {
|
|
switch (element.name) {
|
|
case _TRANSLATIONS_TAG:
|
|
this._bundleDepth++;
|
|
if (this._bundleDepth > 1) {
|
|
this._addError(element, `<${_TRANSLATIONS_TAG}> elements can not be nested`);
|
|
}
|
|
const langAttr = element.attrs.find((attr) => attr.name === 'lang');
|
|
if (langAttr) {
|
|
this._locale = langAttr.value;
|
|
}
|
|
visitAll(this, element.children, null);
|
|
this._bundleDepth--;
|
|
break;
|
|
case _TRANSLATION_TAG:
|
|
const idAttr = element.attrs.find((attr) => attr.name === 'id');
|
|
if (!idAttr) {
|
|
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
|
|
}
|
|
else {
|
|
const id = idAttr.value;
|
|
if (this._msgIdToHtml.hasOwnProperty(id)) {
|
|
this._addError(element, `Duplicated translations for msg ${id}`);
|
|
}
|
|
else {
|
|
const innerTextStart = element.startSourceSpan.end.offset;
|
|
const innerTextEnd = element.endSourceSpan.start.offset;
|
|
const content = element.startSourceSpan.start.file.content;
|
|
const innerText = content.slice(innerTextStart, innerTextEnd);
|
|
this._msgIdToHtml[id] = innerText;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
this._addError(element, 'Unexpected tag');
|
|
}
|
|
}
|
|
visitAttribute(attribute, context) { }
|
|
visitText(text, context) { }
|
|
visitComment(comment, context) { }
|
|
visitExpansion(expansion, context) { }
|
|
visitExpansionCase(expansionCase, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(block, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
this._addError(component, 'Unexpected node');
|
|
}
|
|
visitDirective(directive, context) {
|
|
this._addError(directive, 'Unexpected node');
|
|
}
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
}
|
|
// Convert ml nodes (xtb syntax) to i18n nodes
|
|
class XmlToI18n {
|
|
// using non-null assertion because it's (re)set by convert()
|
|
_errors;
|
|
convert(message, url) {
|
|
const xmlIcu = new XmlParser().parse(message, url, { tokenizeExpansionForms: true });
|
|
this._errors = xmlIcu.errors;
|
|
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0
|
|
? []
|
|
: visitAll(this, xmlIcu.rootNodes);
|
|
return {
|
|
i18nNodes,
|
|
errors: this._errors,
|
|
};
|
|
}
|
|
visitText(text, context) {
|
|
return new Text$2(text.value, text.sourceSpan);
|
|
}
|
|
visitExpansion(icu, context) {
|
|
const caseMap = {};
|
|
visitAll(this, icu.cases).forEach((c) => {
|
|
caseMap[c.value] = new Container(c.nodes, icu.sourceSpan);
|
|
});
|
|
return new Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
|
|
}
|
|
visitExpansionCase(icuCase, context) {
|
|
return {
|
|
value: icuCase.value,
|
|
nodes: visitAll(this, icuCase.expression),
|
|
};
|
|
}
|
|
visitElement(el, context) {
|
|
if (el.name === _PLACEHOLDER_TAG) {
|
|
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
|
|
if (nameAttr) {
|
|
return new Placeholder('', nameAttr.value, el.sourceSpan);
|
|
}
|
|
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
|
|
}
|
|
else {
|
|
this._addError(el, `Unexpected tag`);
|
|
}
|
|
return null;
|
|
}
|
|
visitComment(comment, context) { }
|
|
visitAttribute(attribute, context) { }
|
|
visitBlock(block, context) { }
|
|
visitBlockParameter(block, context) { }
|
|
visitLetDeclaration(decl, context) { }
|
|
visitComponent(component, context) {
|
|
this._addError(component, 'Unexpected node');
|
|
}
|
|
visitDirective(directive, context) {
|
|
this._addError(directive, 'Unexpected node');
|
|
}
|
|
_addError(node, message) {
|
|
this._errors.push(new ParseError(node.sourceSpan, message));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A container for translated messages
|
|
*/
|
|
class TranslationBundle {
|
|
_i18nNodesByMsgId;
|
|
digest;
|
|
mapperFactory;
|
|
_i18nToHtml;
|
|
constructor(_i18nNodesByMsgId = {}, locale, digest, mapperFactory, missingTranslationStrategy = MissingTranslationStrategy.Warning, console) {
|
|
this._i18nNodesByMsgId = _i18nNodesByMsgId;
|
|
this.digest = digest;
|
|
this.mapperFactory = mapperFactory;
|
|
this._i18nToHtml = new I18nToHtmlVisitor(_i18nNodesByMsgId, locale, digest, mapperFactory, missingTranslationStrategy, console);
|
|
}
|
|
// Creates a `TranslationBundle` by parsing the given `content` with the `serializer`.
|
|
static load(content, url, serializer, missingTranslationStrategy, console) {
|
|
const { locale, i18nNodesByMsgId } = serializer.load(content, url);
|
|
const digestFn = (m) => serializer.digest(m);
|
|
const mapperFactory = (m) => serializer.createNameMapper(m);
|
|
return new TranslationBundle(i18nNodesByMsgId, locale, digestFn, mapperFactory, missingTranslationStrategy, console);
|
|
}
|
|
// Returns the translation as HTML nodes from the given source message.
|
|
get(srcMsg) {
|
|
const html = this._i18nToHtml.convert(srcMsg);
|
|
if (html.errors.length) {
|
|
throw new Error(html.errors.join('\n'));
|
|
}
|
|
return html.nodes;
|
|
}
|
|
has(srcMsg) {
|
|
return this.digest(srcMsg) in this._i18nNodesByMsgId;
|
|
}
|
|
}
|
|
class I18nToHtmlVisitor {
|
|
_i18nNodesByMsgId;
|
|
_locale;
|
|
_digest;
|
|
_mapperFactory;
|
|
_missingTranslationStrategy;
|
|
_console;
|
|
// using non-null assertions because they're (re)set by convert()
|
|
_srcMsg;
|
|
_errors = [];
|
|
_contextStack = [];
|
|
_mapper;
|
|
constructor(_i18nNodesByMsgId = {}, _locale, _digest, _mapperFactory, _missingTranslationStrategy, _console) {
|
|
this._i18nNodesByMsgId = _i18nNodesByMsgId;
|
|
this._locale = _locale;
|
|
this._digest = _digest;
|
|
this._mapperFactory = _mapperFactory;
|
|
this._missingTranslationStrategy = _missingTranslationStrategy;
|
|
this._console = _console;
|
|
}
|
|
convert(srcMsg) {
|
|
this._contextStack.length = 0;
|
|
this._errors.length = 0;
|
|
// i18n to text
|
|
const text = this._convertToText(srcMsg);
|
|
// text to html
|
|
const url = srcMsg.nodes[0].sourceSpan.start.file.url;
|
|
const html = new HtmlParser().parse(text, url, { tokenizeExpansionForms: true });
|
|
return {
|
|
nodes: html.rootNodes,
|
|
errors: [...this._errors, ...html.errors],
|
|
};
|
|
}
|
|
visitText(text, context) {
|
|
// `convert()` uses an `HtmlParser` to return `html.Node`s
|
|
// we should then make sure that any special characters are escaped
|
|
return escapeXml(text.value);
|
|
}
|
|
visitContainer(container, context) {
|
|
return container.children.map((n) => n.visit(this)).join('');
|
|
}
|
|
visitIcu(icu, context) {
|
|
const cases = Object.keys(icu.cases).map((k) => `${k} {${icu.cases[k].visit(this)}}`);
|
|
// TODO(vicb): Once all format switch to using expression placeholders
|
|
// we should throw when the placeholder is not in the source message
|
|
const exp = this._srcMsg.placeholders.hasOwnProperty(icu.expression)
|
|
? this._srcMsg.placeholders[icu.expression].text
|
|
: icu.expression;
|
|
return `{${exp}, ${icu.type}, ${cases.join(' ')}}`;
|
|
}
|
|
visitPlaceholder(ph, context) {
|
|
const phName = this._mapper(ph.name);
|
|
if (this._srcMsg.placeholders.hasOwnProperty(phName)) {
|
|
return this._srcMsg.placeholders[phName].text;
|
|
}
|
|
if (this._srcMsg.placeholderToMessage.hasOwnProperty(phName)) {
|
|
return this._convertToText(this._srcMsg.placeholderToMessage[phName]);
|
|
}
|
|
this._addError(ph, `Unknown placeholder "${ph.name}"`);
|
|
return '';
|
|
}
|
|
// Loaded message contains only placeholders (vs tag and icu placeholders).
|
|
// However when a translation can not be found, we need to serialize the source message
|
|
// which can contain tag placeholders
|
|
visitTagPlaceholder(ph, context) {
|
|
const tag = `${ph.tag}`;
|
|
const attrs = Object.keys(ph.attrs)
|
|
.map((name) => `${name}="${ph.attrs[name]}"`)
|
|
.join(' ');
|
|
if (ph.isVoid) {
|
|
return `<${tag} ${attrs}/>`;
|
|
}
|
|
const children = ph.children.map((c) => c.visit(this)).join('');
|
|
return `<${tag} ${attrs}>${children}</${tag}>`;
|
|
}
|
|
// Loaded message contains only placeholders (vs tag and icu placeholders).
|
|
// However when a translation can not be found, we need to serialize the source message
|
|
// which can contain tag placeholders
|
|
visitIcuPlaceholder(ph, context) {
|
|
// An ICU placeholder references the source message to be serialized
|
|
return this._convertToText(this._srcMsg.placeholderToMessage[ph.name]);
|
|
}
|
|
visitBlockPlaceholder(ph, context) {
|
|
const params = ph.parameters.length === 0 ? '' : ` (${ph.parameters.join('; ')})`;
|
|
const children = ph.children.map((c) => c.visit(this)).join('');
|
|
return `@${ph.name}${params} {${children}}`;
|
|
}
|
|
/**
|
|
* Convert a source message to a translated text string:
|
|
* - text nodes are replaced with their translation,
|
|
* - placeholders are replaced with their content,
|
|
* - ICU nodes are converted to ICU expressions.
|
|
*/
|
|
_convertToText(srcMsg) {
|
|
const id = this._digest(srcMsg);
|
|
const mapper = this._mapperFactory ? this._mapperFactory(srcMsg) : null;
|
|
let nodes;
|
|
this._contextStack.push({ msg: this._srcMsg, mapper: this._mapper });
|
|
this._srcMsg = srcMsg;
|
|
if (this._i18nNodesByMsgId.hasOwnProperty(id)) {
|
|
// When there is a translation use its nodes as the source
|
|
// And create a mapper to convert serialized placeholder names to internal names
|
|
nodes = this._i18nNodesByMsgId[id];
|
|
this._mapper = (name) => (mapper ? mapper.toInternalName(name) : name);
|
|
}
|
|
else {
|
|
// When no translation has been found
|
|
// - report an error / a warning / nothing,
|
|
// - use the nodes from the original message
|
|
// - placeholders are already internal and need no mapper
|
|
if (this._missingTranslationStrategy === MissingTranslationStrategy.Error) {
|
|
const ctx = this._locale ? ` for locale "${this._locale}"` : '';
|
|
this._addError(srcMsg.nodes[0], `Missing translation for message "${id}"${ctx}`);
|
|
}
|
|
else if (this._console &&
|
|
this._missingTranslationStrategy === MissingTranslationStrategy.Warning) {
|
|
const ctx = this._locale ? ` for locale "${this._locale}"` : '';
|
|
this._console.warn(`Missing translation for message "${id}"${ctx}`);
|
|
}
|
|
nodes = srcMsg.nodes;
|
|
this._mapper = (name) => name;
|
|
}
|
|
const text = nodes.map((node) => node.visit(this)).join('');
|
|
const context = this._contextStack.pop();
|
|
this._srcMsg = context.msg;
|
|
this._mapper = context.mapper;
|
|
return text;
|
|
}
|
|
_addError(el, msg) {
|
|
this._errors.push(new ParseError(el.sourceSpan, msg));
|
|
}
|
|
}
|
|
|
|
class I18NHtmlParser {
|
|
_htmlParser;
|
|
// @override
|
|
getTagDefinition;
|
|
_translationBundle;
|
|
constructor(_htmlParser, translations, translationsFormat, missingTranslation = MissingTranslationStrategy.Warning, console) {
|
|
this._htmlParser = _htmlParser;
|
|
if (translations) {
|
|
const serializer = createSerializer(translationsFormat);
|
|
this._translationBundle = TranslationBundle.load(translations, 'i18n', serializer, missingTranslation, console);
|
|
}
|
|
else {
|
|
this._translationBundle = new TranslationBundle({}, null, digest$1, undefined, missingTranslation, console);
|
|
}
|
|
}
|
|
parse(source, url, options = {}) {
|
|
const interpolationConfig = options.interpolationConfig || DEFAULT_INTERPOLATION_CONFIG;
|
|
const parseResult = this._htmlParser.parse(source, url, { interpolationConfig, ...options });
|
|
if (parseResult.errors.length) {
|
|
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors);
|
|
}
|
|
return mergeTranslations(parseResult.rootNodes, this._translationBundle, interpolationConfig, [], {});
|
|
}
|
|
}
|
|
function createSerializer(format) {
|
|
format = (format || 'xlf').toLowerCase();
|
|
switch (format) {
|
|
case 'xmb':
|
|
return new Xmb();
|
|
case 'xtb':
|
|
return new Xtb();
|
|
case 'xliff2':
|
|
case 'xlf2':
|
|
return new Xliff2();
|
|
case 'xliff':
|
|
case 'xlf':
|
|
default:
|
|
return new Xliff();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A container for message extracted from the templates.
|
|
*/
|
|
class MessageBundle {
|
|
_htmlParser;
|
|
_implicitTags;
|
|
_implicitAttrs;
|
|
_locale;
|
|
_preserveWhitespace;
|
|
_messages = [];
|
|
constructor(_htmlParser, _implicitTags, _implicitAttrs, _locale = null, _preserveWhitespace = true) {
|
|
this._htmlParser = _htmlParser;
|
|
this._implicitTags = _implicitTags;
|
|
this._implicitAttrs = _implicitAttrs;
|
|
this._locale = _locale;
|
|
this._preserveWhitespace = _preserveWhitespace;
|
|
}
|
|
updateFromTemplate(source, url, interpolationConfig) {
|
|
const htmlParserResult = this._htmlParser.parse(source, url, {
|
|
tokenizeExpansionForms: true,
|
|
interpolationConfig,
|
|
});
|
|
if (htmlParserResult.errors.length) {
|
|
return htmlParserResult.errors;
|
|
}
|
|
// Trim unnecessary whitespace from extracted messages if requested. This
|
|
// makes the messages more durable to trivial whitespace changes without
|
|
// affected message IDs.
|
|
const rootNodes = this._preserveWhitespace
|
|
? htmlParserResult.rootNodes
|
|
: visitAllWithSiblings(new WhitespaceVisitor(/* preserveSignificantWhitespace */ false), htmlParserResult.rootNodes);
|
|
const i18nParserResult = extractMessages(rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs,
|
|
/* preserveSignificantWhitespace */ this._preserveWhitespace);
|
|
if (i18nParserResult.errors.length) {
|
|
return i18nParserResult.errors;
|
|
}
|
|
this._messages.push(...i18nParserResult.messages);
|
|
return [];
|
|
}
|
|
// Return the message in the internal format
|
|
// The public (serialized) format might be different, see the `write` method.
|
|
getMessages() {
|
|
return this._messages;
|
|
}
|
|
write(serializer, filterSources) {
|
|
const messages = {};
|
|
const mapperVisitor = new MapPlaceholderNames();
|
|
// Deduplicate messages based on their ID
|
|
this._messages.forEach((message) => {
|
|
const id = serializer.digest(message);
|
|
if (!messages.hasOwnProperty(id)) {
|
|
messages[id] = message;
|
|
}
|
|
else {
|
|
messages[id].sources.push(...message.sources);
|
|
}
|
|
});
|
|
// Transform placeholder names using the serializer mapping
|
|
const msgList = Object.keys(messages).map((id) => {
|
|
const mapper = serializer.createNameMapper(messages[id]);
|
|
const src = messages[id];
|
|
const nodes = mapper ? mapperVisitor.convert(src.nodes, mapper) : src.nodes;
|
|
let transformedMessage = new Message(nodes, {}, {}, src.meaning, src.description, id);
|
|
transformedMessage.sources = src.sources;
|
|
if (filterSources) {
|
|
transformedMessage.sources.forEach((source) => (source.filePath = filterSources(source.filePath)));
|
|
}
|
|
return transformedMessage;
|
|
});
|
|
return serializer.write(msgList, this._locale);
|
|
}
|
|
}
|
|
// Transform an i18n AST by renaming the placeholder nodes with the given mapper
|
|
class MapPlaceholderNames extends CloneVisitor {
|
|
convert(nodes, mapper) {
|
|
return mapper ? nodes.map((n) => n.visit(this, mapper)) : nodes;
|
|
}
|
|
visitTagPlaceholder(ph, mapper) {
|
|
const startName = mapper.toPublicName(ph.startName);
|
|
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
|
|
const children = ph.children.map((n) => n.visit(this, mapper));
|
|
return new TagPlaceholder(ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
|
|
}
|
|
visitBlockPlaceholder(ph, mapper) {
|
|
const startName = mapper.toPublicName(ph.startName);
|
|
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
|
|
const children = ph.children.map((n) => n.visit(this, mapper));
|
|
return new BlockPlaceholder(ph.name, ph.parameters, startName, closeName, children, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
|
|
}
|
|
visitPlaceholder(ph, mapper) {
|
|
return new Placeholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
|
|
}
|
|
visitIcuPlaceholder(ph, mapper) {
|
|
return new IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
|
|
}
|
|
}
|
|
|
|
function compileClassMetadata(metadata) {
|
|
const fnCall = internalCompileClassMetadata(metadata);
|
|
return arrowFn([], [devOnlyGuardedExpression(fnCall).toStmt()]).callFn([]);
|
|
}
|
|
/** Compiles only the `setClassMetadata` call without any additional wrappers. */
|
|
function internalCompileClassMetadata(metadata) {
|
|
return importExpr(Identifiers.setClassMetadata)
|
|
.callFn([
|
|
metadata.type,
|
|
metadata.decorators,
|
|
metadata.ctorParameters ?? literal(null),
|
|
metadata.propDecorators ?? literal(null),
|
|
]);
|
|
}
|
|
/**
|
|
* Wraps the `setClassMetadata` function with extra logic that dynamically
|
|
* loads dependencies from `@defer` blocks.
|
|
*
|
|
* Generates a call like this:
|
|
* ```ts
|
|
* setClassMetadataAsync(type, () => [
|
|
* import('./cmp-a').then(m => m.CmpA);
|
|
* import('./cmp-b').then(m => m.CmpB);
|
|
* ], (CmpA, CmpB) => {
|
|
* setClassMetadata(type, decorators, ctorParameters, propParameters);
|
|
* });
|
|
* ```
|
|
*
|
|
* Similar to the `setClassMetadata` call, it's wrapped into the `ngDevMode`
|
|
* check to tree-shake away this code in production mode.
|
|
*/
|
|
function compileComponentClassMetadata(metadata, dependencies) {
|
|
if (dependencies === null || dependencies.length === 0) {
|
|
// If there are no deferrable symbols - just generate a regular `setClassMetadata` call.
|
|
return compileClassMetadata(metadata);
|
|
}
|
|
return internalCompileSetClassMetadataAsync(metadata, dependencies.map((dep) => new FnParam(dep.symbolName, DYNAMIC_TYPE)), compileComponentMetadataAsyncResolver(dependencies));
|
|
}
|
|
/**
|
|
* Identical to `compileComponentClassMetadata`. Used for the cases where we're unable to
|
|
* analyze the deferred block dependencies, but we have a reference to the compiled
|
|
* dependency resolver function that we can use as is.
|
|
* @param metadata Class metadata for the internal `setClassMetadata` call.
|
|
* @param deferResolver Expression representing the deferred dependency loading function.
|
|
* @param deferredDependencyNames Names of the dependencies that are being loaded asynchronously.
|
|
*/
|
|
function compileOpaqueAsyncClassMetadata(metadata, deferResolver, deferredDependencyNames) {
|
|
return internalCompileSetClassMetadataAsync(metadata, deferredDependencyNames.map((name) => new FnParam(name, DYNAMIC_TYPE)), deferResolver);
|
|
}
|
|
/**
|
|
* Internal logic used to compile a `setClassMetadataAsync` call.
|
|
* @param metadata Class metadata for the internal `setClassMetadata` call.
|
|
* @param wrapperParams Parameters to be set on the callback that wraps `setClassMetata`.
|
|
* @param dependencyResolverFn Function to resolve the deferred dependencies.
|
|
*/
|
|
function internalCompileSetClassMetadataAsync(metadata, wrapperParams, dependencyResolverFn) {
|
|
// Omit the wrapper since it'll be added around `setClassMetadataAsync` instead.
|
|
const setClassMetadataCall = internalCompileClassMetadata(metadata);
|
|
const setClassMetaWrapper = arrowFn(wrapperParams, [setClassMetadataCall.toStmt()]);
|
|
const setClassMetaAsync = importExpr(Identifiers.setClassMetadataAsync)
|
|
.callFn([metadata.type, dependencyResolverFn, setClassMetaWrapper]);
|
|
return arrowFn([], [devOnlyGuardedExpression(setClassMetaAsync).toStmt()]).callFn([]);
|
|
}
|
|
/**
|
|
* Compiles the function that loads the dependencies for the
|
|
* entire component in `setClassMetadataAsync`.
|
|
*/
|
|
function compileComponentMetadataAsyncResolver(dependencies) {
|
|
const dynamicImports = dependencies.map(({ symbolName, importPath, isDefaultImport }) => {
|
|
// e.g. `(m) => m.CmpA`
|
|
const innerFn =
|
|
// Default imports are always accessed through the `default` property.
|
|
arrowFn([new FnParam('m', DYNAMIC_TYPE)], variable('m').prop(isDefaultImport ? 'default' : symbolName));
|
|
// e.g. `import('./cmp-a').then(...)`
|
|
return new DynamicImportExpr(importPath).prop('then').callFn([innerFn]);
|
|
});
|
|
// e.g. `() => [ ... ];`
|
|
return arrowFn([], literalArr(dynamicImports));
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION$5 = '12.0.0';
|
|
/**
|
|
* Minimum version at which deferred blocks are supported in the linker.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION = '18.0.0';
|
|
function compileDeclareClassMetadata(metadata) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION$5));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', metadata.type);
|
|
definitionMap.set('decorators', metadata.decorators);
|
|
definitionMap.set('ctorParameters', metadata.ctorParameters);
|
|
definitionMap.set('propDecorators', metadata.propDecorators);
|
|
return importExpr(Identifiers.declareClassMetadata).callFn([definitionMap.toLiteralMap()]);
|
|
}
|
|
function compileComponentDeclareClassMetadata(metadata, dependencies) {
|
|
if (dependencies === null || dependencies.length === 0) {
|
|
return compileDeclareClassMetadata(metadata);
|
|
}
|
|
const definitionMap = new DefinitionMap();
|
|
const callbackReturnDefinitionMap = new DefinitionMap();
|
|
callbackReturnDefinitionMap.set('decorators', metadata.decorators);
|
|
callbackReturnDefinitionMap.set('ctorParameters', metadata.ctorParameters ?? literal(null));
|
|
callbackReturnDefinitionMap.set('propDecorators', metadata.propDecorators ?? literal(null));
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', metadata.type);
|
|
definitionMap.set('resolveDeferredDeps', compileComponentMetadataAsyncResolver(dependencies));
|
|
definitionMap.set('resolveMetadata', arrowFn(dependencies.map((dep) => new FnParam(dep.symbolName, DYNAMIC_TYPE)), callbackReturnDefinitionMap.toLiteralMap()));
|
|
return importExpr(Identifiers.declareClassMetadataAsync).callFn([definitionMap.toLiteralMap()]);
|
|
}
|
|
|
|
/**
|
|
* Creates an array literal expression from the given array, mapping all values to an expression
|
|
* using the provided mapping function. If the array is empty or null, then null is returned.
|
|
*
|
|
* @param values The array to transfer into literal array expression.
|
|
* @param mapper The logic to use for creating an expression for the array's values.
|
|
* @returns An array literal expression representing `values`, or null if `values` is empty or
|
|
* is itself null.
|
|
*/
|
|
function toOptionalLiteralArray(values, mapper) {
|
|
if (values === null || values.length === 0) {
|
|
return null;
|
|
}
|
|
return literalArr(values.map((value) => mapper(value)));
|
|
}
|
|
/**
|
|
* Creates an object literal expression from the given object, mapping all values to an expression
|
|
* using the provided mapping function. If the object has no keys, then null is returned.
|
|
*
|
|
* @param object The object to transfer into an object literal expression.
|
|
* @param mapper The logic to use for creating an expression for the object's values.
|
|
* @returns An object literal expression representing `object`, or null if `object` does not have
|
|
* any keys.
|
|
*/
|
|
function toOptionalLiteralMap(object, mapper) {
|
|
const entries = Object.keys(object).map((key) => {
|
|
const value = object[key];
|
|
return { key, value: mapper(value), quoted: true };
|
|
});
|
|
if (entries.length > 0) {
|
|
return literalMap(entries);
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
function compileDependencies(deps) {
|
|
if (deps === 'invalid') {
|
|
// The `deps` can be set to the string "invalid" by the `unwrapConstructorDependencies()`
|
|
// function, which tries to convert `ConstructorDeps` into `R3DependencyMetadata[]`.
|
|
return literal('invalid');
|
|
}
|
|
else if (deps === null) {
|
|
return literal(null);
|
|
}
|
|
else {
|
|
return literalArr(deps.map(compileDependency));
|
|
}
|
|
}
|
|
function compileDependency(dep) {
|
|
const depMeta = new DefinitionMap();
|
|
depMeta.set('token', dep.token);
|
|
if (dep.attributeNameType !== null) {
|
|
depMeta.set('attribute', literal(true));
|
|
}
|
|
if (dep.host) {
|
|
depMeta.set('host', literal(true));
|
|
}
|
|
if (dep.optional) {
|
|
depMeta.set('optional', literal(true));
|
|
}
|
|
if (dep.self) {
|
|
depMeta.set('self', literal(true));
|
|
}
|
|
if (dep.skipSelf) {
|
|
depMeta.set('skipSelf', literal(true));
|
|
}
|
|
return depMeta.toLiteralMap();
|
|
}
|
|
|
|
/**
|
|
* Compile a directive declaration defined by the `R3DirectiveMetadata`.
|
|
*/
|
|
function compileDeclareDirectiveFromMetadata(meta) {
|
|
const definitionMap = createDirectiveDefinitionMap(meta);
|
|
const expression = importExpr(Identifiers.declareDirective).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createDirectiveType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for a directive into a `DefinitionMap`. This allows for reusing
|
|
* this logic for components, as they extend the directive metadata.
|
|
*/
|
|
function createDirectiveDefinitionMap(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
const minVersion = getMinimumVersionForPartialOutput(meta);
|
|
definitionMap.set('minVersion', literal(minVersion));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
// e.g. `type: MyDirective`
|
|
definitionMap.set('type', meta.type.value);
|
|
if (meta.isStandalone !== undefined) {
|
|
definitionMap.set('isStandalone', literal(meta.isStandalone));
|
|
}
|
|
if (meta.isSignal) {
|
|
definitionMap.set('isSignal', literal(meta.isSignal));
|
|
}
|
|
// e.g. `selector: 'some-dir'`
|
|
if (meta.selector !== null) {
|
|
definitionMap.set('selector', literal(meta.selector));
|
|
}
|
|
definitionMap.set('inputs', needsNewInputPartialOutput(meta)
|
|
? createInputsPartialMetadata(meta.inputs)
|
|
: legacyInputsPartialMetadata(meta.inputs));
|
|
definitionMap.set('outputs', conditionallyCreateDirectiveBindingLiteral(meta.outputs));
|
|
definitionMap.set('host', compileHostMetadata(meta.host));
|
|
definitionMap.set('providers', meta.providers);
|
|
if (meta.queries.length > 0) {
|
|
definitionMap.set('queries', literalArr(meta.queries.map(compileQuery)));
|
|
}
|
|
if (meta.viewQueries.length > 0) {
|
|
definitionMap.set('viewQueries', literalArr(meta.viewQueries.map(compileQuery)));
|
|
}
|
|
if (meta.exportAs !== null) {
|
|
definitionMap.set('exportAs', asLiteral(meta.exportAs));
|
|
}
|
|
if (meta.usesInheritance) {
|
|
definitionMap.set('usesInheritance', literal(true));
|
|
}
|
|
if (meta.lifecycle.usesOnChanges) {
|
|
definitionMap.set('usesOnChanges', literal(true));
|
|
}
|
|
if (meta.hostDirectives?.length) {
|
|
definitionMap.set('hostDirectives', createHostDirectives(meta.hostDirectives));
|
|
}
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
return definitionMap;
|
|
}
|
|
/**
|
|
* Determines the minimum linker version for the partial output
|
|
* generated for this directive.
|
|
*
|
|
* Every time we make a breaking change to the declaration interface or partial-linker
|
|
* behavior, we must update the minimum versions to prevent old partial-linkers from
|
|
* incorrectly processing the declaration.
|
|
*
|
|
* NOTE: Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
function getMinimumVersionForPartialOutput(meta) {
|
|
// We are starting with the oldest minimum version that can work for common
|
|
// directive partial compilation output. As we discover usages of new features
|
|
// that require a newer partial output emit, we bump the `minVersion`. Our goal
|
|
// is to keep libraries as much compatible with older linker versions as possible.
|
|
let minVersion = '14.0.0';
|
|
// Note: in order to allow consuming Angular libraries that have been compiled with 16.1+ in
|
|
// Angular 16.0, we only force a minimum version of 16.1 if input transform feature as introduced
|
|
// in 16.1 is actually used.
|
|
const hasDecoratorTransformFunctions = Object.values(meta.inputs).some((input) => input.transformFunction !== null);
|
|
if (hasDecoratorTransformFunctions) {
|
|
minVersion = '16.1.0';
|
|
}
|
|
// If there are input flags and we need the new emit, use the actual minimum version,
|
|
// where this was introduced. i.e. in 17.1.0
|
|
// TODO(legacy-partial-output-inputs): Remove in v18.
|
|
if (needsNewInputPartialOutput(meta)) {
|
|
minVersion = '17.1.0';
|
|
}
|
|
// If there are signal-based queries, partial output generates an extra field
|
|
// that should be parsed by linkers. Ensure a proper minimum linker version.
|
|
if (meta.queries.some((q) => q.isSignal) || meta.viewQueries.some((q) => q.isSignal)) {
|
|
minVersion = '17.2.0';
|
|
}
|
|
return minVersion;
|
|
}
|
|
/**
|
|
* Gets whether the given directive needs the new input partial output structure
|
|
* that can hold additional metadata like `isRequired`, `isSignal` etc.
|
|
*/
|
|
function needsNewInputPartialOutput(meta) {
|
|
return Object.values(meta.inputs).some((input) => input.isSignal);
|
|
}
|
|
/**
|
|
* Compiles the metadata of a single query into its partial declaration form as declared
|
|
* by `R3DeclareQueryMetadata`.
|
|
*/
|
|
function compileQuery(query) {
|
|
const meta = new DefinitionMap();
|
|
meta.set('propertyName', literal(query.propertyName));
|
|
if (query.first) {
|
|
meta.set('first', literal(true));
|
|
}
|
|
meta.set('predicate', Array.isArray(query.predicate)
|
|
? asLiteral(query.predicate)
|
|
: convertFromMaybeForwardRefExpression(query.predicate));
|
|
if (!query.emitDistinctChangesOnly) {
|
|
// `emitDistinctChangesOnly` is special because we expect it to be `true`.
|
|
// Therefore we explicitly emit the field, and explicitly place it only when it's `false`.
|
|
meta.set('emitDistinctChangesOnly', literal(false));
|
|
}
|
|
if (query.descendants) {
|
|
meta.set('descendants', literal(true));
|
|
}
|
|
meta.set('read', query.read);
|
|
if (query.static) {
|
|
meta.set('static', literal(true));
|
|
}
|
|
if (query.isSignal) {
|
|
meta.set('isSignal', literal(true));
|
|
}
|
|
return meta.toLiteralMap();
|
|
}
|
|
/**
|
|
* Compiles the host metadata into its partial declaration form as declared
|
|
* in `R3DeclareDirectiveMetadata['host']`
|
|
*/
|
|
function compileHostMetadata(meta) {
|
|
const hostMetadata = new DefinitionMap();
|
|
hostMetadata.set('attributes', toOptionalLiteralMap(meta.attributes, (expression) => expression));
|
|
hostMetadata.set('listeners', toOptionalLiteralMap(meta.listeners, literal));
|
|
hostMetadata.set('properties', toOptionalLiteralMap(meta.properties, literal));
|
|
if (meta.specialAttributes.styleAttr) {
|
|
hostMetadata.set('styleAttribute', literal(meta.specialAttributes.styleAttr));
|
|
}
|
|
if (meta.specialAttributes.classAttr) {
|
|
hostMetadata.set('classAttribute', literal(meta.specialAttributes.classAttr));
|
|
}
|
|
if (hostMetadata.values.length > 0) {
|
|
return hostMetadata.toLiteralMap();
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
function createHostDirectives(hostDirectives) {
|
|
const expressions = hostDirectives.map((current) => {
|
|
const keys = [
|
|
{
|
|
key: 'directive',
|
|
value: current.isForwardReference
|
|
? generateForwardRef(current.directive.type)
|
|
: current.directive.type,
|
|
quoted: false,
|
|
},
|
|
];
|
|
const inputsLiteral = current.inputs ? createHostDirectivesMappingArray(current.inputs) : null;
|
|
const outputsLiteral = current.outputs
|
|
? createHostDirectivesMappingArray(current.outputs)
|
|
: null;
|
|
if (inputsLiteral) {
|
|
keys.push({ key: 'inputs', value: inputsLiteral, quoted: false });
|
|
}
|
|
if (outputsLiteral) {
|
|
keys.push({ key: 'outputs', value: outputsLiteral, quoted: false });
|
|
}
|
|
return literalMap(keys);
|
|
});
|
|
// If there's a forward reference, we generate a `function() { return [{directive: HostDir}] }`,
|
|
// otherwise we can save some bytes by using a plain array, e.g. `[{directive: HostDir}]`.
|
|
return literalArr(expressions);
|
|
}
|
|
/**
|
|
* Generates partial output metadata for inputs of a directive.
|
|
*
|
|
* The generated structure is expected to match `R3DeclareDirectiveFacade['inputs']`.
|
|
*/
|
|
function createInputsPartialMetadata(inputs) {
|
|
const keys = Object.getOwnPropertyNames(inputs);
|
|
if (keys.length === 0) {
|
|
return null;
|
|
}
|
|
return literalMap(keys.map((declaredName) => {
|
|
const value = inputs[declaredName];
|
|
return {
|
|
key: declaredName,
|
|
// put quotes around keys that contain potentially unsafe characters
|
|
quoted: UNSAFE_OBJECT_KEY_NAME_REGEXP.test(declaredName),
|
|
value: literalMap([
|
|
{ key: 'classPropertyName', quoted: false, value: asLiteral(value.classPropertyName) },
|
|
{ key: 'publicName', quoted: false, value: asLiteral(value.bindingPropertyName) },
|
|
{ key: 'isSignal', quoted: false, value: asLiteral(value.isSignal) },
|
|
{ key: 'isRequired', quoted: false, value: asLiteral(value.required) },
|
|
{ key: 'transformFunction', quoted: false, value: value.transformFunction ?? NULL_EXPR },
|
|
]),
|
|
};
|
|
}));
|
|
}
|
|
/**
|
|
* Pre v18 legacy partial output for inputs.
|
|
*
|
|
* Previously, inputs did not capture metadata like `isSignal` in the partial compilation output.
|
|
* To enable capturing such metadata, we restructured how input metadata is communicated in the
|
|
* partial output. This would make libraries incompatible with older Angular FW versions where the
|
|
* linker would not know how to handle this new "format". For this reason, if we know this metadata
|
|
* does not need to be captured- we fall back to the old format. This is what this function
|
|
* generates.
|
|
*
|
|
* See:
|
|
* https://github.com/angular/angular/blob/d4b423690210872b5c32a322a6090beda30b05a3/packages/core/src/compiler/compiler_facade_interface.ts#L197-L199
|
|
*/
|
|
function legacyInputsPartialMetadata(inputs) {
|
|
// TODO(legacy-partial-output-inputs): Remove function in v18.
|
|
const keys = Object.getOwnPropertyNames(inputs);
|
|
if (keys.length === 0) {
|
|
return null;
|
|
}
|
|
return literalMap(keys.map((declaredName) => {
|
|
const value = inputs[declaredName];
|
|
const publicName = value.bindingPropertyName;
|
|
const differentDeclaringName = publicName !== declaredName;
|
|
let result;
|
|
if (differentDeclaringName || value.transformFunction !== null) {
|
|
const values = [asLiteral(publicName), asLiteral(declaredName)];
|
|
if (value.transformFunction !== null) {
|
|
values.push(value.transformFunction);
|
|
}
|
|
result = literalArr(values);
|
|
}
|
|
else {
|
|
result = asLiteral(publicName);
|
|
}
|
|
return {
|
|
key: declaredName,
|
|
// put quotes around keys that contain potentially unsafe characters
|
|
quoted: UNSAFE_OBJECT_KEY_NAME_REGEXP.test(declaredName),
|
|
value: result,
|
|
};
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Compile a component declaration defined by the `R3ComponentMetadata`.
|
|
*/
|
|
function compileDeclareComponentFromMetadata(meta, template, additionalTemplateInfo) {
|
|
const definitionMap = createComponentDefinitionMap(meta, template, additionalTemplateInfo);
|
|
const expression = importExpr(Identifiers.declareComponent).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createComponentType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for a component into a `DefinitionMap`.
|
|
*/
|
|
function createComponentDefinitionMap(meta, template, templateInfo) {
|
|
const definitionMap = createDirectiveDefinitionMap(meta);
|
|
const blockVisitor = new BlockPresenceVisitor();
|
|
visitAll$1(blockVisitor, template.nodes);
|
|
definitionMap.set('template', getTemplateExpression(template, templateInfo));
|
|
if (templateInfo.isInline) {
|
|
definitionMap.set('isInline', literal(true));
|
|
}
|
|
// Set the minVersion to 17.0.0 if the component is using at least one block in its template.
|
|
// We don't do this for templates without blocks, in order to preserve backwards compatibility.
|
|
if (blockVisitor.hasBlocks) {
|
|
definitionMap.set('minVersion', literal('17.0.0'));
|
|
}
|
|
definitionMap.set('styles', toOptionalLiteralArray(meta.styles, literal));
|
|
definitionMap.set('dependencies', compileUsedDependenciesMetadata(meta));
|
|
definitionMap.set('viewProviders', meta.viewProviders);
|
|
definitionMap.set('animations', meta.animations);
|
|
if (meta.changeDetection !== null) {
|
|
if (typeof meta.changeDetection === 'object') {
|
|
throw new Error('Impossible state! Change detection flag is not resolved!');
|
|
}
|
|
definitionMap.set('changeDetection', importExpr(Identifiers.ChangeDetectionStrategy)
|
|
.prop(ChangeDetectionStrategy[meta.changeDetection]));
|
|
}
|
|
if (meta.encapsulation !== ViewEncapsulation$1.Emulated) {
|
|
definitionMap.set('encapsulation', importExpr(Identifiers.ViewEncapsulation).prop(ViewEncapsulation$1[meta.encapsulation]));
|
|
}
|
|
if (meta.interpolation !== DEFAULT_INTERPOLATION_CONFIG) {
|
|
definitionMap.set('interpolation', literalArr([literal(meta.interpolation.start), literal(meta.interpolation.end)]));
|
|
}
|
|
if (template.preserveWhitespaces === true) {
|
|
definitionMap.set('preserveWhitespaces', literal(true));
|
|
}
|
|
if (meta.defer.mode === 0 /* DeferBlockDepsEmitMode.PerBlock */) {
|
|
const resolvers = [];
|
|
let hasResolvers = false;
|
|
for (const deps of meta.defer.blocks.values()) {
|
|
// Note: we need to push a `null` even if there are no dependencies, because matching of
|
|
// defer resolver functions to defer blocks happens by index and not adding an array
|
|
// entry for a block can throw off the blocks coming after it.
|
|
if (deps === null) {
|
|
resolvers.push(literal(null));
|
|
}
|
|
else {
|
|
resolvers.push(deps);
|
|
hasResolvers = true;
|
|
}
|
|
}
|
|
// If *all* the resolvers are null, we can skip the field.
|
|
if (hasResolvers) {
|
|
definitionMap.set('deferBlockDependencies', literalArr(resolvers));
|
|
}
|
|
}
|
|
else {
|
|
throw new Error('Unsupported defer function emit mode in partial compilation');
|
|
}
|
|
return definitionMap;
|
|
}
|
|
function getTemplateExpression(template, templateInfo) {
|
|
// If the template has been defined using a direct literal, we use that expression directly
|
|
// without any modifications. This is ensures proper source mapping from the partially
|
|
// compiled code to the source file declaring the template. Note that this does not capture
|
|
// template literals referenced indirectly through an identifier.
|
|
if (templateInfo.inlineTemplateLiteralExpression !== null) {
|
|
return templateInfo.inlineTemplateLiteralExpression;
|
|
}
|
|
// If the template is defined inline but not through a literal, the template has been resolved
|
|
// through static interpretation. We create a literal but cannot provide any source span. Note
|
|
// that we cannot use the expression defining the template because the linker expects the template
|
|
// to be defined as a literal in the declaration.
|
|
if (templateInfo.isInline) {
|
|
return literal(templateInfo.content, null, null);
|
|
}
|
|
// The template is external so we must synthesize an expression node with
|
|
// the appropriate source-span.
|
|
const contents = templateInfo.content;
|
|
const file = new ParseSourceFile(contents, templateInfo.sourceUrl);
|
|
const start = new ParseLocation(file, 0, 0, 0);
|
|
const end = computeEndLocation(file, contents);
|
|
const span = new ParseSourceSpan(start, end);
|
|
return literal(contents, null, span);
|
|
}
|
|
function computeEndLocation(file, contents) {
|
|
const length = contents.length;
|
|
let lineStart = 0;
|
|
let lastLineStart = 0;
|
|
let line = 0;
|
|
do {
|
|
lineStart = contents.indexOf('\n', lastLineStart);
|
|
if (lineStart !== -1) {
|
|
lastLineStart = lineStart + 1;
|
|
line++;
|
|
}
|
|
} while (lineStart !== -1);
|
|
return new ParseLocation(file, length, line, length - lastLineStart);
|
|
}
|
|
function compileUsedDependenciesMetadata(meta) {
|
|
const wrapType = meta.declarationListEmitMode !== 0 /* DeclarationListEmitMode.Direct */
|
|
? generateForwardRef
|
|
: (expr) => expr;
|
|
if (meta.declarationListEmitMode === 3 /* DeclarationListEmitMode.RuntimeResolved */) {
|
|
throw new Error(`Unsupported emit mode`);
|
|
}
|
|
return toOptionalLiteralArray(meta.declarations, (decl) => {
|
|
switch (decl.kind) {
|
|
case R3TemplateDependencyKind.Directive:
|
|
const dirMeta = new DefinitionMap();
|
|
dirMeta.set('kind', literal(decl.isComponent ? 'component' : 'directive'));
|
|
dirMeta.set('type', wrapType(decl.type));
|
|
dirMeta.set('selector', literal(decl.selector));
|
|
dirMeta.set('inputs', toOptionalLiteralArray(decl.inputs, literal));
|
|
dirMeta.set('outputs', toOptionalLiteralArray(decl.outputs, literal));
|
|
dirMeta.set('exportAs', toOptionalLiteralArray(decl.exportAs, literal));
|
|
return dirMeta.toLiteralMap();
|
|
case R3TemplateDependencyKind.Pipe:
|
|
const pipeMeta = new DefinitionMap();
|
|
pipeMeta.set('kind', literal('pipe'));
|
|
pipeMeta.set('type', wrapType(decl.type));
|
|
pipeMeta.set('name', literal(decl.name));
|
|
return pipeMeta.toLiteralMap();
|
|
case R3TemplateDependencyKind.NgModule:
|
|
const ngModuleMeta = new DefinitionMap();
|
|
ngModuleMeta.set('kind', literal('ngmodule'));
|
|
ngModuleMeta.set('type', wrapType(decl.type));
|
|
return ngModuleMeta.toLiteralMap();
|
|
}
|
|
});
|
|
}
|
|
class BlockPresenceVisitor extends RecursiveVisitor$1 {
|
|
hasBlocks = false;
|
|
visitDeferredBlock() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitDeferredBlockPlaceholder() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitDeferredBlockLoading() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitDeferredBlockError() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitIfBlock() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitIfBlockBranch() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitForLoopBlock() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitForLoopBlockEmpty() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitSwitchBlock() {
|
|
this.hasBlocks = true;
|
|
}
|
|
visitSwitchBlockCase() {
|
|
this.hasBlocks = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION$4 = '12.0.0';
|
|
function compileDeclareFactoryFunction(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION$4));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', meta.type.value);
|
|
definitionMap.set('deps', compileDependencies(meta.deps));
|
|
definitionMap.set('target', importExpr(Identifiers.FactoryTarget).prop(FactoryTarget[meta.target]));
|
|
return {
|
|
expression: importExpr(Identifiers.declareFactory).callFn([definitionMap.toLiteralMap()]),
|
|
statements: [],
|
|
type: createFactoryType(meta),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION$3 = '12.0.0';
|
|
/**
|
|
* Compile a Injectable declaration defined by the `R3InjectableMetadata`.
|
|
*/
|
|
function compileDeclareInjectableFromMetadata(meta) {
|
|
const definitionMap = createInjectableDefinitionMap(meta);
|
|
const expression = importExpr(Identifiers.declareInjectable).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createInjectableType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for a Injectable into a `DefinitionMap`.
|
|
*/
|
|
function createInjectableDefinitionMap(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION$3));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', meta.type.value);
|
|
// Only generate providedIn property if it has a non-null value
|
|
if (meta.providedIn !== undefined) {
|
|
const providedIn = convertFromMaybeForwardRefExpression(meta.providedIn);
|
|
if (providedIn.value !== null) {
|
|
definitionMap.set('providedIn', providedIn);
|
|
}
|
|
}
|
|
if (meta.useClass !== undefined) {
|
|
definitionMap.set('useClass', convertFromMaybeForwardRefExpression(meta.useClass));
|
|
}
|
|
if (meta.useExisting !== undefined) {
|
|
definitionMap.set('useExisting', convertFromMaybeForwardRefExpression(meta.useExisting));
|
|
}
|
|
if (meta.useValue !== undefined) {
|
|
definitionMap.set('useValue', convertFromMaybeForwardRefExpression(meta.useValue));
|
|
}
|
|
// Factories do not contain `ForwardRef`s since any types are already wrapped in a function call
|
|
// so the types will not be eagerly evaluated. Therefore we do not need to process this expression
|
|
// with `convertFromProviderExpression()`.
|
|
if (meta.useFactory !== undefined) {
|
|
definitionMap.set('useFactory', meta.useFactory);
|
|
}
|
|
if (meta.deps !== undefined) {
|
|
definitionMap.set('deps', literalArr(meta.deps.map(compileDependency)));
|
|
}
|
|
return definitionMap;
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION$2 = '12.0.0';
|
|
function compileDeclareInjectorFromMetadata(meta) {
|
|
const definitionMap = createInjectorDefinitionMap(meta);
|
|
const expression = importExpr(Identifiers.declareInjector).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createInjectorType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for an Injector into a `DefinitionMap`.
|
|
*/
|
|
function createInjectorDefinitionMap(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION$2));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', meta.type.value);
|
|
definitionMap.set('providers', meta.providers);
|
|
if (meta.imports.length > 0) {
|
|
definitionMap.set('imports', literalArr(meta.imports));
|
|
}
|
|
return definitionMap;
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION$1 = '14.0.0';
|
|
function compileDeclareNgModuleFromMetadata(meta) {
|
|
const definitionMap = createNgModuleDefinitionMap(meta);
|
|
const expression = importExpr(Identifiers.declareNgModule).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createNgModuleType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for an NgModule into a `DefinitionMap`.
|
|
*/
|
|
function createNgModuleDefinitionMap(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
if (meta.kind === R3NgModuleMetadataKind.Local) {
|
|
throw new Error('Invalid path! Local compilation mode should not get into the partial compilation path');
|
|
}
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION$1));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
definitionMap.set('type', meta.type.value);
|
|
// We only generate the keys in the metadata if the arrays contain values.
|
|
// We must wrap the arrays inside a function if any of the values are a forward reference to a
|
|
// not-yet-declared class. This is to support JIT execution of the `ɵɵngDeclareNgModule()` call.
|
|
// In the linker these wrappers are stripped and then reapplied for the `ɵɵdefineNgModule()` call.
|
|
if (meta.bootstrap.length > 0) {
|
|
definitionMap.set('bootstrap', refsToArray(meta.bootstrap, meta.containsForwardDecls));
|
|
}
|
|
if (meta.declarations.length > 0) {
|
|
definitionMap.set('declarations', refsToArray(meta.declarations, meta.containsForwardDecls));
|
|
}
|
|
if (meta.imports.length > 0) {
|
|
definitionMap.set('imports', refsToArray(meta.imports, meta.containsForwardDecls));
|
|
}
|
|
if (meta.exports.length > 0) {
|
|
definitionMap.set('exports', refsToArray(meta.exports, meta.containsForwardDecls));
|
|
}
|
|
if (meta.schemas !== null && meta.schemas.length > 0) {
|
|
definitionMap.set('schemas', literalArr(meta.schemas.map((ref) => ref.value)));
|
|
}
|
|
if (meta.id !== null) {
|
|
definitionMap.set('id', meta.id);
|
|
}
|
|
return definitionMap;
|
|
}
|
|
|
|
/**
|
|
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
|
|
* must update this constant to prevent old partial-linkers from incorrectly processing the
|
|
* declaration.
|
|
*
|
|
* Do not include any prerelease in these versions as they are ignored.
|
|
*/
|
|
const MINIMUM_PARTIAL_LINKER_VERSION = '14.0.0';
|
|
/**
|
|
* Compile a Pipe declaration defined by the `R3PipeMetadata`.
|
|
*/
|
|
function compileDeclarePipeFromMetadata(meta) {
|
|
const definitionMap = createPipeDefinitionMap(meta);
|
|
const expression = importExpr(Identifiers.declarePipe).callFn([definitionMap.toLiteralMap()]);
|
|
const type = createPipeType(meta);
|
|
return { expression, type, statements: [] };
|
|
}
|
|
/**
|
|
* Gathers the declaration fields for a Pipe into a `DefinitionMap`.
|
|
*/
|
|
function createPipeDefinitionMap(meta) {
|
|
const definitionMap = new DefinitionMap();
|
|
definitionMap.set('minVersion', literal(MINIMUM_PARTIAL_LINKER_VERSION));
|
|
definitionMap.set('version', literal('20.3.11'));
|
|
definitionMap.set('ngImport', importExpr(Identifiers.core));
|
|
// e.g. `type: MyPipe`
|
|
definitionMap.set('type', meta.type.value);
|
|
if (meta.isStandalone !== undefined) {
|
|
definitionMap.set('isStandalone', literal(meta.isStandalone));
|
|
}
|
|
// e.g. `name: "myPipe"`
|
|
definitionMap.set('name', literal(meta.pipeName ?? meta.name));
|
|
if (meta.pure === false) {
|
|
// e.g. `pure: false`
|
|
definitionMap.set('pure', literal(meta.pure));
|
|
}
|
|
return definitionMap;
|
|
}
|
|
|
|
/**
|
|
* Generate an ngDevMode guarded call to setClassDebugInfo with the debug info about the class
|
|
* (e.g., the file name in which the class is defined)
|
|
*/
|
|
function compileClassDebugInfo(debugInfo) {
|
|
const debugInfoObject = {
|
|
className: debugInfo.className,
|
|
};
|
|
// Include file path and line number only if the file relative path is calculated successfully.
|
|
if (debugInfo.filePath) {
|
|
debugInfoObject.filePath = debugInfo.filePath;
|
|
debugInfoObject.lineNumber = debugInfo.lineNumber;
|
|
}
|
|
// Include forbidOrphanRendering only if it's set to true (to reduce generated code)
|
|
if (debugInfo.forbidOrphanRendering) {
|
|
debugInfoObject.forbidOrphanRendering = literal(true);
|
|
}
|
|
const fnCall = importExpr(Identifiers.setClassDebugInfo)
|
|
.callFn([debugInfo.type, mapLiteral(debugInfoObject)]);
|
|
const iife = arrowFn([], [devOnlyGuardedExpression(fnCall).toStmt()]);
|
|
return iife.callFn([]);
|
|
}
|
|
|
|
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
/**
|
|
* Compiles the expression that initializes HMR for a class.
|
|
* @param meta HMR metadata extracted from the class.
|
|
*/
|
|
function compileHmrInitializer(meta) {
|
|
const moduleName = 'm';
|
|
const dataName = 'd';
|
|
const timestampName = 't';
|
|
const idName = 'id';
|
|
const importCallbackName = `${meta.className}_HmrLoad`;
|
|
const namespaces = meta.namespaceDependencies.map((dep) => {
|
|
return new ExternalExpr({ moduleName: dep.moduleName, name: null });
|
|
});
|
|
// m.default
|
|
const defaultRead = variable(moduleName).prop('default');
|
|
// ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals], import.meta, id);
|
|
const replaceCall = importExpr(Identifiers.replaceMetadata)
|
|
.callFn([
|
|
meta.type,
|
|
defaultRead,
|
|
literalArr(namespaces),
|
|
literalArr(meta.localDependencies.map((l) => l.runtimeRepresentation)),
|
|
variable('import').prop('meta'),
|
|
variable(idName),
|
|
]);
|
|
// (m) => m.default && ɵɵreplaceMetadata(...)
|
|
const replaceCallback = arrowFn([new FnParam(moduleName)], defaultRead.and(replaceCall));
|
|
// getReplaceMetadataURL(id, timestamp, import.meta.url)
|
|
const url = importExpr(Identifiers.getReplaceMetadataURL)
|
|
.callFn([
|
|
variable(idName),
|
|
variable(timestampName),
|
|
variable('import').prop('meta').prop('url'),
|
|
]);
|
|
// function Cmp_HmrLoad(t) {
|
|
// import(/* @vite-ignore */ url).then((m) => m.default && replaceMetadata(...));
|
|
// }
|
|
const importCallback = new DeclareFunctionStmt(importCallbackName, [new FnParam(timestampName)], [
|
|
// The vite-ignore special comment is required to prevent Vite from generating a superfluous
|
|
// warning for each usage within the development code. If Vite provides a method to
|
|
// programmatically avoid this warning in the future, this added comment can be removed here.
|
|
new DynamicImportExpr(url, null, '@vite-ignore')
|
|
.prop('then')
|
|
.callFn([replaceCallback])
|
|
.toStmt(),
|
|
], null, StmtModifier.Final);
|
|
// (d) => d.id === id && Cmp_HmrLoad(d.timestamp)
|
|
const updateCallback = arrowFn([new FnParam(dataName)], variable(dataName)
|
|
.prop('id')
|
|
.identical(variable(idName))
|
|
.and(variable(importCallbackName).callFn([variable(dataName).prop('timestamp')])));
|
|
// Cmp_HmrLoad(Date.now());
|
|
// Initial call to kick off the loading in order to avoid edge cases with components
|
|
// coming from lazy chunks that change before the chunk has loaded.
|
|
const initialCall = variable(importCallbackName)
|
|
.callFn([variable('Date').prop('now').callFn([])]);
|
|
// import.meta.hot
|
|
const hotRead = variable('import').prop('meta').prop('hot');
|
|
// import.meta.hot.on('angular:component-update', () => ...);
|
|
const hotListener = hotRead
|
|
.clone()
|
|
.prop('on')
|
|
.callFn([literal('angular:component-update'), updateCallback]);
|
|
return arrowFn([], [
|
|
// const id = <id>;
|
|
new DeclareVarStmt(idName, literal(encodeURIComponent(`${meta.filePath}@${meta.className}`)), null, StmtModifier.Final),
|
|
// function Cmp_HmrLoad() {...}.
|
|
importCallback,
|
|
// ngDevMode && Cmp_HmrLoad(Date.now());
|
|
devOnlyGuardedExpression(initialCall).toStmt(),
|
|
// ngDevMode && import.meta.hot && import.meta.hot.on(...)
|
|
devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt(),
|
|
])
|
|
.callFn([]);
|
|
}
|
|
/**
|
|
* Compiles the HMR update callback for a class.
|
|
* @param definitions Compiled definitions for the class (e.g. `defineComponent` calls).
|
|
* @param constantStatements Supporting constants statements that were generated alongside
|
|
* the definition.
|
|
* @param meta HMR metadata extracted from the class.
|
|
*/
|
|
function compileHmrUpdateCallback(definitions, constantStatements, meta) {
|
|
const namespaces = 'ɵɵnamespaces';
|
|
const params = [meta.className, namespaces].map((name) => new FnParam(name, DYNAMIC_TYPE));
|
|
const body = [];
|
|
for (const local of meta.localDependencies) {
|
|
params.push(new FnParam(local.name));
|
|
}
|
|
// Declare variables that read out the individual namespaces.
|
|
for (let i = 0; i < meta.namespaceDependencies.length; i++) {
|
|
body.push(new DeclareVarStmt(meta.namespaceDependencies[i].assignedName, variable(namespaces).key(literal(i)), DYNAMIC_TYPE, StmtModifier.Final));
|
|
}
|
|
body.push(...constantStatements);
|
|
for (const field of definitions) {
|
|
if (field.initializer !== null) {
|
|
body.push(variable(meta.className).prop(field.name).set(field.initializer).toStmt());
|
|
for (const stmt of field.statements) {
|
|
body.push(stmt);
|
|
}
|
|
}
|
|
}
|
|
return new DeclareFunctionStmt(`${meta.className}_UpdateMetadata`, params, body, null, StmtModifier.Final);
|
|
}
|
|
|
|
/**
|
|
* @module
|
|
* @description
|
|
* Entry point for all public APIs of the compiler package.
|
|
*/
|
|
const VERSION = new Version('20.3.11');
|
|
|
|
//////////////////////////////////////
|
|
// THIS FILE HAS GLOBAL SIDE EFFECT //
|
|
// (see bottom of file) //
|
|
//////////////////////////////////////
|
|
/**
|
|
* @module
|
|
* @description
|
|
* Entry point for all APIs of the compiler package.
|
|
*
|
|
* <div class="callout is-critical">
|
|
* <header>Unstable APIs</header>
|
|
* <p>
|
|
* All compiler apis are currently considered experimental and private!
|
|
* </p>
|
|
* <p>
|
|
* We expect the APIs in this package to keep on changing. Do not rely on them.
|
|
* </p>
|
|
* </div>
|
|
*/
|
|
// This file only reexports content of the `src` folder. Keep it that way.
|
|
// This function call has a global side effects and publishes the compiler into global namespace for
|
|
// the late binding of the Compiler to the @angular/core for jit compilation.
|
|
publishFacade(_global);
|
|
|
|
export { AST, ASTWithName, ASTWithSource, AbsoluteSourceSpan, ArrayType, ArrowFunctionExpr, Attribute, Binary, BinaryOperator, BinaryOperatorExpr, BindingPipe, BindingPipeType, BindingType, Block, BlockParameter, BoundElementProperty, BuiltinType, BuiltinTypeName, CUSTOM_ELEMENTS_SCHEMA, Call, Chain, ChangeDetectionStrategy, CombinedRecursiveAstVisitor, CommaExpr, Comment, CompilerConfig, CompilerFacadeImpl, Component, Conditional, ConditionalExpr, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DYNAMIC_TYPE, DeclareFunctionStmt, DeclareVarStmt, Directive, DomElementSchemaRegistry, DynamicImportExpr, EOF, Element, ElementSchemaRegistry, EmitterVisitorContext, EmptyExpr$1 as EmptyExpr, Expansion, ExpansionCase, Expression, ExpressionBinding, ExpressionStatement, ExpressionType, ExternalExpr, ExternalReference, FactoryTarget, FunctionExpr, HtmlParser, HtmlTagDefinition, I18NHtmlParser, IfStmt, ImplicitReceiver, InstantiateExpr, Interpolation$1 as Interpolation, InterpolationConfig, InvokeFunctionExpr, JSDocComment, JitEvaluator, KeyedRead, LeadingComment, LetDeclaration, Lexer, LiteralArray, LiteralArrayExpr, LiteralExpr, LiteralMap, LiteralMapExpr, LiteralPrimitive, LocalizedString, MapType, MessageBundle, NONE_TYPE, NO_ERRORS_SCHEMA, NodeWithI18n, NonNullAssert, NotExpr, ParenthesizedExpr, ParenthesizedExpression, ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan, ParseSpan, ParseTreeResult, ParsedEvent, ParsedEventType, ParsedProperty, ParsedPropertyType, ParsedVariable, Parser, PrefixNot, PropertyRead, Identifiers as R3Identifiers, R3NgModuleMetadataKind, R3SelectorScopeMode, R3TargetBinder, R3TemplateDependencyKind, ReadKeyExpr, ReadPropExpr, ReadVarExpr, RecursiveAstVisitor, RecursiveVisitor, ResourceLoader, ReturnStatement, SCHEMA, SECURITY_SCHEMA, STRING_TYPE, SafeCall, SafeKeyedRead, SafePropertyRead, SelectorContext, SelectorListContext, SelectorMatcher, SelectorlessMatcher, Serializer, SplitInterpolation, Statement, StmtModifier, StringToken, StringTokenKind, TagContentType, TaggedTemplateLiteral, TaggedTemplateLiteralExpr, TemplateBindingParseResult, TemplateLiteral, TemplateLiteralElement, TemplateLiteralElementExpr, TemplateLiteralExpr, Text, ThisReceiver, BlockNode as TmplAstBlockNode, BoundAttribute as TmplAstBoundAttribute, BoundDeferredTrigger as TmplAstBoundDeferredTrigger, BoundEvent as TmplAstBoundEvent, BoundText as TmplAstBoundText, Component$1 as TmplAstComponent, Content as TmplAstContent, DeferredBlock as TmplAstDeferredBlock, DeferredBlockError as TmplAstDeferredBlockError, DeferredBlockLoading as TmplAstDeferredBlockLoading, DeferredBlockPlaceholder as TmplAstDeferredBlockPlaceholder, DeferredTrigger as TmplAstDeferredTrigger, Directive$1 as TmplAstDirective, Element$1 as TmplAstElement, ForLoopBlock as TmplAstForLoopBlock, ForLoopBlockEmpty as TmplAstForLoopBlockEmpty, HostElement as TmplAstHostElement, HoverDeferredTrigger as TmplAstHoverDeferredTrigger, Icu$1 as TmplAstIcu, IdleDeferredTrigger as TmplAstIdleDeferredTrigger, IfBlock as TmplAstIfBlock, IfBlockBranch as TmplAstIfBlockBranch, ImmediateDeferredTrigger as TmplAstImmediateDeferredTrigger, InteractionDeferredTrigger as TmplAstInteractionDeferredTrigger, LetDeclaration$1 as TmplAstLetDeclaration, NeverDeferredTrigger as TmplAstNeverDeferredTrigger, RecursiveVisitor$1 as TmplAstRecursiveVisitor, Reference as TmplAstReference, SwitchBlock as TmplAstSwitchBlock, SwitchBlockCase as TmplAstSwitchBlockCase, Template as TmplAstTemplate, Text$3 as TmplAstText, TextAttribute as TmplAstTextAttribute, TimerDeferredTrigger as TmplAstTimerDeferredTrigger, UnknownBlock as TmplAstUnknownBlock, Variable as TmplAstVariable, ViewportDeferredTrigger as TmplAstViewportDeferredTrigger, Token, TokenType, TransplantedType, TreeError, Type, TypeModifier, TypeofExpr, TypeofExpression, Unary, UnaryOperator, UnaryOperatorExpr, VERSION, VariableBinding, Version, ViewEncapsulation$1 as ViewEncapsulation, VoidExpr, VoidExpression, WrappedNodeExpr, Xliff, Xliff2, Xmb, XmlParser, Xtb, _ATTR_TO_PROP, compileClassDebugInfo, compileClassMetadata, compileComponentClassMetadata, compileComponentDeclareClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, compileDeclareDirectiveFromMetadata, compileDeclareFactoryFunction, compileDeclareInjectableFromMetadata, compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileDeclarePipeFromMetadata, compileDeferResolverFunction, compileDirectiveFromMetadata, compileFactoryFunction, compileHmrInitializer, compileHmrUpdateCallback, compileInjectable, compileInjector, compileNgModule, compileOpaqueAsyncClassMetadata, compilePipeFromMetadata, computeMsgId, core, createCssSelectorFromNode, createInjectableType, createMayBeForwardRefExpression, devOnlyGuardedExpression, emitDistinctChangesOnlyDefaultValue, encapsulateStyle, escapeRegExp, findMatchingDirectivesAndPipes, getHtmlTagDefinition, getNsPrefix, getSafePropertyAccessString, identifierName, isNgContainer, isNgContent, isNgTemplate, jsDocComment, leadingComment, literal, literalMap, makeBindingParser, mergeNsAndName, output_ast as outputAst, parseHostBindings, parseTemplate, preserveWhitespacesDefault, publishFacade, r3JitTypeSourceSpan, sanitizeIdentifier, setEnableTemplateSourceLocations, splitNsName, visitAll$1 as tmplAstVisitAll, verifyHostBindings, visitAll };
|
|
//# sourceMappingURL=compiler.mjs.map
|