Skip to content

Commit

Permalink
refactor(compiler): move the MessagePiece classes into output_ast.ts (#…
Browse files Browse the repository at this point in the history
…38747)

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 #38747
  • Loading branch information
petebacondarwin authored and atscott committed Sep 8, 2020
1 parent cfd4c0b commit 6b0dba4
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 95 deletions.
27 changes: 20 additions & 7 deletions packages/compiler/src/output/output_ast.ts
Expand Up @@ -9,7 +9,6 @@

import {ParseSourceSpan} from '../parse_util';
import {I18nMeta} from '../render3/view/i18n/meta';
import {error} from '../util';

//// Types
export enum TypeModifier {
Expand Down Expand Up @@ -516,11 +515,16 @@ export class LiteralExpr extends Expression {
}
}

export abstract class MessagePiece {
constructor(public text: string, public sourceSpan: ParseSourceSpan) {}
}
export class LiteralPiece extends MessagePiece {}
export class PlaceholderPiece extends MessagePiece {}

export class LocalizedString extends Expression {
constructor(
readonly metaBlock: I18nMeta, readonly messageParts: string[],
readonly placeHolderNames: string[], readonly expressions: Expression[],
readonly metaBlock: I18nMeta, readonly messageParts: LiteralPiece[],
readonly placeHolderNames: PlaceholderPiece[], readonly expressions: Expression[],
sourceSpan?: ParseSourceSpan|null) {
super(STRING_TYPE, sourceSpan);
}
Expand Down Expand Up @@ -563,7 +567,16 @@ export class LocalizedString extends Expression {
metaBlock = `${metaBlock}${LEGACY_ID_INDICATOR}${legacyId}`;
});
}
return createCookedRawString(metaBlock, this.messageParts[0]);
return createCookedRawString(metaBlock, this.messageParts[0].text);
}

getMessagePartSourceSpan(i: number): ParseSourceSpan|null {
return this.messageParts[i]?.sourceSpan ?? this.sourceSpan;
}

getPlaceholderSourceSpan(i: number): ParseSourceSpan {
return this.placeHolderNames[i]?.sourceSpan ?? this.expressions[i]?.sourceSpan ??
this.sourceSpan;
}

/**
Expand All @@ -574,9 +587,9 @@ export class LocalizedString extends Expression {
* @param messagePart The following message string after this placeholder
*/
serializeI18nTemplatePart(partIndex: number): {cooked: string, raw: string} {
const placeholderName = this.placeHolderNames[partIndex - 1];
const placeholderName = this.placeHolderNames[partIndex - 1].text;
const messagePart = this.messageParts[partIndex];
return createCookedRawString(placeholderName, messagePart);
return createCookedRawString(placeholderName, messagePart.text);
}
}

Expand Down Expand Up @@ -1799,7 +1812,7 @@ export function literal(
}

export function localizedString(
metaBlock: I18nMeta, messageParts: string[], placeholderNames: string[],
metaBlock: I18nMeta, messageParts: LiteralPiece[], placeholderNames: PlaceholderPiece[],
expressions: Expression[], sourceSpan?: ParseSourceSpan|null): LocalizedString {
return new LocalizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan);
}
Expand Down
77 changes: 38 additions & 39 deletions packages/compiler/src/render3/view/i18n/localize_utils.ts
Expand Up @@ -7,7 +7,7 @@
*/
import * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';
import {ParseSourceSpan} from '../../../parse_util';
import {ParseLocation, ParseSourceSpan} from '../../../parse_util';

import {serializeIcuNode} from './icu_serializer';
import {formatI18nPlaceholderName} from './util';
Expand All @@ -17,60 +17,55 @@ export function createLocalizeStatements(
params: {[name: string]: o.Expression}): o.Statement[] {
const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);
const sourceSpan = getSourceSpan(message);
const expressions = placeHolders.map(ph => params[ph]);
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)];
}

class MessagePiece {
constructor(public text: string) {}
}
class LiteralPiece extends MessagePiece {}
class PlaceholderPiece extends MessagePiece {
constructor(name: string) {
super(formatI18nPlaceholderName(name, /* useCamelCase */ false));
}
}

