Skip to content

Commit f5aab2b

Browse files
crisbetoalxhub
authored andcommitted
fix(compiler): handle strings inside bindings that contain binding characters (#39826)
Currently the compiler treats something like `{{ '{{a}}' }}` as a nested binding and throws an error, because it doesn't account for quotes when it looks for binding characters. These changes add a bit of logic to skip over text inside quotes when parsing. Fixes #39601. PR Close #39826
1 parent 46fcfe0 commit f5aab2b

File tree

4 files changed

+133
-4
lines changed

4 files changed

+133
-4
lines changed

packages/compiler/src/expression_parser/parser.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ export class Parser {
244244

245245
atInterpolation = true;
246246
} else {
247-
// parse from starting {{ to ending }}
247+
// parse from starting {{ to ending }} while ignoring content inside quotes.
248248
const fullStart = i;
249249
const exprStart = fullStart + interpStart.length;
250-
const exprEnd = input.indexOf(interpEnd, exprStart);
250+
const exprEnd = this._getExpressiondEndIndex(input, interpEnd, exprStart);
251251
if (exprEnd === -1) {
252252
// Could not find the end of the interpolation; do not parse an expression.
253253
// Instead we should extend the content on the last raw string.
@@ -340,10 +340,39 @@ export class Parser {
340340

341341
return errLocation.length;
342342
}
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+
}
343372
}
344373

345374
export class IvyParser extends Parser {
346-
simpleExpressionChecker = IvySimpleExpressionChecker; //
375+
simpleExpressionChecker = IvySimpleExpressionChecker;
347376
}
348377

349378
/** Describes a stateful context an expression parser is in. */

packages/compiler/test/expression_parser/parser_spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,37 @@ describe('parser', () => {
762762
expect(ast.expressions[0].name).toEqual('a');
763763
});
764764

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+
765796
it('should parse prefix/suffix with multiple interpolation', () => {
766797
const originalExp = 'before {{ a }} middle {{ b }} after';
767798
const ast = parseInterpolation(originalExp)!.ast;
@@ -819,6 +850,10 @@ describe('parser', () => {
819850
it('should retain // in nested, unterminated strings', () => {
820851
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
821852
});
853+
854+
it('should ignore quotes inside a comment', () => {
855+
checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`);
856+
});
822857
});
823858
});
824859

@@ -999,8 +1034,11 @@ function parseSimpleBindingIvy(
9991034
}
10001035

10011036
function checkInterpolation(exp: string, expected?: string) {
1002-
const ast = parseInterpolation(exp)!;
1037+
const ast = parseInterpolation(exp);
10031038
if (expected == null) expected = exp;
1039+
if (ast === null) {
1040+
throw Error(`Failed to parse expression "${exp}"`);
1041+
}
10041042
expect(unparse(ast)).toEqual(expected);
10051043
validate(ast);
10061044
}

packages/compiler/test/template_parser/template_parser_spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,54 @@ describe('TemplateParser', () => {
540540
expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]);
541541
});
542542

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+
543591
it('should parse with custom interpolation config',
544592
inject([TemplateParser], (parser: TemplateParser) => {
545593
const component = CompileDirectiveMetadata.create({

packages/core/test/acceptance/text_spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,18 @@ describe('text instructions', () => {
171171
// `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `Symbol(hello)`.
172172
expect(fixture.nativeElement.textContent).toContain('Symbol(hello)');
173173
});
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+
});
174188
});

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