GeezerVison is showing github.com — fix(compiler): ensure that placeholders have the correct sourceSpan (… · angular/angular@8ec7156
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: packages/compiler-cli/test/ngtsc/template_mapping_spec.ts +89Lines changed: 89 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -427,6 +427,95 @@ runInEachFileSystem((os) => { | 427 | 427 | }); | 428 | 428 | }); | 429 | 429 |
| | 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 | + | 430 | 519 | it('should create tag (container) placeholder source-mappings', () => { | 431 | 520 | const mappings = compileAndMap(`<div i18n>Hello, <b>World</b>!</div>`); | 432 | 521 | expectMapping(mappings, { | |
Collapse file Copy file name to clipboardExpand all lines: packages/compiler/src/i18n/i18n_parser.ts +43-3Lines changed: 43 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -105,12 +105,13 @@ class _I18nVisitor implements html.Visitor { | 105 | 105 | } | 106 | 106 |
| 107 | 107 | 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); | 109 | 110 | return context.visitNodeFn(attribute, node); | 110 | 111 | } | 111 | 112 |
| 112 | 113 | 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); | 114 | 115 | return context.visitNodeFn(text, node); | 115 | 116 | } | 116 | 117 |
| @@ -156,7 +157,8 @@ class _I18nVisitor implements html.Visitor { | 156 | 157 | } | 157 | 158 |
| 158 | 159 | 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 { | 160 | 162 | const splitInterpolation = this._expressionParser.splitInterpolation( | 161 | 163 | text, sourceSpan.start.toString(), this._interpolationConfig); | 162 | 164 |
| @@ -197,10 +199,48 @@ class _I18nVisitor implements html.Visitor { | 197 | 199 | getOffsetSourceSpan(sourceSpan, splitInterpolation.stringSpans[lastStringIdx]); | 198 | 200 | nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], stringSpan)); | 199 | 201 | } | | 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 | + | 200 | 222 | return container; | 201 | 223 | } | 202 | 224 | } | 203 | 225 |
| | 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 | + | 204 | 244 | function getOffsetSourceSpan( | 205 | 245 | sourceSpan: ParseSourceSpan, {start, end}: {start: number, end: number}): ParseSourceSpan { | 206 | 246 | return new ParseSourceSpan(sourceSpan.fullStart.moveBy(start), sourceSpan.fullStart.moveBy(end)); | |
|