/**
* 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: MessagePiece[]): any {
if (context[context.length - 1] instanceof LiteralPiece) {
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 LiteralPiece(text.value));
context.push(new o.LiteralPiece(text.value, text.sourceSpan));
}
}

visitContainer(container: i18n.Container, context: MessagePiece[]): any {
visitContainer(container: i18n.Container, context: o.MessagePiece[]): any {
container.children.forEach(child => child.visit(this, context));
}

visitIcu(icu: i18n.Icu, context: MessagePiece[]): any {
context.push(new LiteralPiece(serializeIcuNode(icu)));
visitIcu(icu: i18n.Icu, context: o.MessagePiece[]): any {
context.push(new o.LiteralPiece(serializeIcuNode(icu), icu.sourceSpan));
}

visitTagPlaceholder(ph: i18n.TagPlaceholder, context: MessagePiece[]): any {
context.push(new PlaceholderPiece(ph.startName));
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(new PlaceholderPiece(ph.closeName));
context.push(this.createPlaceholderPiece(ph.closeName, ph.closeSourceSpan ?? ph.sourceSpan));
}
}

visitPlaceholder(ph: i18n.Placeholder, context: MessagePiece[]): any {
context.push(new PlaceholderPiece(ph.name));
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(new PlaceholderPiece(ph.name));
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);
}
}

Expand All @@ -85,8 +80,8 @@ const serializerVisitor = new LocalizeSerializerVisitor();
* @returns an object containing the messageParts and placeholders.
*/
export function serializeI18nMessageForLocalize(message: i18n.Message):
{messageParts: string[], placeHolders: string[]} {
const pieces: MessagePiece[] = [];
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const pieces: o.MessagePiece[] = [];
message.nodes.forEach(node => node.visit(serializerVisitor, pieces));
return processMessagePieces(pieces);
}
Expand All @@ -107,31 +102,35 @@ function getSourceSpan(message: i18n.Message): ParseSourceSpan {
* @param pieces The pieces to process.
* @returns an object containing the messageParts and placeholders.
*/
function processMessagePieces(pieces: MessagePiece[]):
{messageParts: string[], placeHolders: string[]} {
const messageParts: string[] = [];
const placeHolders: string[] = [];
function processMessagePieces(pieces: o.MessagePiece[]):
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const messageParts: o.LiteralPiece[] = [];
const placeHolders: o.PlaceholderPiece[] = [];

if (pieces[0] instanceof 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('');
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.text);
if (part instanceof o.LiteralPiece) {
messageParts.push(part);
} else {
placeHolders.push(part.text);
if (pieces[i - 1] instanceof PlaceholderPiece) {
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('');
messageParts.push(createEmptyMessagePart(part.sourceSpan.end));
}
}
}
if (pieces[pieces.length - 1] instanceof PlaceholderPiece) {
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('');
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));
}
9 changes: 6 additions & 3 deletions packages/compiler/test/output/js_emitter_spec.ts
Expand Up @@ -205,9 +205,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
});

it('should support ES5 localized strings', () => {
expect(emitStmt(new o.ExpressionStatement(o.localizedString(
{}, ['ab\\:c', 'd"e\'f'], ['ph1'],
[o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]))))
const messageParts =
[new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)];
const placeholders = [new o.PlaceholderPiece('ph1', {} as any)];
const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))];
const localizedString = o.localizedString({}, messageParts, placeholders, expressions);
expect(emitStmt(new o.ExpressionStatement(localizedString)))
.toEqual(
String.raw
`$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(['ab\\:c', ':ph1:d"e\'f'], ['ab\\\\:c', ':ph1:d"e\'f']), (7 + 8));`);
Expand Down
9 changes: 6 additions & 3 deletions packages/compiler/test/output/ts_emitter_spec.ts
Expand Up @@ -254,9 +254,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
});

it('should support localized strings', () => {
expect(emitStmt(new o.ExpressionStatement(o.localizedString(
{}, ['ab\\:c', 'd"e\'f'], ['ph1'],
[o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]))))
const messageParts =
[new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)];
const placeholders = [new o.PlaceholderPiece('ph1', {} as any)];
const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))];
const localizedString = o.localizedString({}, messageParts, placeholders, expressions);
expect(emitStmt(new o.ExpressionStatement(localizedString)))
.toEqual('$localize `ab\\\\:c${(7 + 8)}:ph1:d"e\'f`;');
});

Expand Down

0 comments on commit 6b0dba4

Please sign in to comment.