diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts
index ebd1c5825da7d..2fa7e36b5e8bc 100644
--- a/packages/compiler/src/expression_parser/parser.ts
+++ b/packages/compiler/src/expression_parser/parser.ts
@@ -8,7 +8,6 @@
import * as chars from '../chars';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
-import {escapeRegExp} from '../util';
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
@@ -30,20 +29,6 @@ export class TemplateBindingParseResult {
public errors: ParserError[]) {}
}
-const defaultInterpolateRegExp = _createInterpolateRegExp(DEFAULT_INTERPOLATION_CONFIG);
-function _getInterpolateRegExp(config: InterpolationConfig): RegExp {
- if (config === DEFAULT_INTERPOLATION_CONFIG) {
- return defaultInterpolateRegExp;
- } else {
- return _createInterpolateRegExp(config);
- }
-}
-
-function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
- const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
- return new RegExp(pattern, 'g');
-}
-
export class Parser {
private errors: ParserError[] = [];
@@ -247,7 +232,7 @@ export class Parser {
// parse from starting {{ to ending }} while ignoring content inside quotes.
const fullStart = i;
const exprStart = fullStart + interpStart.length;
- const exprEnd = this._getExpressiondEndIndex(input, interpEnd, exprStart);
+ const exprEnd = this._getInterpolationEndIndex(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.
@@ -315,59 +300,71 @@ export class Parser {
return null;
}
- private _checkNoInterpolation(
- input: string, location: string, interpolationConfig: InterpolationConfig): void {
- const regexp = _getInterpolateRegExp(interpolationConfig);
- const parts = input.split(regexp);
- if (parts.length > 1) {
+ private _checkNoInterpolation(input: string, location: string, {start, end}: InterpolationConfig):
+ void {
+ let startIndex = -1;
+ let endIndex = -1;
+
+ for (const charIndex of this._forEachUnquotedChar(input, 0)) {
+ if (startIndex === -1) {
+ if (input.startsWith(start)) {
+ startIndex = charIndex;
+ }
+ } else {
+ endIndex = this._getInterpolationEndIndex(input, end, charIndex);
+ if (endIndex > -1) {
+ break;
+ }
+ }
+ }
+
+ if (startIndex > -1 && endIndex > -1) {
this._reportError(
- `Got interpolation (${interpolationConfig.start}${
- interpolationConfig.end}) where expression was expected`,
- input,
- `at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
- location);
+ `Got interpolation (${start}${end}) where expression was expected`, input,
+ `at column ${startIndex} in`, location);
}
}
- private _findInterpolationErrorColumn(
- parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
- let errLocation = '';
- for (let j = 0; j < partInErrIdx; j++) {
- errLocation += j % 2 === 0 ?
- parts[j] :
- `${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
+ /**
+ * Finds the index of the end of an interpolation expression
+ * while ignoring comments and quoted content.
+ */
+ private _getInterpolationEndIndex(input: string, expressionEnd: string, start: number): number {
+ for (const charIndex of this._forEachUnquotedChar(input, start)) {
+ if (input.startsWith(expressionEnd, charIndex)) {
+ return charIndex;
+ }
+
+ // Nothing else in the expression matters after we've
+ // hit a comment so look directly for the end token.
+ if (input.startsWith('//', charIndex)) {
+ return input.indexOf(expressionEnd, charIndex);
+ }
}
- return errLocation.length;
+ return -1;
}
/**
- * Finds the index of the end of an interpolation expression
- * while ignoring comments and quoted content.
+ * Generator used to iterate over the character indexes of a string that are outside of quotes.
+ * @param input String to loop through.
+ * @param start Index within the string at which to start.
*/
- private _getExpressiondEndIndex(input: string, expressionEnd: string, start: number): number {
+ private * _forEachUnquotedChar(input: string, start: 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.
+ // 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);
- }
+ yield i;
}
escapeCount = char === '\\' ? escapeCount + 1 : 0;
}
- return -1;
}
}
diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts
index 6e7eda3406869..b4421d198a392 100644
--- a/packages/compiler/test/expression_parser/parser_spec.ts
+++ b/packages/compiler/test/expression_parser/parser_spec.ts
@@ -281,6 +281,13 @@ describe('parser', () => {
it('should report when encountering interpolation', () => {
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
});
+
+ it('should not report interpolation inside a string', () => {
+ expect(parseAction(`"{{a()}}"`).errors).toEqual([]);
+ expect(parseAction(`'{{a()}}'`).errors).toEqual([]);
+ expect(parseAction(`"{{a('\\"')}}"`).errors).toEqual([]);
+ expect(parseAction(`'{{a("\\'")}}'`).errors).toEqual([]);
+ });
});
describe('parse spans', () => {
@@ -410,6 +417,13 @@ describe('parser', () => {
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
});
+ it('should not report interpolation inside a string', () => {
+ expect(parseBinding(`"{{exp}}"`).errors).toEqual([]);
+ expect(parseBinding(`'{{exp}}'`).errors).toEqual([]);
+ expect(parseBinding(`'{{\\"}}'`).errors).toEqual([]);
+ expect(parseBinding(`'{{\\'}}'`).errors).toEqual([]);
+ });
+
it('should parse conditional expression', () => {
checkBinding('a < b ? a : b');
});
@@ -876,6 +890,13 @@ describe('parser', () => {
'Got interpolation ({{}}) where expression was expected');
});
+ it('should not report interpolation inside a string', () => {
+ expect(parseSimpleBinding(`"{{exp}}"`).errors).toEqual([]);
+ expect(parseSimpleBinding(`'{{exp}}'`).errors).toEqual([]);
+ expect(parseSimpleBinding(`'{{\\"}}'`).errors).toEqual([]);
+ expect(parseSimpleBinding(`'{{\\'}}'`).errors).toEqual([]);
+ });
+
it('should report when encountering field write', () => {
expectError(validate(parseSimpleBinding('a = b')), 'Bindings cannot contain assignments');
});
diff --git a/packages/core/test/acceptance/property_binding_spec.ts b/packages/core/test/acceptance/property_binding_spec.ts
index ddb9a18ffeed6..fdf7ca9a4b3fa 100644
--- a/packages/core/test/acceptance/property_binding_spec.ts
+++ b/packages/core/test/acceptance/property_binding_spec.ts
@@ -623,4 +623,26 @@ describe('property bindings', () => {
fixture.detectChanges();
}).not.toThrow();
});
+
+ it('should allow quoted binding syntax inside property binding', () => {
+ @Component({template: ``})
+ class Comp {
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp]});
+ const fixture = TestBed.createComponent(Comp);
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelector('span').id).toBe('{{ id }}');
+ });
+
+ it('should allow quoted binding syntax with escaped quotes inside property binding', () => {
+ @Component({template: ``})
+ class Comp {
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp]});
+ const fixture = TestBed.createComponent(Comp);
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelector('span').id).toBe('{{ \' }}');
+ });
});