Skip to content

Commit 8ec7156

Browse files
petebacondarwinAndrewKushnir
authored andcommitted
fix(compiler): ensure that placeholders have the correct sourceSpan (#39717)
When the `preserveWhitespaces` is not true, the template parser will process the parsed AST nodes to remove excess whitespace. Since the generated `goog.getMsg()` statements rely upon the AST nodes after this whitespace is removed, the i18n extraction must make a second pass. Previously this resulted in innacurrate source-spans for the i18n text and placeholder nodes that were extracted in the second pass. This commit fixes this by reusing the source-spans from the first pass when extracting the nodes in the second pass. Fixes #39671 PR Close #39717
1 parent d172882 commit 8ec7156

File tree

2 files changed

+132
-3
lines changed

2 files changed

+132
-3
lines changed

packages/compiler-cli/test/ngtsc/template_mapping_spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,95 @@ runInEachFileSystem((os) => {
427427
});
428428
});
429429

430+
it('should correctly handle collapsed whitespace in interpolation placeholder source-mappings',
431+
() => {
432+
const mappings = compileAndMap(
433+
`<div i18n title=" pre-title {{title_value}} post-title" i18n-title> pre-body {{body_value}} post-body</div>`);
434+
expectMapping(mappings, {
435+
source: '<div i18n title=" pre-title {{title_value}} post-title" i18n-title> ',
436+
generated: 'i0.ɵɵelementStart(0, "div", 0)',
437+
sourceUrl: '../test.ts',
438+
});
439+
expectMapping(mappings, {
440+
source: '</div>',
441+
generated: 'i0.ɵɵelementEnd()',
442+
sourceUrl: '../test.ts',
443+
});
444+
expectMapping(mappings, {
445+
source: ' pre-body ',
446+
generated: '` pre-body ${',
447+
sourceUrl: '../test.ts',
448+
});
449+
expectMapping(mappings, {
450+
source: '{{body_value}}',
451+
generated: '"\\uFFFD0\\uFFFD"',
452+
sourceUrl: '../test.ts',
453+
});
454+
expectMapping(mappings, {
455+
source: ' post-body',
456+
generated: '}:INTERPOLATION: post-body`',
457+
sourceUrl: '../test.ts',
458+
});
459+
});
460+
461+
it('should correctly handle collapsed whitespace in element placeholder source-mappings',
462+
() => {
463+
const mappings =
464+
compileAndMap(`<div i18n>\n pre-p\n <p>\n in-p\n </p>\n post-p\n</div>`);
465+
// $localize expressions
466+
expectMapping(mappings, {
467+
sourceUrl: '../test.ts',
468+
source: 'pre-p\n ',
469+
generated: '` pre-p ${',
470+
});
471+
expectMapping(mappings, {
472+
sourceUrl: '../test.ts',
473+
source: '<p>\n ',
474+
generated: '"\\uFFFD#2\\uFFFD"',
475+
});
476+
expectMapping(mappings, {
477+
sourceUrl: '../test.ts',
478+
source: 'in-p\n ',
479+
generated: '}:START_PARAGRAPH: in-p ${',
480+
});
481+
expectMapping(mappings, {
482+
sourceUrl: '../test.ts',
483+
source: '</p>\n ',
484+
generated: '"\\uFFFD/#2\\uFFFD"',
485+
});
486+
expectMapping(mappings, {
487+
sourceUrl: '../test.ts',
488+
source: 'post-p\n',
489+
generated: '}:CLOSE_PARAGRAPH: post-p\n`',
490+
});
491+
// ivy instructions
492+
expectMapping(mappings, {
493+
sourceUrl: '../test.ts',
494+
source: '<div i18n>\n ',
495+
generated: 'i0.ɵɵelementStart(0, "div")',
496+
});
497+
expectMapping(mappings, {
498+
sourceUrl: '../test.ts',
499+
source: '<div i18n>\n ',
500+
generated: 'i0.ɵɵi18nStart(1, 0)',
501+
});
502+
expectMapping(mappings, {
503+
sourceUrl: '../test.ts',
504+
source: '<p>\n in-p\n </p>',
505+
generated: 'i0.ɵɵelement(2, "p")',
506+
});
507+
expectMapping(mappings, {
508+
sourceUrl: '../test.ts',
509+
source: '</div>',
510+
generated: 'i0.ɵɵi18nEnd()',
511+
});
512+
expectMapping(mappings, {
513+
sourceUrl: '../test.ts',
514+
source: '</div>',
515+
generated: 'i0.ɵɵelementEnd()',
516+
});
517+
});
518+
430519
it('should create tag (container) placeholder source-mappings', () => {
431520
const mappings = compileAndMap(`<div i18n>Hello, <b>World</b>!</div>`);
432521
expectMapping(mappings, {

packages/compiler/src/i18n/i18n_parser.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,13 @@ class _I18nVisitor implements html.Visitor {
105105
}
106106

107107
visitAttribute(attribute: html.Attribute, context: I18nMessageVisitorContext): i18n.Node {
108-
const node = this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan, context);
108+
const node = this._visitTextWithInterpolation(
109+
attribute.value, attribute.valueSpan || attribute.sourceSpan, context, attribute.i18n);
109110
return context.visitNodeFn(attribute, node);
110111
}
111112

112113
visitText(text: html.Text, context: I18nMessageVisitorContext): i18n.Node {
113-
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context);
114+
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context, text.i18n);
114115
return context.visitNodeFn(text, node);
115116
}
116117

@@ -156,7 +157,8 @@ class _I18nVisitor implements html.Visitor {
156157
}
157158

158159
private _visitTextWithInterpolation(
159-
text: string, sourceSpan: ParseSourceSpan, context: I18nMessageVisitorContext): i18n.Node {
160+
text: string, sourceSpan: ParseSourceSpan, context: I18nMessageVisitorContext,
161+
previousI18n: i18n.I18nMeta|undefined): i18n.Node {
160162
const splitInterpolation = this._expressionParser.splitInterpolation(
161163
text, sourceSpan.start.toString(), this._interpolationConfig);
162164

@@ -197,10 +199,48 @@ class _I18nVisitor implements html.Visitor {
197199
getOffsetSourceSpan(sourceSpan, splitInterpolation.stringSpans[lastStringIdx]);
198200
nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], stringSpan));
199201
}
202+
203+
if (previousI18n instanceof i18n.Message) {
204+
// The `previousI18n` is an i18n `Message`, so we are processing an `Attribute` with i18n
205+
// metadata. The `Message` should consist only of a single `Container` that contains the
206+
// parts (`Text` and `Placeholder`) to process.
207+
assertSingleContainerMessage(previousI18n);
208+
previousI18n = previousI18n.nodes[0];
209+
}
210+
211+
if (previousI18n instanceof i18n.Container) {
212+
// The `previousI18n` is a `Container`, which means that this is a second i18n extraction pass
213+
// after whitespace has been removed from the AST ndoes.
214+
assertEquivalentNodes(previousI18n.children, nodes);
215+
216+
// Reuse the source-spans from the first pass.
217+
for (let i = 0; i < nodes.length; i++) {
218+
nodes[i].sourceSpan = previousI18n.children[i].sourceSpan;
219+
}
220+
}
221+
200222
return container;
201223
}
202224
}
203225

226+
function assertSingleContainerMessage(message: i18n.Message): void {
227+
const nodes = message.nodes;
228+
if (nodes.length !== 1 || !(nodes[0] instanceof i18n.Container)) {
229+
throw new Error(
230+
'Unexpected previous i18n message - expected it to consist of only a single `Container` node.');
231+
}
232+
}
233+
234+
function assertEquivalentNodes(previousNodes: i18n.Node[], nodes: i18n.Node[]): void {
235+
if (previousNodes.length !== nodes.length) {
236+
throw new Error('The number of i18n message children changed between first and second pass.');
237+
}
238+
if (previousNodes.some((node, i) => nodes[i].constructor !== node.constructor)) {
239+
throw new Error(
240+
'The types of the i18n message children changed between first and second pass.');
241+
}
242+
}
243+
204244
function getOffsetSourceSpan(
205245
sourceSpan: ParseSourceSpan, {start, end}: {start: number, end: number}): ParseSourceSpan {
206246
return new ParseSourceSpan(sourceSpan.fullStart.moveBy(start), sourceSpan.fullStart.moveBy(end));

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy