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
This commit is contained in:
parent
6b38c44a22
commit
0462a616c3
|
@ -427,6 +427,95 @@ runInEachFileSystem((os) => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should correctly handle collapsed whitespace in interpolation placeholder source-mappings',
|
||||
() => {
|
||||
const mappings = compileAndMap(
|
||||
`<div i18n title=" pre-title {{title_value}} post-title" i18n-title> pre-body {{body_value}} post-body</div>`);
|
||||
expectMapping(mappings, {
|
||||
source: '<div i18n title=" pre-title {{title_value}} post-title" i18n-title> ',
|
||||
generated: 'i0.ɵɵelementStart(0, "div", 0)',
|
||||
sourceUrl: '../test.ts',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
source: '</div>',
|
||||
generated: 'i0.ɵɵelementEnd()',
|
||||
sourceUrl: '../test.ts',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
source: ' pre-body ',
|
||||
generated: '` pre-body ${',
|
||||
sourceUrl: '../test.ts',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
source: '{{body_value}}',
|
||||
generated: '"\\uFFFD0\\uFFFD"',
|
||||
sourceUrl: '../test.ts',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
source: ' post-body',
|
||||
generated: '}:INTERPOLATION: post-body`',
|
||||
sourceUrl: '../test.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly handle collapsed whitespace in element placeholder source-mappings',
|
||||
() => {
|
||||
const mappings =
|
||||
compileAndMap(`<div i18n>\n pre-p\n <p>\n in-p\n </p>\n post-p\n</div>`);
|
||||
// $localize expressions
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: 'pre-p\n ',
|
||||
generated: '` pre-p ${',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '<p>\n ',
|
||||
generated: '"\\uFFFD#2\\uFFFD"',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: 'in-p\n ',
|
||||
generated: '}:START_PARAGRAPH: in-p ${',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '</p>\n ',
|
||||
generated: '"\\uFFFD/#2\\uFFFD"',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: 'post-p\n',
|
||||
generated: '}:CLOSE_PARAGRAPH: post-p\n`',
|
||||
});
|
||||
// ivy instructions
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '<div i18n>\n ',
|
||||
generated: 'i0.ɵɵelementStart(0, "div")',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '<div i18n>\n ',
|
||||
generated: 'i0.ɵɵi18nStart(1, 0)',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '<p>\n in-p\n </p>',
|
||||
generated: 'i0.ɵɵelement(2, "p")',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '</div>',
|
||||
generated: 'i0.ɵɵi18nEnd()',
|
||||
});
|
||||
expectMapping(mappings, {
|
||||
sourceUrl: '../test.ts',
|
||||
source: '</div>',
|
||||
generated: 'i0.ɵɵelementEnd()',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create tag (container) placeholder source-mappings', () => {
|
||||
const mappings = compileAndMap(`<div i18n>Hello, <b>World</b>!</div>`);
|
||||
expectMapping(mappings, {
|
||||
|
|
|
@ -105,12 +105,13 @@ class _I18nVisitor implements html.Visitor {
|
|||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: I18nMessageVisitorContext): i18n.Node {
|
||||
const node = this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan, context);
|
||||
const node = this._visitTextWithInterpolation(
|
||||
attribute.value, attribute.valueSpan || attribute.sourceSpan, context, attribute.i18n);
|
||||
return context.visitNodeFn(attribute, node);
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: I18nMessageVisitorContext): i18n.Node {
|
||||
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context);
|
||||
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan, context, text.i18n);
|
||||
return context.visitNodeFn(text, node);
|
||||
}
|
||||
|
||||
|
@ -156,7 +157,8 @@ class _I18nVisitor implements html.Visitor {
|
|||
}
|
||||
|
||||
private _visitTextWithInterpolation(
|
||||
text: string, sourceSpan: ParseSourceSpan, context: I18nMessageVisitorContext): i18n.Node {
|
||||
text: string, sourceSpan: ParseSourceSpan, context: I18nMessageVisitorContext,
|
||||
previousI18n: i18n.I18nMeta|undefined): i18n.Node {
|
||||
const splitInterpolation = this._expressionParser.splitInterpolation(
|
||||
text, sourceSpan.start.toString(), this._interpolationConfig);
|
||||
|
||||
|
@ -197,10 +199,48 @@ class _I18nVisitor implements html.Visitor {
|
|||
getOffsetSourceSpan(sourceSpan, splitInterpolation.stringSpans[lastStringIdx]);
|
||||
nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], stringSpan));
|
||||
}
|
||||
|
||||
if (previousI18n instanceof i18n.Message) {
|
||||
// The `previousI18n` is an i18n `Message`, so we are processing an `Attribute` with i18n
|
||||
// metadata. The `Message` should consist only of a single `Container` that contains the
|
||||
// parts (`Text` and `Placeholder`) to process.
|
||||
assertSingleContainerMessage(previousI18n);
|
||||
previousI18n = previousI18n.nodes[0];
|
||||
}
|
||||
|
||||
if (previousI18n instanceof i18n.Container) {
|
||||
// The `previousI18n` is a `Container`, which means that this is a second i18n extraction pass
|
||||
// after whitespace has been removed from the AST ndoes.
|
||||
assertEquivalentNodes(previousI18n.children, nodes);
|
||||
|
||||
// Reuse the source-spans from the first pass.
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nodes[i].sourceSpan = previousI18n.children[i].sourceSpan;
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
function assertSingleContainerMessage(message: i18n.Message): void {
|
||||
const nodes = message.nodes;
|
||||
if (nodes.length !== 1 || !(nodes[0] instanceof i18n.Container)) {
|
||||
throw new Error(
|
||||
'Unexpected previous i18n message - expected it to consist of only a single `Container` node.');
|
||||
}
|
||||
}
|
||||
|
||||
function assertEquivalentNodes(previousNodes: i18n.Node[], nodes: i18n.Node[]): void {
|
||||
if (previousNodes.length !== nodes.length) {
|
||||
throw new Error('The number of i18n message children changed between first and second pass.');
|
||||
}
|
||||
if (previousNodes.some((node, i) => nodes[i].constructor !== node.constructor)) {
|
||||
throw new Error(
|
||||
'The types of the i18n message children changed between first and second pass.');
|
||||
}
|
||||
}
|
||||
|
||||
function getOffsetSourceSpan(
|
||||
sourceSpan: ParseSourceSpan, {start, end}: {start: number, end: number}): ParseSourceSpan {
|
||||
return new ParseSourceSpan(sourceSpan.fullStart.moveBy(start), sourceSpan.fullStart.moveBy(end));
|
||||
|
|
Loading…
Reference in New Issue