Pete Bacon Darwin 7a6a061a9e refactor(compiler): move the MessagePiece classes into output_ast.ts (#38645)
The `MessagePiece` and derived classes, `LiteralPiece` and `PlaceholderPiece`
need to be referenced in the `LocalizedString` output AST class, so that we
can render the source-spans of each piece.

PR Close #38645
2020-09-08 13:17:21 -07:00

137 lines
5.3 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 * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';
import {ParseLocation, ParseSourceSpan} from '../../../parse_util';
import {serializeIcuNode} from './icu_serializer';
import {formatI18nPlaceholderName} from './util';
export function createLocalizeStatements(
variable: o.ReadVarExpr, message: i18n.Message,
params: {[name: string]: o.Expression}): o.Statement[] {
const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);
const sourceSpan = getSourceSpan(message);
const expressions = placeHolders.map(ph => params[ph.text]);
const localizedString =
o.localizedString(message, messageParts, placeHolders, expressions, sourceSpan);
const variableInitialization = variable.set(localizedString);
return [new o.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 implements i18n.Visitor {
visitText(text: i18n.Text, context: o.MessagePiece[]): any {
if (context[context.length - 1] instanceof o.LiteralPiece) {
// Two literal pieces in a row means that there was some comment node in-between.
context[context.length - 1].text += text.value;
} else {
context.push(new o.LiteralPiece(text.value, text.sourceSpan));
}
}
visitContainer(container: i18n.Container, context: o.MessagePiece[]): any {
container.children.forEach(child => child.visit(this, context));
}
visitIcu(icu: i18n.Icu, context: o.MessagePiece[]): any {
context.push(new o.LiteralPiece(serializeIcuNode(icu), icu.sourceSpan));
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: o.MessagePiece[]): any {
context.push(this.createPlaceholderPiece(ph.startName, ph.sourceSpan));
if (!ph.isVoid) {
ph.children.forEach(child => child.visit(this, context));
context.push(this.createPlaceholderPiece(ph.closeName, ph.closeSourceSpan ?? ph.sourceSpan));
}
}
visitPlaceholder(ph: i18n.Placeholder, context: o.MessagePiece[]): any {
context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
private createPlaceholderPiece(name: string, sourceSpan: ParseSourceSpan): o.PlaceholderPiece {
return new o.PlaceholderPiece(
formatI18nPlaceholderName(name, /* useCamelCase */ false), sourceSpan);
}
}
const serializerVisitor = new LocalizeSerializerVisitor();
/**
* 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.
*/
export function serializeI18nMessageForLocalize(message: i18n.Message):
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const pieces: o.MessagePiece[] = [];
message.nodes.forEach(node => node.visit(serializerVisitor, pieces));
return processMessagePieces(pieces);
}
function getSourceSpan(message: i18n.Message): ParseSourceSpan {
const startNode = message.nodes[0];
const endNode = message.nodes[message.nodes.length - 1];
return new ParseSourceSpan(
startNode.sourceSpan.start, endNode.sourceSpan.end, 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: o.MessagePiece[]):
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const messageParts: o.LiteralPiece[] = [];
const placeHolders: o.PlaceholderPiece[] = [];
if (pieces[0] instanceof o.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 o.LiteralPiece) {
messageParts.push(part);
} else {
placeHolders.push(part);
if (pieces[i - 1] instanceof o.PlaceholderPiece) {
// There were two placeholders in a row, so we need to add an empty message part.
messageParts.push(createEmptyMessagePart(part.sourceSpan.end));
}
}
}
if (pieces[pieces.length - 1] instanceof o.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: ParseLocation): o.LiteralPiece {
return new o.LiteralPiece('', new ParseSourceSpan(location, location));
}