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
This commit is contained in:
parent
93a83266f9
commit
dc6d40e5bc
@ -244,10 +244,10 @@ export class Parser {
|
|||||||
|
|
||||||
atInterpolation = true;
|
atInterpolation = true;
|
||||||
} else {
|
} else {
|
||||||
// parse from starting {{ to ending }}
|
// parse from starting {{ to ending }} while ignoring content inside quotes.
|
||||||
const fullStart = i;
|
const fullStart = i;
|
||||||
const exprStart = fullStart + interpStart.length;
|
const exprStart = fullStart + interpStart.length;
|
||||||
const exprEnd = input.indexOf(interpEnd, exprStart);
|
const exprEnd = this._getExpressiondEndIndex(input, interpEnd, exprStart);
|
||||||
if (exprEnd === -1) {
|
if (exprEnd === -1) {
|
||||||
// Could not find the end of the interpolation; do not parse an expression.
|
// Could not find the end of the interpolation; do not parse an expression.
|
||||||
// Instead we should extend the content on the last raw string.
|
// Instead we should extend the content on the last raw string.
|
||||||
@ -340,10 +340,39 @@ export class Parser {
|
|||||||
|
|
||||||
return errLocation.length;
|
return errLocation.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the index of the end of an interpolation expression
|
||||||
|
* while ignoring comments and quoted content.
|
||||||
|
*/
|
||||||
|
private _getExpressiondEndIndex(input: string, expressionEnd: string, start: number): number {
|
||||||
|
let currentQuote: string|null = null;
|
||||||
|
let escapeCount = 0;
|
||||||
|
for (let i = start; i < input.length; i++) {
|
||||||
|
const char = input[i];
|
||||||
|
// Skip the characters inside quotes. Note that we only care about the
|
||||||
|
// outer-most quotes matching up and we need to account for escape characters.
|
||||||
|
if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) &&
|
||||||
|
escapeCount % 2 === 0) {
|
||||||
|
currentQuote = currentQuote === null ? char : null;
|
||||||
|
} else if (currentQuote === null) {
|
||||||
|
if (input.startsWith(expressionEnd, i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
// Nothing else in the expression matters after we've
|
||||||
|
// hit a comment so look directly for the end token.
|
||||||
|
if (input.startsWith('//', i)) {
|
||||||
|
return input.indexOf(expressionEnd, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
escapeCount = char === '\\' ? escapeCount + 1 : 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IvyParser extends Parser {
|
export class IvyParser extends Parser {
|
||||||
simpleExpressionChecker = IvySimpleExpressionChecker; //
|
simpleExpressionChecker = IvySimpleExpressionChecker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Describes a stateful context an expression parser is in. */
|
/** Describes a stateful context an expression parser is in. */
|
||||||
|
@ -838,6 +838,37 @@ describe('parser', () => {
|
|||||||
expect(ast.expressions[0].name).toEqual('a');
|
expect(ast.expressions[0].name).toEqual('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse interpolation inside quotes', () => {
|
||||||
|
const ast = parseInterpolation('"{{a}}"')!.ast as Interpolation;
|
||||||
|
expect(ast.strings).toEqual(['"', '"']);
|
||||||
|
expect(ast.expressions.length).toEqual(1);
|
||||||
|
expect(ast.expressions[0].name).toEqual('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse interpolation with interpolation characters inside quotes', () => {
|
||||||
|
checkInterpolation('{{"{{a}}"}}', '{{ "{{a}}" }}');
|
||||||
|
checkInterpolation('{{"{{"}}', '{{ "{{" }}');
|
||||||
|
checkInterpolation('{{"}}"}}', '{{ "}}" }}');
|
||||||
|
checkInterpolation('{{"{"}}', '{{ "{" }}');
|
||||||
|
checkInterpolation('{{"}"}}', '{{ "}" }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse interpolation with escaped quotes', () => {
|
||||||
|
checkInterpolation(`{{'It\\'s just Angular'}}`, `{{ "It's just Angular" }}`);
|
||||||
|
checkInterpolation(`{{'It\\'s {{ just Angular'}}`, `{{ "It's {{ just Angular" }}`);
|
||||||
|
checkInterpolation(`{{'It\\'s }} just Angular'}}`, `{{ "It's }} just Angular" }}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse interpolation with escaped backslashes', () => {
|
||||||
|
checkInterpolation(`{{foo.split('\\\\')}}`, `{{ foo.split("\\") }}`);
|
||||||
|
checkInterpolation(`{{foo.split('\\\\\\\\')}}`, `{{ foo.split("\\\\") }}`);
|
||||||
|
checkInterpolation(`{{foo.split('\\\\\\\\\\\\')}}`, `{{ foo.split("\\\\\\") }}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not parse interpolation with mismatching quotes', () => {
|
||||||
|
expect(parseInterpolation(`{{ "{{a}}' }}`)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse prefix/suffix with multiple interpolation', () => {
|
it('should parse prefix/suffix with multiple interpolation', () => {
|
||||||
const originalExp = 'before {{ a }} middle {{ b }} after';
|
const originalExp = 'before {{ a }} middle {{ b }} after';
|
||||||
const ast = parseInterpolation(originalExp)!.ast;
|
const ast = parseInterpolation(originalExp)!.ast;
|
||||||
@ -895,6 +926,10 @@ describe('parser', () => {
|
|||||||
it('should retain // in nested, unterminated strings', () => {
|
it('should retain // in nested, unterminated strings', () => {
|
||||||
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
|
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore quotes inside a comment', () => {
|
||||||
|
checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1075,8 +1110,11 @@ function parseSimpleBindingIvy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkInterpolation(exp: string, expected?: string) {
|
function checkInterpolation(exp: string, expected?: string) {
|
||||||
const ast = parseInterpolation(exp)!;
|
const ast = parseInterpolation(exp);
|
||||||
if (expected == null) expected = exp;
|
if (expected == null) expected = exp;
|
||||||
|
if (ast === null) {
|
||||||
|
throw Error(`Failed to parse expression "${exp}"`);
|
||||||
|
}
|
||||||
expect(unparse(ast)).toEqual(expected);
|
expect(unparse(ast)).toEqual(expected);
|
||||||
validate(ast);
|
validate(ast);
|
||||||
}
|
}
|
||||||
|
@ -540,6 +540,54 @@ describe('TemplateParser', () => {
|
|||||||
expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]);
|
expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse bound text nodes inside quotes', () => {
|
||||||
|
expect(humanizeTplAst(parse('"{{a}}"', []))).toEqual([[BoundTextAst, '"{{ a }}"']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse bound text nodes with interpolations inside quotes', () => {
|
||||||
|
expect(humanizeTplAst(parse('{{ "{{a}}" }}', []))).toEqual([[BoundTextAst, '{{ "{{a}}" }}']]);
|
||||||
|
expect(humanizeTplAst(parse('{{"{{"}}', []))).toEqual([[BoundTextAst, '{{ "{{" }}']]);
|
||||||
|
expect(humanizeTplAst(parse('{{"}}"}}', []))).toEqual([[BoundTextAst, '{{ "}}" }}']]);
|
||||||
|
expect(humanizeTplAst(parse('{{"{"}}', []))).toEqual([[BoundTextAst, '{{ "{" }}']]);
|
||||||
|
expect(humanizeTplAst(parse('{{"}"}}', []))).toEqual([[BoundTextAst, '{{ "}" }}']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse bound text nodes with escaped quotes', () => {
|
||||||
|
expect(humanizeTplAst(parse(`{{'It\\'s just Angular'}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ "It's just Angular" }}`]
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(humanizeTplAst(parse(`{{'It\\'s {{ just Angular'}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ "It's {{ just Angular" }}`]
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(humanizeTplAst(parse(`{{'It\\'s }} just Angular'}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ "It's }} just Angular" }}`]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not parse bound text nodes with mismatching quotes', () => {
|
||||||
|
expect(humanizeTplAst(parse(`{{ "{{a}}' }}`, []))).toEqual([[TextAst, `{{ "{{a}}' }}`]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse interpolation with escaped backslashes', () => {
|
||||||
|
expect(humanizeTplAst(parse(`{{foo.split('\\\\')}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ foo.split("\\") }}`]
|
||||||
|
]);
|
||||||
|
expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\')}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ foo.split("\\\\") }}`]
|
||||||
|
]);
|
||||||
|
expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\\\\\')}}`, []))).toEqual([
|
||||||
|
[BoundTextAst, `{{ foo.split("\\\\\\") }}`]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore quotes inside a comment', () => {
|
||||||
|
expect(humanizeTplAst(parse(`"{{name // " }}"`, []))).toEqual([
|
||||||
|
[BoundTextAst, `"{{ name }}"`]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse with custom interpolation config',
|
it('should parse with custom interpolation config',
|
||||||
inject([TemplateParser], (parser: TemplateParser) => {
|
inject([TemplateParser], (parser: TemplateParser) => {
|
||||||
const component = CompileDirectiveMetadata.create({
|
const component = CompileDirectiveMetadata.create({
|
||||||
|
@ -171,4 +171,18 @@ describe('text instructions', () => {
|
|||||||
// `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `Symbol(hello)`.
|
// `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `Symbol(hello)`.
|
||||||
expect(fixture.nativeElement.textContent).toContain('Symbol(hello)');
|
expect(fixture.nativeElement.textContent).toContain('Symbol(hello)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle binding syntax used inside quoted text', () => {
|
||||||
|
@Component({
|
||||||
|
template: `{{'Interpolations look like {{this}}'}}`,
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('Interpolations look like {{this}}');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user