Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler): handle strings inside bindings that contain binding characters #39826

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 32 additions & 3 deletions packages/compiler/src/expression_parser/parser.ts
Expand Up @@ -244,10 +244,10 @@ export class Parser {

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

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 {
simpleExpressionChecker = IvySimpleExpressionChecker; //
simpleExpressionChecker = IvySimpleExpressionChecker;
}

/** Describes a stateful context an expression parser is in. */
Expand Down
40 changes: 39 additions & 1 deletion packages/compiler/test/expression_parser/parser_spec.ts
Expand Up @@ -838,6 +838,37 @@ describe('parser', () => {
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', () => {
crisbeto marked this conversation as resolved.
Show resolved Hide resolved
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("\\\\\\") }}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tip: this code would greatly benefit from String.raw`\no \escaping \here`. See MDN for details.

});

it('should not parse interpolation with mismatching quotes', () => {
expect(parseInterpolation(`{{ "{{a}}' }}`)).toBeNull();
});

it('should parse prefix/suffix with multiple interpolation', () => {
const originalExp = 'before {{ a }} middle {{ b }} after';
const ast = parseInterpolation(originalExp)!.ast;
Expand Down Expand Up @@ -895,6 +926,10 @@ describe('parser', () => {
it('should retain // in nested, unterminated strings', () => {
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
});

it('should ignore quotes inside a comment', () => {
checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`);
AndrewKushnir marked this conversation as resolved.
Show resolved Hide resolved
});
});
});

Expand Down Expand Up @@ -1075,8 +1110,11 @@ function parseSimpleBindingIvy(
}

function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp)!;
const ast = parseInterpolation(exp);
if (expected == null) expected = exp;
if (ast === null) {
throw Error(`Failed to parse expression "${exp}"`);
}
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler/test/template_parser/template_parser_spec.ts
Expand Up @@ -540,6 +540,54 @@ describe('TemplateParser', () => {
expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]);
});

it('should parse bound text nodes inside quotes', () => {
crisbeto marked this conversation as resolved.
Show resolved Hide resolved
crisbeto marked this conversation as resolved.
Show resolved Hide resolved
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',
inject([TemplateParser], (parser: TemplateParser) => {
const component = CompileDirectiveMetadata.create({
Expand Down
14 changes: 14 additions & 0 deletions packages/core/test/acceptance/text_spec.ts
Expand Up @@ -171,4 +171,18 @@ describe('text instructions', () => {
// `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `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}}');
});
});