Skip to content

Commit dc6d40e

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 93a8326 commit dc6d40e

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
@@ -838,6 +838,37 @@ describe('parser', () => {
838838
expect(ast.expressions[0].name).toEqual('a');
839839
});
840840

841+
it('should parse interpolation inside quotes', () => {
842+
const ast = parseInterpolation('"{{a}}"')!.ast as Interpolation;
843+
expect(ast.strings).toEqual(['"', '"']);
844+
expect(ast.expressions.length).toEqual(1);
845+
expect(ast.expressions[0].name).toEqual('a');
846+
});
847+
848+
it('should parse interpolation with interpolation characters inside quotes', () => {
849+
checkInterpolation('{{"{{a}}"}}', '{{ "{{a}}" }}');
850+
checkInterpolation('{{"{{"}}', '{{ "{{" }}');
851+
checkInterpolation('{{"}}"}}', '{{ "}}" }}');
852+
checkInterpolation('{{"{"}}', '{{ "{" }}');
853+
checkInterpolation('{{"}"}}', '{{ "}" }}');
854+
});
855+
856+
it('should parse interpolation with escaped quotes', () => {
857+
checkInterpolation(`{{'It\\'s just Angular'}}`, `{{ "It's just Angular" }}`);
858+
checkInterpolation(`{{'It\\'s {{ just Angular'}}`, `{{ "It's {{ just Angular" }}`);
859+
checkInterpolation(`{{'It\\'s }} just Angular'}}`, `{{ "It's }} just Angular" }}`);
860+
});
861+
862+
it('should parse interpolation with escaped backslashes', () => {
863+
checkInterpolation(`{{foo.split('\\\\')}}`, `{{ foo.split("\\") }}`);
864+
checkInterpolation(`{{foo.split('\\\\\\\\')}}`, `{{ foo.split("\\\\") }}`);
865+
checkInterpolation(`{{foo.split('\\\\\\\\\\\\')}}`, `{{ foo.split("\\\\\\") }}`);
866+
});
867+
868+
it('should not parse interpolation with mismatching quotes', () => {
869+
expect(parseInterpolation(`{{ "{{a}}' }}`)).toBeNull();
870+
});
871+
841872
it('should parse prefix/suffix with multiple interpolation', () => {
842873
const originalExp = 'before {{ a }} middle {{ b }} after';
843874
const ast = parseInterpolation(originalExp)!.ast;
@@ -895,6 +926,10 @@ describe('parser', () => {
895926
it('should retain // in nested, unterminated strings', () => {
896927
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
897928
});
929+
930+
it('should ignore quotes inside a comment', () => {
931+
checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`);
932+
});
898933
});
899934
});
900935

@@ -1075,8 +1110,11 @@ function parseSimpleBindingIvy(
10751110
}
10761111

10771112
function checkInterpolation(exp: string, expected?: string) {
1078-
const ast = parseInterpolation(exp)!;
1113+
const ast = parseInterpolation(exp);
10791114
if (expected == null) expected = exp;
1115+
if (ast === null) {
1116+
throw Error(`Failed to parse expression "${exp}"`);
1117+
}
10801118
expect(unparse(ast)).toEqual(expected);
10811119
validate(ast);
10821120
}

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