Skip to content

Commit 89c5255

Browse files
ayazhafizjosephperrott
authored andcommitted
refactor(compiler): iteratively parse interpolations (angular#38977)
This patch refactors the interpolation parser to do so iteratively rather than using a regex. Doing so prepares us for supporting granular recovery on poorly-formed interpolations, for example when an interpolation does not terminate (`{{ 1 + 2`) or is not terminated properly (`{{ 1 + 2 {{ 2 + 3 }}`). Part of angular#38596 PR Close angular#38977
1 parent 5dbf357 commit 89c5255

File tree

2 files changed

+70
-28
lines changed

2 files changed

+70
-28
lines changed

packages/compiler/src/expression_parser/parser.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -185,47 +185,82 @@ export class Parser {
185185
location, absoluteOffset, this.errors);
186186
}
187187

188+
/**
189+
* Splits a string of text into "raw" text segments and expressions present in interpolations in
190+
* the string.
191+
* Returns `null` if there are no interpolations, otherwise a
192+
* `SplitInterpolation` with splits that look like
193+
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
194+
*/
188195
splitInterpolation(
189196
input: string, location: string,
190197
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
191198
|null {
192-
const regexp = _getInterpolateRegExp(interpolationConfig);
193-
const parts = input.split(regexp);
194-
if (parts.length <= 1) {
195-
return null;
196-
}
197199
const strings: string[] = [];
198200
const expressions: string[] = [];
199201
const offsets: number[] = [];
200202
const stringSpans: {start: number, end: number}[] = [];
201203
const expressionSpans: {start: number, end: number}[] = [];
202-
let offset = 0;
203-
for (let i = 0; i < parts.length; i++) {
204-
const part: string = parts[i];
205-
if (i % 2 === 0) {
206-
// fixed string
204+
let i = 0;
205+
let atInterpolation = false;
206+
let extendLastString = false;
207+
let {start: interpStart, end: interpEnd} = interpolationConfig;
208+
while (i < input.length) {
209+
if (!atInterpolation) {
210+
// parse until starting {{
211+
const start = i;
212+
i = input.indexOf(interpStart, i);
213+
if (i === -1) {
214+
i = input.length;
215+
}
216+
const part = input.substring(start, i);
207217
strings.push(part);
208-
const start = offset;
209-
offset += part.length;
210-
stringSpans.push({start, end: offset});
211-
} else if (part.trim().length > 0) {
212-
const start = offset;
213-
offset += interpolationConfig.start.length;
214-
expressions.push(part);
215-
offsets.push(offset);
216-
offset += part.length + interpolationConfig.end.length;
217-
expressionSpans.push({start, end: offset});
218+
stringSpans.push({start, end: i});
219+
220+
atInterpolation = true;
221+
} else {
222+
// parse from starting {{ to ending }}
223+
const fullStart = i;
224+
const exprStart = fullStart + interpStart.length;
225+
const exprEnd = input.indexOf(interpEnd, exprStart);
226+
if (exprEnd === -1) {
227+
// Could not find the end of the interpolation; do not parse an expression.
228+
// Instead we should extend the content on the last raw string.
229+
atInterpolation = false;
230+
extendLastString = true;
231+
break;
232+
}
233+
const fullEnd = exprEnd + interpEnd.length;
234+
235+
const part = input.substring(exprStart, exprEnd);
236+
if (part.trim().length > 0) {
237+
expressions.push(part);
238+
} else {
239+
this._reportError(
240+
'Blank expressions are not allowed in interpolated strings', input,
241+
`at column ${i} in`, location);
242+
expressions.push('$implicit');
243+
}
244+
offsets.push(exprStart);
245+
expressionSpans.push({start: fullStart, end: fullEnd});
246+
247+
i = fullEnd;
248+
atInterpolation = false;
249+
}
250+
}
251+
if (!atInterpolation) {
252+
// If we are now at a text section, add the remaining content as a raw string.
253+
if (extendLastString) {
254+
strings[strings.length - 1] += input.substring(i);
255+
stringSpans[stringSpans.length - 1].end = input.length;
218256
} else {
219-
this._reportError(
220-
'Blank expressions are not allowed in interpolated strings', input,
221-
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
222-
location);
223-
expressions.push('$implicit');
224-
offsets.push(offset);
225-
expressionSpans.push({start: offset, end: offset});
257+
strings.push(input.substring(i));
258+
stringSpans.push({start: i, end: input.length});
226259
}
227260
}
228-
return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
261+
return expressions.length === 0 ?
262+
null :
263+
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
229264
}
230265

231266
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {

packages/compiler/test/expression_parser/parser_spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,13 @@ describe('parser', () => {
728728
expect(parseInterpolation('nothing')).toBe(null);
729729
});
730730

731+
it('should not parse malformed interpolations as strings', () => {
732+
const ast = parseInterpolation('{{a}} {{example}<!--->}')!.ast as Interpolation;
733+
expect(ast.strings).toEqual(['', ' {{example}<!--->}']);
734+
expect(ast.expressions.length).toEqual(1);
735+
expect(ast.expressions[0].name).toEqual('a');
736+
});
737+
731738
it('should parse no prefix/suffix interpolation', () => {
732739
const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
733740
expect(ast.strings).toEqual(['', '']);

0 commit comments

Comments
 (0)