461 lines
16 KiB
TypeScript
Raw Normal View History

/**
* @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 {assertEqual, assertLessThan} from './assert';
import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load} from './instructions';
import {LContainerNode, LElementNode, LNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX} from './interfaces/view';
import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation';
import {stringify} from './util';
/**
* A list of flags to encode the i18n instructions used to translate the template.
* We shift the flags by 29 so that 30 & 31 & 32 bits contains the instructions.
*/
export const enum I18nInstructions {
Text = 1 << 29,
Element = 2 << 29,
Expression = 3 << 29,
CloseNode = 4 << 29,
RemoveNode = 5 << 29,
/** Used to decode the number encoded with the instruction. */
IndexMask = (1 << 29) - 1,
/** Used to test the type of instruction. */
InstructionMask = ~((1 << 29) - 1),
}
/**
* Represents the instructions used to translate the template.
* Instructions can be a placeholder index, a static text or a simple bit field (`I18nFlag`).
* When the instruction is the flag `Text`, it is always followed by its text value.
*/
export type I18nInstruction = number | string;
/**
* Represents the instructions used to translate attributes containing expressions.
* Even indexes contain static strings, while odd indexes contain the index of the expression whose
* value will be concatenated into the final translation.
*/
export type I18nExpInstruction = number | string;
/** Mapping of placeholder names to their absolute indexes in their templates. */
export type PlaceholderMap = {
[name: string]: number
};
const i18nTagRegex = /\{\$([^}]+)\}/g;
/**
* Takes a translation string, the initial list of placeholders (elements and expressions) and the
* indexes of their corresponding expression nodes to return a list of instructions for each
* template function.
*
* Because embedded templates have different indexes for each placeholder, each parameter (except
* the translation) is an array, where each value corresponds to a different template, by order of
* appearance.
*
* @param translation A translation string where placeholders are represented by `{$name}`
* @param elements An array containing, for each template, the maps of element placeholders and
* their indexes.
* @param expressions An array containing, for each template, the maps of expression placeholders
* and their indexes.
* @param tmplContainers An array of template container placeholders whose content should be ignored
* when generating the instructions for their parent template.
* @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is
* an ng-container.
*
* @returns A list of instructions used to translate each template.
*/
export function i18nMapping(
translation: string, elements: (PlaceholderMap | null)[] | null,
expressions?: (PlaceholderMap | null)[] | null, tmplContainers?: string[] | null,
lastChildIndex?: number | null): I18nInstruction[][] {
const translationParts = translation.split(i18nTagRegex);
const instructions: I18nInstruction[][] = [];
generateMappingInstructions(
0, translationParts, instructions, elements, expressions, tmplContainers, lastChildIndex);
return instructions;
}
/**
* Internal function that reads the translation parts and generates a set of instructions for each
* template.
*
* See `i18nMapping()` for more details.
*
* @param index The current index in `translationParts`.
* @param translationParts The translation string split into an array of placeholders and text
* elements.
* @param instructions The current list of instructions to update.
* @param elements An array containing, for each template, the maps of element placeholders and
* their indexes.
* @param expressions An array containing, for each template, the maps of expression placeholders
* and their indexes.
* @param tmplContainers An array of template container placeholders whose content should be ignored
* when generating the instructions for their parent template.
* @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is
* an ng-container.
* @returns the current index in `translationParts`
*/
function generateMappingInstructions(
index: number, translationParts: string[], instructions: I18nInstruction[][],
elements: (PlaceholderMap | null)[] | null, expressions?: (PlaceholderMap | null)[] | null,
tmplContainers?: string[] | null, lastChildIndex?: number | null): number {
const tmplIndex = instructions.length;
const tmplInstructions: I18nInstruction[] = [];
const phVisited = [];
let openedTagCount = 0;
let maxIndex = 0;
instructions.push(tmplInstructions);
for (; index < translationParts.length; index++) {
const value = translationParts[index];
// Odd indexes are placeholders
if (index & 1) {
let phIndex;
if (elements && elements[tmplIndex] &&
typeof(phIndex = elements[tmplIndex] ![value]) !== 'undefined') {
// The placeholder represents a DOM element
// Add an instruction to move the element
tmplInstructions.push(phIndex | I18nInstructions.Element);
phVisited.push(value);
openedTagCount++;
} else if (
expressions && expressions[tmplIndex] &&
typeof(phIndex = expressions[tmplIndex] ![value]) !== 'undefined') {
// The placeholder represents an expression
// Add an instruction to move the expression
tmplInstructions.push(phIndex | I18nInstructions.Expression);
phVisited.push(value);
} else { // It is a closing tag
tmplInstructions.push(I18nInstructions.CloseNode);
if (tmplIndex > 0) {
openedTagCount--;
// If we have reached the closing tag for this template, exit the loop
if (openedTagCount === 0) {
break;
}
}
}
if (typeof phIndex !== 'undefined' && phIndex > maxIndex) {
maxIndex = phIndex;
}
if (tmplContainers && tmplContainers.indexOf(value) !== -1 &&
tmplContainers.indexOf(value) >= tmplIndex) {
index = generateMappingInstructions(
index, translationParts, instructions, elements, expressions, tmplContainers,
lastChildIndex);
}
} else if (value) {
// It's a non-empty string, create a text node
tmplInstructions.push(I18nInstructions.Text, value);
}
}
// Check if some elements from the template are missing from the translation
if (elements) {
const tmplElements = elements[tmplIndex];
if (tmplElements) {
const phKeys = Object.keys(tmplElements);
for (let i = 0; i < phKeys.length; i++) {
const ph = phKeys[i];
if (phVisited.indexOf(ph) === -1) {
let index = tmplElements[ph];
// Add an instruction to remove the element
tmplInstructions.push(index | I18nInstructions.RemoveNode);
if (index > maxIndex) {
maxIndex = index;
}
}
}
}
}
// Check if some expressions from the template are missing from the translation
if (expressions) {
const tmplExpressions = expressions[tmplIndex];
if (tmplExpressions) {
const phKeys = Object.keys(tmplExpressions);
for (let i = 0; i < phKeys.length; i++) {
const ph = phKeys[i];
if (phVisited.indexOf(ph) === -1) {
let index = tmplExpressions[ph];
if (ngDevMode) {
assertLessThan(
index.toString(2).length, 28, `Index ${index} is too big and will overflow`);
}
// Add an instruction to remove the expression
tmplInstructions.push(index | I18nInstructions.RemoveNode);
if (index > maxIndex) {
maxIndex = index;
}
}
}
}
}
if (tmplIndex === 0 && typeof lastChildIndex === 'number') {
// The current parent is an ng-container and it has more children after the translation that we
// need to append to keep the order of the DOM nodes correct
for (let i = maxIndex + 1; i <= lastChildIndex; i++) {
if (ngDevMode) {
assertLessThan(i.toString(2).length, 28, `Index ${i} is too big and will overflow`);
}
// We consider those additional placeholders as expressions because we don't care about
// their children, all we need to do is to append them
tmplInstructions.push(i | I18nInstructions.Expression);
}
}
return index;
}
function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
if (ngDevMode) {
ngDevMode.rendererMoveNode++;
}
const viewData = getViewData();
appendChild(parentNode, node.native || null, viewData);
if (previousNode === parentNode && parentNode.pChild === null) {
parentNode.pChild = node;
} else {
previousNode.pNextOrParent = node;
}
// Template containers also have a comment node for the `ViewContainerRef` that should be moved
if (node.tNode.type === TNodeType.Container && node.dynamicLContainerNode) {
// (node.native as RComment).textContent = 'test';
// console.log(node.native);
appendChild(parentNode, node.dynamicLContainerNode.native || null, viewData);
node.pNextOrParent = node.dynamicLContainerNode;
return node.dynamicLContainerNode;
}
return node;
}
/**
* Takes a list of instructions generated by `i18nMapping()` to transform the template accordingly.
*
* @param startIndex Index of the first element to translate (for instance the first child of the
* element with the i18n attribute).
* @param instructions The list of instructions to apply on the current view.
*/
export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void {
const viewData = getViewData();
if (ngDevMode) {
assertEqual(viewData[BINDING_INDEX], -1, 'i18nApply should be called before any binding');
}
if (!instructions) {
return;
}
const renderer = getRenderer();
let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode();
let localPreviousNode: LNode = localParentNode;
for (let i = 0; i < instructions.length; i++) {
const instruction = instructions[i] as number;
switch (instruction & I18nInstructions.InstructionMask) {
case I18nInstructions.Element:
const element: LNode = load(instruction & I18nInstructions.IndexMask);
localPreviousNode = appendI18nNode(element, localParentNode, localPreviousNode);
localParentNode = element;
break;
case I18nInstructions.Expression:
const expr: LNode = load(instruction & I18nInstructions.IndexMask);
localPreviousNode = appendI18nNode(expr, localParentNode, localPreviousNode);
break;
case I18nInstructions.Text:
if (ngDevMode) {
ngDevMode.rendererCreateTextNode++;
}
const value = instructions[++i];
const textRNode = createTextNode(value, renderer);
// If we were to only create a `RNode` then projections won't move the text.
// But since this text doesn't have an index in `LViewData`, we need to create an
// `LElementNode` with the index -1 so that it isn't saved in `LViewData`
const textLNode = createLNode(-1, TNodeType.Element, textRNode, null, null);
textLNode.dynamicParent = localParentNode as LElementNode | LContainerNode;
localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode);
break;
case I18nInstructions.CloseNode:
localPreviousNode = localParentNode;
localParentNode = getParentLNode(localParentNode) !;
break;
case I18nInstructions.RemoveNode:
if (ngDevMode) {
ngDevMode.rendererRemoveNode++;
}
const index = instruction & I18nInstructions.IndexMask;
const removedNode: LNode|LContainerNode = load(index);
const parentNode = getParentLNode(removedNode) !;
removeChild(parentNode, removedNode.native || null, viewData);
// For template containers we also need to remove their `ViewContainerRef` from the DOM
if (removedNode.tNode.type === TNodeType.Container && removedNode.dynamicLContainerNode) {
removeChild(parentNode, removedNode.dynamicLContainerNode.native || null, viewData);
removedNode.dynamicLContainerNode.tNode.detached = true;
}
break;
}
}
}
/**
* Takes a translation string and the initial list of expressions and returns a list of instructions
* that will be used to translate an attribute.
* Even indexes contain static strings, while odd indexes contain the index of the expression whose
* value will be concatenated into the final translation.
*/
export function i18nExpMapping(
translation: string, placeholders: PlaceholderMap): I18nExpInstruction[] {
const staticText: I18nExpInstruction[] = translation.split(i18nTagRegex);
// odd indexes are placeholders
for (let i = 1; i < staticText.length; i += 2) {
staticText[i] = placeholders[staticText[i]];
}
return staticText;
}
/**
* Checks if the value of up to 8 expressions have changed and replaces them by their values in a
* translation, or returns NO_CHANGE.
*
* @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise.
*/
export function i18nInterpolation(
instructions: I18nExpInstruction[], numberOfExp: number, v0: any, v1?: any, v2?: any, v3?: any,
v4?: any, v5?: any, v6?: any, v7?: any): string|NO_CHANGE {
let different = bindingUpdated(v0);
if (numberOfExp > 1) {
different = bindingUpdated(v1) || different;
if (numberOfExp > 2) {
different = bindingUpdated(v2) || different;
if (numberOfExp > 3) {
different = bindingUpdated(v3) || different;
if (numberOfExp > 4) {
different = bindingUpdated(v4) || different;
if (numberOfExp > 5) {
different = bindingUpdated(v5) || different;
if (numberOfExp > 6) {
different = bindingUpdated(v6) || different;
if (numberOfExp > 7) {
different = bindingUpdated(v7) || different;
}
}
}
}
}
}
}
if (!different) {
return NO_CHANGE;
}
let res = '';
for (let i = 0; i < instructions.length; i++) {
let value: any;
// Odd indexes are placeholders
if (i & 1) {
switch (instructions[i]) {
case 0:
value = v0;
break;
case 1:
value = v1;
break;
case 2:
value = v2;
break;
case 3:
value = v3;
break;
case 4:
value = v4;
break;
case 5:
value = v5;
break;
case 6:
value = v6;
break;
case 7:
value = v7;
break;
}
res += stringify(value);
} else {
res += instructions[i];
}
}
return res;
}
/**
* Create a translated interpolation binding with a variable number of expressions.
*
* If there are 1 to 8 expressions then `i18nInterpolation()` should be used instead. It is faster
* because there is no need to create an array of expressions and iterate over it.
*
* @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise.
*/
export function i18nInterpolationV(instructions: I18nExpInstruction[], values: any[]): string|
NO_CHANGE {
let different = false;
for (let i = 0; i < values.length; i++) {
// Check if bindings have changed
bindingUpdated(values[i]) && (different = true);
}
if (!different) {
return NO_CHANGE;
}
let res = '';
for (let i = 0; i < instructions.length; i++) {
// Odd indexes are placeholders
if (i & 1) {
res += stringify(values[instructions[i] as number]);
} else {
res += instructions[i];
}
}
return res;
}