Files
angular/packages/compiler/src/render3/view/i18n/context.ts
Joey Perrott d1ea1f4c7f build: update license headers to reference Google LLC (#37205)
Update the license headers throughout the repository to reference Google LLC
rather than Google Inc, for the required license headers.

PR Close #37205
2020-05-26 14:26:58 -04:00

225 lines
8.1 KiB
TypeScript

/**
* @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.io/license
*/
import {AST} from '../../../expression_parser/ast';
import * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';
import {assembleBoundTextPlaceholders, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util';
enum TagType {
ELEMENT,
TEMPLATE,
PROJECTION
}
/**
* Generates an object that is used as a shared state between parent and all child contexts.
*/
function setupRegistry() {
return {getUniqueId: getSeqNumberGenerator(), icus: new Map<string, any[]>()};
}
/**
* I18nContext is a helper class which keeps track of all i18n-related aspects
* (accumulates placeholders, bindings, etc) between i18nStart and i18nEnd instructions.
*
* When we enter a nested template, the top-level context is being passed down
* to the nested component, which uses this context to generate a child instance
* of I18nContext class (to handle nested template) and at the end, reconciles it back
* with the parent context.
*
* @param index Instruction index of i18nStart, which initiates this context
* @param ref Reference to a translation const that represents the content if thus context
* @param level Nestng level defined for child contexts
* @param templateIndex Instruction index of a template which this context belongs to
* @param meta Meta information (id, meaning, description, etc) associated with this context
*/
export class I18nContext {
public readonly id: number;
public bindings = new Set<AST>();
public placeholders = new Map<string, any[]>();
public isEmitted: boolean = false;
private _registry!: any;
private _unresolvedCtxCount: number = 0;
constructor(
readonly index: number, readonly ref: o.ReadVarExpr, readonly level: number = 0,
readonly templateIndex: number|null = null, readonly meta: i18n.I18nMeta,
private registry?: any) {
this._registry = registry || setupRegistry();
this.id = this._registry.getUniqueId();
}
private appendTag(type: TagType, node: i18n.TagPlaceholder, index: number, closed?: boolean) {
if (node.isVoid && closed) {
return; // ignore "close" for void tags
}
const ph = node.isVoid || !closed ? node.startName : node.closeName;
const content = {type, index, ctx: this.id, isVoid: node.isVoid, closed};
updatePlaceholderMap(this.placeholders, ph, content);
}
get icus() {
return this._registry.icus;
}
get isRoot() {
return this.level === 0;
}
get isResolved() {
return this._unresolvedCtxCount === 0;
}
getSerializedPlaceholders() {
const result = new Map<string, any[]>();
this.placeholders.forEach(
(values, key) => result.set(key, values.map(serializePlaceholderValue)));
return result;
}
// public API to accumulate i18n-related content
appendBinding(binding: AST) {
this.bindings.add(binding);
}
appendIcu(name: string, ref: o.Expression) {
updatePlaceholderMap(this._registry.icus, name, ref);
}
appendBoundText(node: i18n.I18nMeta) {
const phs = assembleBoundTextPlaceholders(node, this.bindings.size, this.id);
phs.forEach((values, key) => updatePlaceholderMap(this.placeholders, key, ...values));
}
appendTemplate(node: i18n.I18nMeta, index: number) {
// add open and close tags at the same time,
// since we process nested templates separately
this.appendTag(TagType.TEMPLATE, node as i18n.TagPlaceholder, index, false);
this.appendTag(TagType.TEMPLATE, node as i18n.TagPlaceholder, index, true);
this._unresolvedCtxCount++;
}
appendElement(node: i18n.I18nMeta, index: number, closed?: boolean) {
this.appendTag(TagType.ELEMENT, node as i18n.TagPlaceholder, index, closed);
}
appendProjection(node: i18n.I18nMeta, index: number) {
// add open and close tags at the same time,
// since we process projected content separately
this.appendTag(TagType.PROJECTION, node as i18n.TagPlaceholder, index, false);
this.appendTag(TagType.PROJECTION, node as i18n.TagPlaceholder, index, true);
}
/**
* Generates an instance of a child context based on the root one,
* when we enter a nested template within I18n section.
*
* @param index Instruction index of corresponding i18nStart, which initiates this context
* @param templateIndex Instruction index of a template which this context belongs to
* @param meta Meta information (id, meaning, description, etc) associated with this context
*
* @returns I18nContext instance
*/
forkChildContext(index: number, templateIndex: number, meta: i18n.I18nMeta) {
return new I18nContext(index, this.ref, this.level + 1, templateIndex, meta, this._registry);
}
/**
* Reconciles child context into parent one once the end of the i18n block is reached (i18nEnd).
*
* @param context Child I18nContext instance to be reconciled with parent context.
*/
reconcileChildContext(context: I18nContext) {
// set the right context id for open and close
// template tags, so we can use it as sub-block ids
['start', 'close'].forEach((op: string) => {
const key = (context.meta as any)[`${op}Name`];
const phs = this.placeholders.get(key) || [];
const tag = phs.find(findTemplateFn(this.id, context.templateIndex));
if (tag) {
tag.ctx = context.id;
}
});
// reconcile placeholders
const childPhs = context.placeholders;
childPhs.forEach((values: any[], key: string) => {
const phs = this.placeholders.get(key);
if (!phs) {
this.placeholders.set(key, values);
return;
}
// try to find matching template...
const tmplIdx = phs.findIndex(findTemplateFn(context.id, context.templateIndex));
if (tmplIdx >= 0) {
// ... if found - replace it with nested template content
const isCloseTag = key.startsWith('CLOSE');
const isTemplateTag = key.endsWith('NG-TEMPLATE');
if (isTemplateTag) {
// current template's content is placed before or after
// parent template tag, depending on the open/close atrribute
phs.splice(tmplIdx + (isCloseTag ? 0 : 1), 0, ...values);
} else {
const idx = isCloseTag ? values.length - 1 : 0;
values[idx].tmpl = phs[tmplIdx];
phs.splice(tmplIdx, 1, ...values);
}
} else {
// ... otherwise just append content to placeholder value
phs.push(...values);
}
this.placeholders.set(key, phs);
});
this._unresolvedCtxCount--;
}
}
//
// Helper methods
//
function wrap(symbol: string, index: number, contextId: number, closed?: boolean): string {
const state = closed ? '/' : '';
return wrapI18nPlaceholder(`${state}${symbol}${index}`, contextId);
}
function wrapTag(symbol: string, {index, ctx, isVoid}: any, closed?: boolean): string {
return isVoid ? wrap(symbol, index, ctx) + wrap(symbol, index, ctx, true) :
wrap(symbol, index, ctx, closed);
}
function findTemplateFn(ctx: number, templateIndex: number|null) {
return (token: any) => typeof token === 'object' && token.type === TagType.TEMPLATE &&
token.index === templateIndex && token.ctx === ctx;
}
function serializePlaceholderValue(value: any): string {
const element = (data: any, closed?: boolean) => wrapTag('#', data, closed);
const template = (data: any, closed?: boolean) => wrapTag('*', data, closed);
const projection = (data: any, closed?: boolean) => wrapTag('!', data, closed);
switch (value.type) {
case TagType.ELEMENT:
// close element tag
if (value.closed) {
return element(value, true) + (value.tmpl ? template(value.tmpl, true) : '');
}
// open element tag that also initiates a template
if (value.tmpl) {
return template(value.tmpl) + element(value) +
(value.isVoid ? template(value.tmpl, true) : '');
}
return element(value);
case TagType.TEMPLATE:
return template(value, value.closed);
case TagType.PROJECTION:
return projection(value, value.closed);
default:
return value;
}
}