UNPKG

@angular/core

Version:

Angular - the core framework

1,199 lines 183 kB
/** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @license * Copyright Google Inc. 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.io/license */ import '../util/ng_i18n_closure_mode'; import { getPluralCase } from '../i18n/localization'; import { SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent } from '../sanitization/html_sanitizer'; import { InertBodyHelper } from '../sanitization/inert_body'; import { _sanitizeUrl, sanitizeSrcset } from '../sanitization/url_sanitizer'; import { addAllToArray } from '../util/array_utils'; import { assertDataInRange, assertDefined, assertEqual, assertGreaterThan } from '../util/assert'; import { attachPatchData } from './context_discovery'; import { bind, setDelayProjection, ɵɵload } from './instructions/all'; import { attachI18nOpCodesDebug } from './instructions/lview_debug'; import { allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, textBindingInternal } from './instructions/shared'; import { NATIVE } from './interfaces/container'; import { COMMENT_MARKER, ELEMENT_MARKER } from './interfaces/i18n'; import { BINDING_INDEX, HEADER_OFFSET, RENDERER, TVIEW, T_HOST } from './interfaces/view'; import { appendChild, appendProjectedNodes, createTextNode, nativeRemoveNode } from './node_manipulation'; import { getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode } from './state'; import { NO_CHANGE } from './tokens'; import { renderStringify } from './util/misc_utils'; import { findComponentView } from './util/view_traversal_utils'; import { getNativeByIndex, getNativeByTNode, getTNode, isLContainer } from './util/view_utils'; /** @type {?} */ const MARKER = `�`; /** @type {?} */ const ICU_BLOCK_REGEXP = /^\s*(�\d+:?\d*�)\s*,\s*(select|plural)\s*,/; /** @type {?} */ const SUBTEMPLATE_REGEXP = /�\/?\*(\d+:\d+)�/gi; /** @type {?} */ const PH_REGEXP = /�(\/?[#*!]\d+):?\d*�/gi; /** @type {?} */ const BINDING_REGEXP = /�(\d+):?\d*�/gi; /** @type {?} */ const ICU_REGEXP = /({\s*�\d+:?\d*�\s*,\s*\S{6}\s*,[\s\S]*})/gi; /** @enum {string} */ const TagType = { ELEMENT: '#', TEMPLATE: '*', PROJECTION: '!', }; // i18nPostprocess consts /** @type {?} */ const ROOT_TEMPLATE_ID = 0; /** @type {?} */ const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]/; /** @type {?} */ const PP_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]|(�\/?\*\d+:\d+�)/g; /** @type {?} */ const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g; /** @type {?} */ const PP_ICUS_REGEXP = /�I18N_EXP_(ICU(_\d+)?)�/g; /** @type {?} */ const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/; /** @type {?} */ const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/; /** * @record */ function IcuExpression() { } if (false) { /** @type {?} */ IcuExpression.prototype.type; /** @type {?} */ IcuExpression.prototype.mainBinding; /** @type {?} */ IcuExpression.prototype.cases; /** @type {?} */ IcuExpression.prototype.values; } /** * @record */ function IcuCase() { } if (false) { /** * Number of slots to allocate in expando for this case. * * This is the max number of DOM elements which will be created by this i18n + ICU blocks. When * the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can * write into them. * @type {?} */ IcuCase.prototype.vars; /** * An optional array of child/sub ICUs. * @type {?} */ IcuCase.prototype.childIcus; /** * A set of OpCodes to apply in order to build up the DOM render tree for the ICU * @type {?} */ IcuCase.prototype.create; /** * A set of OpCodes to apply in order to destroy the DOM render tree for the ICU. * @type {?} */ IcuCase.prototype.remove; /** * A set of OpCodes to apply in order to update the DOM render tree for the ICU bindings. * @type {?} */ IcuCase.prototype.update; } /** * Breaks pattern into strings and top level {...} blocks. * Can be used to break a message into text and ICU expressions, or to break an ICU expression into * keys and cases. * Original code from closure library, modified for Angular. * * @param {?} pattern (sub)Pattern to be broken. * * @return {?} */ function extractParts(pattern) { if (!pattern) { return []; } /** @type {?} */ let prevPos = 0; /** @type {?} */ const braceStack = []; /** @type {?} */ const results = []; /** @type {?} */ const braces = /[{}]/g; // lastIndex doesn't get set to 0 so we have to. braces.lastIndex = 0; /** @type {?} */ let match; while (match = braces.exec(pattern)) { /** @type {?} */ const pos = match.index; if (match[0] == '}') { braceStack.pop(); if (braceStack.length == 0) { // End of the block. /** @type {?} */ const block = pattern.substring(prevPos, pos); if (ICU_BLOCK_REGEXP.test(block)) { results.push(parseICUBlock(block)); } else { results.push(block); } prevPos = pos + 1; } } else { if (braceStack.length == 0) { /** @type {?} */ const substring = pattern.substring(prevPos, pos); results.push(substring); prevPos = pos + 1; } braceStack.push('{'); } } /** @type {?} */ const substring = pattern.substring(prevPos); results.push(substring); return results; } /** * Parses text containing an ICU expression and produces a JSON object for it. * Original code from closure library, modified for Angular. * * @param {?} pattern Text containing an ICU expression that needs to be parsed. * * @return {?} */ function parseICUBlock(pattern) { /** @type {?} */ const cases = []; /** @type {?} */ const values = []; /** @type {?} */ let icuType = 1 /* plural */; /** @type {?} */ let mainBinding = 0; pattern = pattern.replace(ICU_BLOCK_REGEXP, (/** * @param {?} str * @param {?} binding * @param {?} type * @return {?} */ function (str, binding, type) { if (type === 'select') { icuType = 0 /* select */; } else { icuType = 1 /* plural */; } mainBinding = parseInt(binding.substr(1), 10); return ''; })); /** @type {?} */ const parts = (/** @type {?} */ (extractParts(pattern))); // Looking for (key block)+ sequence. One of the keys has to be "other". for (let pos = 0; pos < parts.length;) { /** @type {?} */ let key = parts[pos++].trim(); if (icuType === 1 /* plural */) { // Key can be "=x", we just want "x" key = key.replace(/\s*(?:=)?(\w+)\s*/, '$1'); } if (key.length) { cases.push(key); } /** @type {?} */ const blocks = (/** @type {?} */ (extractParts(parts[pos++]))); if (cases.length > values.length) { values.push(blocks); } } assertGreaterThan(cases.indexOf('other'), -1, 'Missing key "other" in ICU statement.'); // TODO(ocombe): support ICU expressions in attributes, see #21615 return { type: icuType, mainBinding: mainBinding, cases, values }; } /** * Removes everything inside the sub-templates of a message. * @param {?} message * @return {?} */ function removeInnerTemplateTranslation(message) { /** @type {?} */ let match; /** @type {?} */ let res = ''; /** @type {?} */ let index = 0; /** @type {?} */ let inTemplate = false; /** @type {?} */ let tagMatched; while ((match = SUBTEMPLATE_REGEXP.exec(message)) !== null) { if (!inTemplate) { res += message.substring(index, match.index + match[0].length); tagMatched = match[1]; inTemplate = true; } else { if (match[0] === `${MARKER}/*${tagMatched}${MARKER}`) { index = match.index; inTemplate = false; } } } ngDevMode && assertEqual(inTemplate, false, `Tag mismatch: unable to find the end of the sub-template in the translation "${message}"`); res += message.substr(index); return res; } /** * Extracts a part of a message and removes the rest. * * This method is used for extracting a part of the message associated with a template. A translated * message can span multiple templates. * * Example: * ``` * <div i18n>Translate <span *ngIf>me</span>!</div> * ``` * * @param {?} message The message to crop * @param {?=} subTemplateIndex Index of the sub-template to extract. If undefined it returns the * external template and removes all sub-templates. * @return {?} */ export function getTranslationForTemplate(message, subTemplateIndex) { if (typeof subTemplateIndex !== 'number') { // We want the root template message, ignore all sub-templates return removeInnerTemplateTranslation(message); } else { // We want a specific sub-template /** @type {?} */ const start = message.indexOf(`:${subTemplateIndex}${MARKER}`) + 2 + subTemplateIndex.toString().length; /** @type {?} */ const end = message.search(new RegExp(`${MARKER}\\/\\*\\d+:${subTemplateIndex}${MARKER}`)); return removeInnerTemplateTranslation(message.substring(start, end)); } } /** * Generate the OpCodes to update the bindings of a string. * * @param {?} str The string containing the bindings. * @param {?} destinationNode Index of the destination node which will receive the binding. * @param {?=} attrName Name of the attribute, if the string belongs to an attribute. * @param {?=} sanitizeFn Sanitization function used to sanitize the string after update, if necessary. * @return {?} */ function generateBindingUpdateOpCodes(str, destinationNode, attrName, sanitizeFn = null) { /** @type {?} */ const updateOpCodes = [null, null]; // Alloc space for mask and size /** @type {?} */ const textParts = str.split(BINDING_REGEXP); /** @type {?} */ let mask = 0; for (let j = 0; j < textParts.length; j++) { /** @type {?} */ const textValue = textParts[j]; if (j & 1) { // Odd indexes are bindings /** @type {?} */ const bindingIndex = parseInt(textValue, 10); updateOpCodes.push(-1 - bindingIndex); mask = mask | toMaskBit(bindingIndex); } else if (textValue !== '') { // Even indexes are text updateOpCodes.push(textValue); } } updateOpCodes.push(destinationNode << 2 /* SHIFT_REF */ | (attrName ? 1 /* Attr */ : 0 /* Text */)); if (attrName) { updateOpCodes.push(attrName, sanitizeFn); } updateOpCodes[0] = mask; updateOpCodes[1] = updateOpCodes.length - 2; return updateOpCodes; } /** * @param {?} icuExpression * @param {?=} mask * @return {?} */ function getBindingMask(icuExpression, mask = 0) { mask = mask | toMaskBit(icuExpression.mainBinding); /** @type {?} */ let match; for (let i = 0; i < icuExpression.values.length; i++) { /** @type {?} */ const valueArr = icuExpression.values[i]; for (let j = 0; j < valueArr.length; j++) { /** @type {?} */ const value = valueArr[j]; if (typeof value === 'string') { while (match = BINDING_REGEXP.exec(value)) { mask = mask | toMaskBit(parseInt(match[1], 10)); } } else { mask = getBindingMask((/** @type {?} */ (value)), mask); } } } return mask; } /** @type {?} */ const i18nIndexStack = []; /** @type {?} */ let i18nIndexStackPointer = -1; /** * Convert binding index to mask bit. * * Each index represents a single bit on the bit-mask. Because bit-mask only has 32 bits, we make * the 32nd bit share all masks for all bindings higher than 32. Since it is extremely rare to have * more than 32 bindings this will be hit very rarely. The downside of hitting this corner case is * that we will execute binding code more often than necessary. (penalty of performance) * @param {?} bindingIndex * @return {?} */ function toMaskBit(bindingIndex) { return 1 << Math.min(bindingIndex, 31); } /** @type {?} */ const parentIndexStack = []; /** * Marks a block of text as translatable. * * The instructions `i18nStart` and `i18nEnd` mark the translation block in the template. * The translation `message` is the value which is locale specific. The translation string may * contain placeholders which associate inner elements and sub-templates within the translation. * * The translation `message` placeholders are: * - `�{index}(:{block})�`: *Binding Placeholder*: Marks a location where an expression will be * interpolated into. The placeholder `index` points to the expression binding index. An optional * `block` that matches the sub-template in which it was declared. * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *Element Placeholder*: Marks the beginning * and end of DOM element that were embedded in the original translation block. The placeholder * `index` points to the element index in the template instructions set. An optional `block` that * matches the sub-template in which it was declared. * - `�!{index}(:{block})�`/`�/!{index}(:{block})�`: *Projection Placeholder*: Marks the * beginning and end of <ng-content> that was embedded in the original translation block. * The placeholder `index` points to the element index in the template instructions set. * An optional `block` that matches the sub-template in which it was declared. * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *Sub-template Placeholder*: Sub-templates must be * split up and translated separately in each angular template function. The `index` points to the * `template` instruction index. A `block` that matches the sub-template in which it was declared. * * \@codeGenApi * @param {?} index A unique index of the translation in the static block. * @param {?} message The translation message. * @param {?=} subTemplateIndex Optional sub-template index in the `message`. * * @return {?} */ export function ɵɵi18nStart(index, message, subTemplateIndex) { /** @type {?} */ const tView = getLView()[TVIEW]; ngDevMode && assertDefined(tView, `tView should be defined`); i18nIndexStack[++i18nIndexStackPointer] = index; // We need to delay projections until `i18nEnd` setDelayProjection(true); if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { i18nStartFirstPass(tView, index, message, subTemplateIndex); } } // Count for the number of vars that will be allocated for each i18n block. // It is global because this is used in multiple functions that include loops and recursive calls. // This is reset to 0 when `i18nStartFirstPass` is called. /** @type {?} */ let i18nVarsCount; /** * See `i18nStart` above. * @param {?} tView * @param {?} index * @param {?} message * @param {?=} subTemplateIndex * @return {?} */ function i18nStartFirstPass(tView, index, message, subTemplateIndex) { /** @type {?} */ const viewData = getLView(); /** @type {?} */ const startIndex = tView.blueprint.length - HEADER_OFFSET; i18nVarsCount = 0; /** @type {?} */ const previousOrParentTNode = getPreviousOrParentTNode(); /** @type {?} */ const parentTNode = getIsParent() ? getPreviousOrParentTNode() : previousOrParentTNode && previousOrParentTNode.parent; /** @type {?} */ let parentIndex = parentTNode && parentTNode !== viewData[T_HOST] ? parentTNode.index - HEADER_OFFSET : index; /** @type {?} */ let parentIndexPointer = 0; parentIndexStack[parentIndexPointer] = parentIndex; /** @type {?} */ const createOpCodes = []; // If the previous node wasn't the direct parent then we have a translation without top level // element and we need to keep a reference of the previous element if there is one if (index > 0 && previousOrParentTNode !== parentTNode) { // Create an OpCode to select the previous TNode createOpCodes.push(previousOrParentTNode.index << 3 /* SHIFT_REF */ | 0 /* Select */); } /** @type {?} */ const updateOpCodes = []; /** @type {?} */ const icuExpressions = []; /** @type {?} */ const templateTranslation = getTranslationForTemplate(message, subTemplateIndex); /** @type {?} */ const msgParts = templateTranslation.split(PH_REGEXP); for (let i = 0; i < msgParts.length; i++) { /** @type {?} */ let value = msgParts[i]; if (i & 1) { // Odd indexes are placeholders (elements and sub-templates) if (value.charAt(0) === '/') { // It is a closing tag if (value.charAt(1) === "#" /* ELEMENT */) { /** @type {?} */ const phIndex = parseInt(value.substr(2), 10); parentIndex = parentIndexStack[--parentIndexPointer]; createOpCodes.push(phIndex << 3 /* SHIFT_REF */ | 5 /* ElementEnd */); } } else { /** @type {?} */ const phIndex = parseInt(value.substr(1), 10); // The value represents a placeholder that we move to the designated index createOpCodes.push(phIndex << 3 /* SHIFT_REF */ | 0 /* Select */, parentIndex << 17 /* SHIFT_PARENT */ | 1 /* AppendChild */); if (value.charAt(0) === "#" /* ELEMENT */) { parentIndexStack[++parentIndexPointer] = parentIndex = phIndex; } } } else { // Even indexes are text (including bindings & ICU expressions) /** @type {?} */ const parts = extractParts(value); for (let j = 0; j < parts.length; j++) { if (j & 1) { // Odd indexes are ICU expressions // Create the comment node that will anchor the ICU expression /** @type {?} */ const icuNodeIndex = startIndex + i18nVarsCount++; createOpCodes.push(COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex, parentIndex << 17 /* SHIFT_PARENT */ | 1 /* AppendChild */); // Update codes for the ICU expression /** @type {?} */ const icuExpression = (/** @type {?} */ (parts[j])); /** @type {?} */ const mask = getBindingMask(icuExpression); icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex); // Since this is recursive, the last TIcu that was pushed is the one we want /** @type {?} */ const tIcuIndex = icuExpressions.length - 1; updateOpCodes.push(toMaskBit(icuExpression.mainBinding), // mask of the main binding 3, // skip 3 opCodes if not changed -1 - icuExpression.mainBinding, icuNodeIndex << 2 /* SHIFT_REF */ | 2 /* IcuSwitch */, tIcuIndex, mask, // mask of all the bindings of this ICU expression 2, // skip 2 opCodes if not changed icuNodeIndex << 2 /* SHIFT_REF */ | 3 /* IcuUpdate */, tIcuIndex); } else if (parts[j] !== '') { /** @type {?} */ const text = (/** @type {?} */ (parts[j])); // Even indexes are text (including bindings) /** @type {?} */ const hasBinding = text.match(BINDING_REGEXP); // Create text nodes /** @type {?} */ const textNodeIndex = startIndex + i18nVarsCount++; createOpCodes.push( // If there is a binding, the value will be set during update hasBinding ? '' : text, textNodeIndex, parentIndex << 17 /* SHIFT_PARENT */ | 1 /* AppendChild */); if (hasBinding) { addAllToArray(generateBindingUpdateOpCodes(text, textNodeIndex), updateOpCodes); } } } } } allocExpando(viewData, i18nVarsCount); ngDevMode && attachI18nOpCodesDebug(createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, viewData); // NOTE: local var needed to properly assert the type of `TI18n`. /** @type {?} */ const tI18n = { vars: i18nVarsCount, create: createOpCodes, update: updateOpCodes, icus: icuExpressions.length ? icuExpressions : null, }; tView.data[index + HEADER_OFFSET] = tI18n; } /** * @param {?} tNode * @param {?} parentTNode * @param {?} previousTNode * @param {?} viewData * @return {?} */ function appendI18nNode(tNode, parentTNode, previousTNode, viewData) { ngDevMode && ngDevMode.rendererMoveNode++; /** @type {?} */ const nextNode = tNode.next; if (!previousTNode) { previousTNode = parentTNode; } // Re-organize node tree to put this node in the correct position. if (previousTNode === parentTNode && tNode !== parentTNode.child) { tNode.next = parentTNode.child; parentTNode.child = tNode; } else if (previousTNode !== parentTNode && tNode !== previousTNode.next) { tNode.next = previousTNode.next; previousTNode.next = tNode; } else { tNode.next = null; } if (parentTNode !== viewData[T_HOST]) { tNode.parent = (/** @type {?} */ (parentTNode)); } // If tNode was moved around, we might need to fix a broken link. /** @type {?} */ let cursor = tNode.next; while (cursor) { if (cursor.next === tNode) { cursor.next = nextNode; } cursor = cursor.next; } // If the placeholder to append is a projection, we need to move the projected nodes instead if (tNode.type === 1 /* Projection */) { /** @type {?} */ const tProjectionNode = (/** @type {?} */ (tNode)); appendProjectedNodes(viewData, tProjectionNode, tProjectionNode.projection, findComponentView(viewData)); return tNode; } appendChild(getNativeByTNode(tNode, viewData), tNode, viewData); /** @type {?} */ const slotValue = viewData[tNode.index]; if (tNode.type !== 0 /* Container */ && isLContainer(slotValue)) { // Nodes that inject ViewContainerRef also have a comment node that should be moved appendChild(slotValue[NATIVE], tNode, viewData); } return tNode; } /** * Handles message string post-processing for internationalization. * * Handles message string post-processing by transforming it from intermediate * format (that might contain some markers that we need to replace) to the final * form, consumable by i18nStart instruction. Post processing steps include: * * 1. Resolve all multi-value cases (like [�*1:1��#2:1�|�#4:1�|�5�]) * 2. Replace all ICU vars (like "VAR_PLURAL") * 3. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) * in case multiple ICUs have the same placeholder name * * \@codeGenApi * @param {?} message Raw translation string for post processing * @param {?=} replacements Set of replacements that should be applied * * @return {?} Transformed string that can be consumed by i18nStart instruction * */ export function ɵɵi18nPostprocess(message, replacements = {}) { /** * Step 1: resolve all multi-value placeholders like [�#5�|�*1:1��#2:1�|�#4:1�] * * Note: due to the way we process nested templates (BFS), multi-value placeholders are typically * grouped by templates, for example: [�#5�|�#6�|�#1:1�|�#3:2�] where �#5� and �#6� belong to root * template, �#1:1� belong to nested template with index 1 and �#1:2� - nested template with index * 3. However in real templates the order might be different: i.e. �#1:1� and/or �#3:2� may go in * front of �#6�. The post processing step restores the right order by keeping track of the * template id stack and looks for placeholders that belong to the currently active template. * @type {?} */ let result = message; if (PP_MULTI_VALUE_PLACEHOLDERS_REGEXP.test(message)) { /** @type {?} */ const matches = {}; /** @type {?} */ const templateIdsStack = [ROOT_TEMPLATE_ID]; result = result.replace(PP_PLACEHOLDERS_REGEXP, (/** * @param {?} m * @param {?} phs * @param {?} tmpl * @return {?} */ (m, phs, tmpl) => { /** @type {?} */ const content = phs || tmpl; /** @type {?} */ const placeholders = matches[content] || []; if (!placeholders.length) { content.split('|').forEach((/** * @param {?} placeholder * @return {?} */ (placeholder) => { /** @type {?} */ const match = placeholder.match(PP_TEMPLATE_ID_REGEXP); /** @type {?} */ const templateId = match ? parseInt(match[1], 10) : ROOT_TEMPLATE_ID; /** @type {?} */ const isCloseTemplateTag = PP_CLOSE_TEMPLATE_REGEXP.test(placeholder); placeholders.push([templateId, isCloseTemplateTag, placeholder]); })); matches[content] = placeholders; } if (!placeholders.length) { throw new Error(`i18n postprocess: unmatched placeholder - ${content}`); } /** @type {?} */ const currentTemplateId = templateIdsStack[templateIdsStack.length - 1]; /** @type {?} */ let idx = 0; // find placeholder index that matches current template id for (let i = 0; i < placeholders.length; i++) { if (placeholders[i][0] === currentTemplateId) { idx = i; break; } } // update template id stack based on the current tag extracted const [templateId, isCloseTemplateTag, placeholder] = placeholders[idx]; if (isCloseTemplateTag) { templateIdsStack.pop(); } else if (currentTemplateId !== templateId) { templateIdsStack.push(templateId); } // remove processed tag from the list placeholders.splice(idx, 1); return placeholder; })); } // return current result if no replacements specified if (!Object.keys(replacements).length) { return result; } /** * Step 2: replace all ICU vars (like "VAR_PLURAL") */ result = result.replace(PP_ICU_VARS_REGEXP, (/** * @param {?} match * @param {?} start * @param {?} key * @param {?} _type * @param {?} _idx * @param {?} end * @return {?} */ (match, start, key, _type, _idx, end) => { return replacements.hasOwnProperty(key) ? `${start}${replacements[key]}${end}` : match; })); /** * Step 3: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) in case * multiple ICUs have the same placeholder name */ result = result.replace(PP_ICUS_REGEXP, (/** * @param {?} match * @param {?} key * @return {?} */ (match, key) => { if (replacements.hasOwnProperty(key)) { /** @type {?} */ const list = (/** @type {?} */ (replacements[key])); if (!list.length) { throw new Error(`i18n postprocess: unmatched ICU - ${match} with key: ${key}`); } return (/** @type {?} */ (list.shift())); } return match; })); return result; } /** * Translates a translation block marked by `i18nStart` and `i18nEnd`. It inserts the text/ICU nodes * into the render tree, moves the placeholder nodes and removes the deleted nodes. * * \@codeGenApi * @return {?} */ export function ɵɵi18nEnd() { /** @type {?} */ const tView = getLView()[TVIEW]; ngDevMode && assertDefined(tView, `tView should be defined`); i18nEndFirstPass(tView); // Stop delaying projections setDelayProjection(false); } /** * See `i18nEnd` above. * @param {?} tView * @return {?} */ function i18nEndFirstPass(tView) { /** @type {?} */ const viewData = getLView(); ngDevMode && assertEqual(viewData[BINDING_INDEX], viewData[TVIEW].bindingStartIndex, 'i18nEnd should be called before any binding'); /** @type {?} */ const rootIndex = i18nIndexStack[i18nIndexStackPointer--]; /** @type {?} */ const tI18n = (/** @type {?} */ (tView.data[rootIndex + HEADER_OFFSET])); ngDevMode && assertDefined(tI18n, `You should call i18nStart before i18nEnd`); // Find the last node that was added before `i18nEnd` /** @type {?} */ let lastCreatedNode = getPreviousOrParentTNode(); // Read the instructions to insert/move/remove DOM elements /** @type {?} */ const visitedNodes = readCreateOpCodes(rootIndex, tI18n.create, tI18n.icus, viewData); // Remove deleted nodes for (let i = rootIndex + 1; i <= lastCreatedNode.index - HEADER_OFFSET; i++) { if (visitedNodes.indexOf(i) === -1) { removeNode(i, viewData); } } } /** * Creates and stores the dynamic TNode, and unhooks it from the tree for now. * @param {?} lView * @param {?} index * @param {?} type * @param {?} native * @param {?} name * @return {?} */ function createDynamicNodeAtIndex(lView, index, type, native, name) { /** @type {?} */ const previousOrParentTNode = getPreviousOrParentTNode(); ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); lView[index + HEADER_OFFSET] = native; /** @type {?} */ const tNode = getOrCreateTNode(lView[TVIEW], lView[T_HOST], index, (/** @type {?} */ (type)), name, null); // We are creating a dynamic node, the previous tNode might not be pointing at this node. // We will link ourselves into the tree later with `appendI18nNode`. if (previousOrParentTNode.next === tNode) { previousOrParentTNode.next = null; } return tNode; } /** * @param {?} index * @param {?} createOpCodes * @param {?} icus * @param {?} viewData * @return {?} */ function readCreateOpCodes(index, createOpCodes, icus, viewData) { /** @type {?} */ const renderer = getLView()[RENDERER]; /** @type {?} */ let currentTNode = null; /** @type {?} */ let previousTNode = null; /** @type {?} */ const visitedNodes = []; for (let i = 0; i < createOpCodes.length; i++) { /** @type {?} */ const opCode = createOpCodes[i]; if (typeof opCode == 'string') { /** @type {?} */ const textRNode = createTextNode(opCode, renderer); /** @type {?} */ const textNodeIndex = (/** @type {?} */ (createOpCodes[++i])); ngDevMode && ngDevMode.rendererCreateTextNode++; previousTNode = currentTNode; currentTNode = createDynamicNodeAtIndex(viewData, textNodeIndex, 3 /* Element */, textRNode, null); visitedNodes.push(textNodeIndex); setIsNotParent(); } else if (typeof opCode == 'number') { switch (opCode & 7 /* MASK_OPCODE */) { case 1 /* AppendChild */: /** @type {?} */ const destinationNodeIndex = opCode >>> 17 /* SHIFT_PARENT */; /** @type {?} */ let destinationTNode; if (destinationNodeIndex === index) { // If the destination node is `i18nStart`, we don't have a // top-level node and we should use the host node instead destinationTNode = (/** @type {?} */ (viewData[T_HOST])); } else { destinationTNode = getTNode(destinationNodeIndex, viewData); } ngDevMode && assertDefined((/** @type {?} */ (currentTNode)), `You need to create or select a node before you can insert it into the DOM`); previousTNode = appendI18nNode((/** @type {?} */ (currentTNode)), destinationTNode, previousTNode, viewData); break; case 0 /* Select */: /** @type {?} */ const nodeIndex = opCode >>> 3 /* SHIFT_REF */; visitedNodes.push(nodeIndex); previousTNode = currentTNode; currentTNode = getTNode(nodeIndex, viewData); if (currentTNode) { setPreviousOrParentTNode(currentTNode, currentTNode.type === 3 /* Element */); } break; case 5 /* ElementEnd */: /** @type {?} */ const elementIndex = opCode >>> 3 /* SHIFT_REF */; previousTNode = currentTNode = getTNode(elementIndex, viewData); setPreviousOrParentTNode(currentTNode, false); break; case 4 /* Attr */: /** @type {?} */ const elementNodeIndex = opCode >>> 3 /* SHIFT_REF */; /** @type {?} */ const attrName = (/** @type {?} */ (createOpCodes[++i])); /** @type {?} */ const attrValue = (/** @type {?} */ (createOpCodes[++i])); // This code is used for ICU expressions only, since we don't support // directives/components in ICUs, we don't need to worry about inputs here elementAttributeInternal(elementNodeIndex, attrName, attrValue, viewData); break; default: throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); } } else { switch (opCode) { case COMMENT_MARKER: /** @type {?} */ const commentValue = (/** @type {?} */ (createOpCodes[++i])); /** @type {?} */ const commentNodeIndex = (/** @type {?} */ (createOpCodes[++i])); ngDevMode && assertEqual(typeof commentValue, 'string', `Expected "${commentValue}" to be a comment node value`); /** @type {?} */ const commentRNode = renderer.createComment(commentValue); ngDevMode && ngDevMode.rendererCreateComment++; previousTNode = currentTNode; currentTNode = createDynamicNodeAtIndex(viewData, commentNodeIndex, 5 /* IcuContainer */, commentRNode, null); visitedNodes.push(commentNodeIndex); attachPatchData(commentRNode, viewData); ((/** @type {?} */ (currentTNode))).activeCaseIndex = null; // We will add the case nodes later, during the update phase setIsNotParent(); break; case ELEMENT_MARKER: /** @type {?} */ const tagNameValue = (/** @type {?} */ (createOpCodes[++i])); /** @type {?} */ const elementNodeIndex = (/** @type {?} */ (createOpCodes[++i])); ngDevMode && assertEqual(typeof tagNameValue, 'string', `Expected "${tagNameValue}" to be an element node tag name`); /** @type {?} */ const elementRNode = renderer.createElement(tagNameValue); ngDevMode && ngDevMode.rendererCreateElement++; previousTNode = currentTNode; currentTNode = createDynamicNodeAtIndex(viewData, elementNodeIndex, 3 /* Element */, elementRNode, tagNameValue); visitedNodes.push(elementNodeIndex); break; default: throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); } } } setIsNotParent(); return visitedNodes; } /** * @param {?} updateOpCodes * @param {?} icus * @param {?} bindingsStartIndex * @param {?} changeMask * @param {?} viewData * @param {?=} bypassCheckBit * @return {?} */ function readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, viewData, bypassCheckBit = false) { /** @type {?} */ let caseCreated = false; for (let i = 0; i < updateOpCodes.length; i++) { // bit code to check if we should apply the next update /** @type {?} */ const checkBit = (/** @type {?} */ (updateOpCodes[i])); // Number of opCodes to skip until next set of update codes /** @type {?} */ const skipCodes = (/** @type {?} */ (updateOpCodes[++i])); if (bypassCheckBit || (checkBit & changeMask)) { // The value has been updated since last checked /** @type {?} */ let value = ''; for (let j = i + 1; j <= (i + skipCodes); j++) { /** @type {?} */ const opCode = updateOpCodes[j]; if (typeof opCode == 'string') { value += opCode; } else if (typeof opCode == 'number') { if (opCode < 0) { // It's a binding index whose value is negative value += renderStringify(viewData[bindingsStartIndex - opCode]); } else { /** @type {?} */ const nodeIndex = opCode >>> 2 /* SHIFT_REF */; /** @type {?} */ let tIcuIndex; /** @type {?} */ let tIcu; /** @type {?} */ let icuTNode; switch (opCode & 3 /* MASK_OPCODE */) { case 1 /* Attr */: /** @type {?} */ const propName = (/** @type {?} */ (updateOpCodes[++j])); /** @type {?} */ const sanitizeFn = (/** @type {?} */ (updateOpCodes[++j])); elementPropertyInternal(nodeIndex, propName, value, sanitizeFn); break; case 0 /* Text */: textBindingInternal(viewData, nodeIndex, value); break; case 2 /* IcuSwitch */: tIcuIndex = (/** @type {?} */ (updateOpCodes[++j])); tIcu = (/** @type {?} */ (icus))[tIcuIndex]; icuTNode = (/** @type {?} */ (getTNode(nodeIndex, viewData))); // If there is an active case, delete the old nodes if (icuTNode.activeCaseIndex !== null) { /** @type {?} */ const removeCodes = tIcu.remove[icuTNode.activeCaseIndex]; for (let k = 0; k < removeCodes.length; k++) { /** @type {?} */ const removeOpCode = (/** @type {?} */ (removeCodes[k])); switch (removeOpCode & 7 /* MASK_OPCODE */) { case 3 /* Remove */: /** @type {?} */ const nodeIndex = removeOpCode >>> 3 /* SHIFT_REF */; removeNode(nodeIndex, viewData); break; case 6 /* RemoveNestedIcu */: /** @type {?} */ const nestedIcuNodeIndex = (/** @type {?} */ (removeCodes[k + 1])) >>> 3 /* SHIFT_REF */; /** @type {?} */ const nestedIcuTNode = (/** @type {?} */ (getTNode(nestedIcuNodeIndex, viewData))); /** @type {?} */ const activeIndex = nestedIcuTNode.activeCaseIndex; if (activeIndex !== null) { /** @type {?} */ const nestedIcuTIndex = removeOpCode >>> 3 /* SHIFT_REF */; /** @type {?} */ const nestedTIcu = (/** @type {?} */ (icus))[nestedIcuTIndex]; addAllToArray(nestedTIcu.remove[activeIndex], removeCodes); } break; } } } // Update the active caseIndex /** @type {?} */ const caseIndex = getCaseIndex(tIcu, value); icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null; // Add the nodes for the new case readCreateOpCodes(-1, tIcu.create[caseIndex], icus, viewData); caseCreated = true; break; case 3 /* IcuUpdate */: tIcuIndex = (/** @type {?} */ (updateOpCodes[++j])); tIcu = (/** @type {?} */ (icus))[tIcuIndex]; icuTNode = (/** @type {?} */ (getTNode(nodeIndex, viewData))); readUpdateOpCodes(tIcu.update[(/** @type {?} */ (icuTNode.activeCaseIndex))], icus, bindingsStartIndex, changeMask, viewData, caseCreated); break; } } } } } i += skipCodes; } } /** * @param {?} index * @param {?} viewData * @return {?} */ function removeNode(index, viewData) { /** @type {?} */ const removedPhTNode = getTNode(index, viewData); /** @type {?} */ const removedPhRNode = getNativeByIndex(index, viewData); if (removedPhRNode) { nativeRemoveNode(viewData[RENDERER], removedPhRNode); } /** @type {?} */ const slotValue = (/** @type {?} */ (ɵɵload(index))); if (isLContainer(slotValue)) { /** @type {?} */ const lContainer = (/** @type {?} */ (slotValue)); if (removedPhTNode.type !== 0 /* Container */) { nativeRemoveNode(viewData[RENDERER], lContainer[NATIVE]); } } // Define this node as detached so that we don't risk projecting it removedPhTNode.flags |= 32 /* isDetached */; ngDevMode && ngDevMode.rendererRemoveNode++; } /** * * Use this instruction to create a translation block that doesn't contain any placeholder. * It calls both {\@link i18nStart} and {\@link i18nEnd} in one instruction. * * The translation `message` is the value which is locale specific. The translation string may * contain placeholders which associate inner elements and sub-templates within the translation. * * The translation `message` placeholders are: * - `�{index}(:{block})�`: *Binding Placeholder*: Marks a location where an expression will be * interpolated into. The placeholder `index` points to the expression binding index. An optional * `block` that matches the sub-template in which it was declared. * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *Element Placeholder*: Marks the beginning * and end of DOM element that were embedded in the original translation block. The placeholder * `index` points to the element index in the template instructions set. An optional `block` that * matches the sub-template in which it was declared. * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *Sub-template Placeholder*: Sub-templates must be * split up and translated separately in each angular template function. The `index` points to the * `template` instruction index. A `block` that matches the sub-template in which it was declared. * * \@codeGenApi * @param {?} index A unique index of the translation in the static block. * @param {?} message The translation message. * @param {?=} subTemplateIndex Optional sub-template index in the `message`. * * @return {?} */ export function ɵɵi18n(index, message, subTemplateIndex) { ɵɵi18nStart(index, message, subTemplateIndex); ɵɵi18nEnd(); } /** * Marks a list of attributes as translatable. * * \@codeGenApi * @param {?} index A unique index in the static block * @param {?} values * * @return {?} */ export function ɵɵi18nAttributes(index, values) { /** @type {?} */ const tView = getLView()[TVIEW]; ngDevMode && assertDefined(tView, `tView should be defined`); i18nAttributesFirstPass(tView, index, values); } /** * See `i18nAttributes` above. * @param {?} tView * @param {?} index * @param {?} values * @return {?} */ function i18nAttributesFirstPass(tView, index, values) { /** @type {?} */ const previousElement = getPreviousOrParentTNode(); /** @type {?} */ const previousElementIndex = previousElement.index - HEADER_OFFSET; /** @type {?} */ const updateOpCodes = []; for (let i = 0; i < values.length; i += 2) { /** @type {?} */ const attrName = values[i]; /** @type {?} */ const message = values[i + 1]; /** @type {?} */ const parts = message.split(ICU_REGEXP); for (let j = 0; j < parts.length; j++) { /** @type {?} */ const value = parts[j]; if (j & 1) { // Odd indexes are ICU expressions // TODO(ocombe): support ICU expressions in attributes throw new Error('ICU expressions are not yet supported in attributes'); } else if (value !== '') { // Even indexes are text (including bindings) /** @type {?} */ const hasBinding = !!value.match(BINDING_REGEXP); if (hasBinding) { if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { addAllToArray(generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes); } } else { /** @type {?} */ const lView = getLView(); elementAttributeInternal(previousElementIndex, attrName, value, lView); // Check if that attribute is a directive input /** @type {?} */ const tNode = getTNode(previousElementIndex, lView); /** @type {?} */ const dataValue = tNode.inputs && tNode.inputs[attrName]; if (dataValue) { setInputsForProperty(lView, dataValue, value); } } } } } if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { tView.data[index + HEADER_OFFSET] = updateOpCodes; } } /** @type {?} */ let changeMask = 0b0; /** @type {?} */ let shiftsCounter = 0; /** * Stores the values of the bindings during each update cycle in order to determine if we need to * update the translated nodes. * * \@codeGenApi * @template T * @param {?} value The binding's value * @return {?} This function returns itself so that it may be chained * (e.g. `i18nExp(ctx.name)(ctx.title)`) * */ export function ɵɵi18nExp(value) { /** @type {?} */ const lView = getLView(); /** @type {?} */ const expression = bind(lView, value); if (expression !== NO_CHANGE) { changeMask = changeMask | (1 << shiftsCounter); } shiftsCounter++; return ɵɵi18nExp; } /** * Updates a translation block or an i18n attribute when the bindings have changed. * * \@codeGenApi * @param {?} index Index of either {\@link i18nStart} (translation block) or {\@link i18nAttributes} * (i18n attribute) on which it should update the content. * * @return {?} */ export function ɵɵi18nApply(index) { if (shiftsCounter) { /** @type {?} */ const lVie