GeezerVison is showing github.com — fix(compiler): handle strings inside bindings that contain binding ch… · angular/angular@f5aab2b
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/src/expression_parser/parser.ts +32-3Lines changed: 32 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -244,10 +244,10 @@ export class Parser { | 244 | 244 |
| 245 | 245 | atInterpolation = true; | 246 | 246 | } else { | 247 | | - // parse from starting {{ to ending }} | | 247 | + // parse from starting {{ to ending }} while ignoring content inside quotes. | 248 | 248 | const fullStart = i; | 249 | 249 | const exprStart = fullStart + interpStart.length; | 250 | | - const exprEnd = input.indexOf(interpEnd, exprStart); | | 250 | + const exprEnd = this._getExpressiondEndIndex(input, interpEnd, exprStart); | 251 | 251 | if (exprEnd === -1) { | 252 | 252 | // Could not find the end of the interpolation; do not parse an expression. | 253 | 253 | // Instead we should extend the content on the last raw string. | @@ -340,10 +340,39 @@ export class Parser { | 340 | 340 |
| 341 | 341 | return errLocation.length; | 342 | 342 | } | | 343 | + | | 344 | + /** | | 345 | + * Finds the index of the end of an interpolation expression | | 346 | + * while ignoring comments and quoted content. | | 347 | + */ | | 348 | + private _getExpressiondEndIndex(input: string, expressionEnd: string, start: number): number { | | 349 | + let currentQuote: string|null = null; | | 350 | + let escapeCount = 0; | | 351 | + for (let i = start; i < input.length; i++) { | | 352 | + const char = input[i]; | | 353 | + // Skip the characters inside quotes. Note that we only care about the | | 354 | + // outer-most quotes matching up and we need to account for escape characters. | | 355 | + if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) && | | 356 | + escapeCount % 2 === 0) { | | 357 | + currentQuote = currentQuote === null ? char : null; | | 358 | + } else if (currentQuote === null) { | | 359 | + if (input.startsWith(expressionEnd, i)) { | | 360 | + return i; | | 361 | + } | | 362 | + // Nothing else in the expression matters after we've | | 363 | + // hit a comment so look directly for the end token. | | 364 | + if (input.startsWith('//', i)) { | | 365 | + return input.indexOf(expressionEnd, i); | | 366 | + } | | 367 | + } | | 368 | + escapeCount = char === '\\' ? escapeCount + 1 : 0; | | 369 | + } | | 370 | + return -1; | | 371 | + } | 343 | 372 | } | 344 | 373 |
| 345 | 374 | export class IvyParser extends Parser { | 346 | | - simpleExpressionChecker = IvySimpleExpressionChecker; // | | 375 | + simpleExpressionChecker = IvySimpleExpressionChecker; | 347 | 376 | } | 348 | 377 |
| 349 | 378 | /** Describes a stateful context an expression parser is in. */ | |
Collapse file Copy file name to clipboardExpand all lines: packages/compiler/test/expression_parser/parser_spec.ts +39-1Lines changed: 39 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -762,6 +762,37 @@ describe('parser', () => { | 762 | 762 | expect(ast.expressions[0].name).toEqual('a'); | 763 | 763 | }); | 764 | 764 |
| | 765 | + it('should parse interpolation inside quotes', () => { | | 766 | + const ast = parseInterpolation('"{{a}}"')!.ast as Interpolation; | | 767 | + expect(ast.strings).toEqual(['"', '"']); | | 768 | + expect(ast.expressions.length).toEqual(1); | | 769 | + expect(ast.expressions[0].name).toEqual('a'); | | 770 | + }); | | 771 | + | | 772 | + it('should parse interpolation with interpolation characters inside quotes', () => { | | 773 | + checkInterpolation('{{"{{a}}"}}', '{{ "{{a}}" }}'); | | 774 | + checkInterpolation('{{"{{"}}', '{{ "{{" }}'); | | 775 | + checkInterpolation('{{"}}"}}', '{{ "}}" }}'); | | 776 | + checkInterpolation('{{"{"}}', '{{ "{" }}'); | | 777 | + checkInterpolation('{{"}"}}', '{{ "}" }}'); | | 778 | + }); | | 779 | + | | 780 | + it('should parse interpolation with escaped quotes', () => { | | 781 | + checkInterpolation(`{{'It\\'s just Angular'}}`, `{{ "It's just Angular" }}`); | | 782 | + checkInterpolation(`{{'It\\'s {{ just Angular'}}`, `{{ "It's {{ just Angular" }}`); | | 783 | + checkInterpolation(`{{'It\\'s }} just Angular'}}`, `{{ "It's }} just Angular" }}`); | | 784 | + }); | | 785 | + | | 786 | + it('should parse interpolation with escaped backslashes', () => { | | 787 | + checkInterpolation(`{{foo.split('\\\\')}}`, `{{ foo.split("\\") }}`); | | 788 | + checkInterpolation(`{{foo.split('\\\\\\\\')}}`, `{{ foo.split("\\\\") }}`); | | 789 | + checkInterpolation(`{{foo.split('\\\\\\\\\\\\')}}`, `{{ foo.split("\\\\\\") }}`); | | 790 | + }); | | 791 | + | | 792 | + it('should not parse interpolation with mismatching quotes', () => { | | 793 | + expect(parseInterpolation(`{{ "{{a}}' }}`)).toBeNull(); | | 794 | + }); | | 795 | + | 765 | 796 | it('should parse prefix/suffix with multiple interpolation', () => { | 766 | 797 | const originalExp = 'before {{ a }} middle {{ b }} after'; | 767 | 798 | const ast = parseInterpolation(originalExp)!.ast; | @@ -819,6 +850,10 @@ describe('parser', () => { | 819 | 850 | it('should retain // in nested, unterminated strings', () => { | 820 | 851 | checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`); | 821 | 852 | }); | | 853 | + | | 854 | + it('should ignore quotes inside a comment', () => { | | 855 | + checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`); | | 856 | + }); | 822 | 857 | }); | 823 | 858 | }); | 824 | 859 |
| @@ -999,8 +1034,11 @@ function parseSimpleBindingIvy( | 999 | 1034 | } | 1000 | 1035 |
| 1001 | 1036 | function checkInterpolation(exp: string, expected?: string) { | 1002 | | - const ast = parseInterpolation(exp)!; | | 1037 | + const ast = parseInterpolation(exp); | 1003 | 1038 | if (expected == null) expected = exp; | | 1039 | + if (ast === null) { | | 1040 | + throw Error(`Failed to parse expression "${exp}"`); | | 1041 | + } | 1004 | 1042 | expect(unparse(ast)).toEqual(expected); | 1005 | 1043 | validate(ast); | 1006 | 1044 | } | |
Collapse file Copy file name to clipboardExpand all lines: packages/compiler/test/template_parser/template_parser_spec.ts +48Lines changed: 48 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -540,6 +540,54 @@ describe('TemplateParser', () => { | 540 | 540 | expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); | 541 | 541 | }); | 542 | 542 |
| | 543 | + it('should parse bound text nodes inside quotes', () => { | | 544 | + expect(humanizeTplAst(parse('"{{a}}"', []))).toEqual([[BoundTextAst, '"{{ a }}"']]); | | 545 | + }); | | 546 | + | | 547 | + it('should parse bound text nodes with interpolations inside quotes', () => { | | 548 | + expect(humanizeTplAst(parse('{{ "{{a}}" }}', []))).toEqual([[BoundTextAst, '{{ "{{a}}" }}']]); | | 549 | + expect(humanizeTplAst(parse('{{"{{"}}', []))).toEqual([[BoundTextAst, '{{ "{{" }}']]); | | 550 | + expect(humanizeTplAst(parse('{{"}}"}}', []))).toEqual([[BoundTextAst, '{{ "}}" }}']]); | | 551 | + expect(humanizeTplAst(parse('{{"{"}}', []))).toEqual([[BoundTextAst, '{{ "{" }}']]); | | 552 | + expect(humanizeTplAst(parse('{{"}"}}', []))).toEqual([[BoundTextAst, '{{ "}" }}']]); | | 553 | + }); | | 554 | + | | 555 | + it('should parse bound text nodes with escaped quotes', () => { | | 556 | + expect(humanizeTplAst(parse(`{{'It\\'s just Angular'}}`, []))).toEqual([ | | 557 | + [BoundTextAst, `{{ "It's just Angular" }}`] | | 558 | + ]); | | 559 | + | | 560 | + expect(humanizeTplAst(parse(`{{'It\\'s {{ just Angular'}}`, []))).toEqual([ | | 561 | + [BoundTextAst, `{{ "It's {{ just Angular" }}`] | | 562 | + ]); | | 563 | + | | 564 | + expect(humanizeTplAst(parse(`{{'It\\'s }} just Angular'}}`, []))).toEqual([ | | 565 | + [BoundTextAst, `{{ "It's }} just Angular" }}`] | | 566 | + ]); | | 567 | + }); | | 568 | + | | 569 | + it('should not parse bound text nodes with mismatching quotes', () => { | | 570 | + expect(humanizeTplAst(parse(`{{ "{{a}}' }}`, []))).toEqual([[TextAst, `{{ "{{a}}' }}`]]); | | 571 | + }); | | 572 | + | | 573 | + it('should parse interpolation with escaped backslashes', () => { | | 574 | + expect(humanizeTplAst(parse(`{{foo.split('\\\\')}}`, []))).toEqual([ | | 575 | + [BoundTextAst, `{{ foo.split("\\") }}`] | | 576 | + ]); | | 577 | + expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\')}}`, []))).toEqual([ | | 578 | + [BoundTextAst, `{{ foo.split("\\\\") }}`] | | 579 | + ]); | | 580 | + expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\\\\\')}}`, []))).toEqual([ | | 581 | + [BoundTextAst, `{{ foo.split("\\\\\\") }}`] | | 582 | + ]); | | 583 | + }); | | 584 | + | | 585 | + it('should ignore quotes inside a comment', () => { | | 586 | + expect(humanizeTplAst(parse(`"{{name // " }}"`, []))).toEqual([ | | 587 | + [BoundTextAst, `"{{ name }}"`] | | 588 | + ]); | | 589 | + }); | | 590 | + | 543 | 591 | it('should parse with custom interpolation config', | 544 | 592 | inject([TemplateParser], (parser: TemplateParser) => { | 545 | 593 | const component = CompileDirectiveMetadata.create({ | |
Collapse file Copy file name to clipboardExpand all lines: packages/core/test/acceptance/text_spec.ts +14Lines changed: 14 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -171,4 +171,18 @@ describe('text instructions', () => { | 171 | 171 | // `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `Symbol(hello)`. | 172 | 172 | expect(fixture.nativeElement.textContent).toContain('Symbol(hello)'); | 173 | 173 | }); | | 174 | + | | 175 | + it('should handle binding syntax used inside quoted text', () => { | | 176 | + @Component({ | | 177 | + template: `{{'Interpolations look like {{this}}'}}`, | | 178 | + }) | | 179 | + class App { | | 180 | + } | | 181 | + | | 182 | + TestBed.configureTestingModule({declarations: [App]}); | | 183 | + const fixture = TestBed.createComponent(App); | | 184 | + fixture.detectChanges(); | | 185 | + | | 186 | + expect(fixture.nativeElement.textContent).toBe('Interpolations look like {{this}}'); | | 187 | + }); | 174 | 188 | }); |
|