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

+32-3
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

+39-1
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

+48
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

+14
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)