refactor(compiler): iteratively parse interpolations (#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 #38596 PR Close #38977
This commit is contained in:
parent
5dbf357224
commit
89c5255b8c
|
@ -185,47 +185,82 @@ export class Parser {
|
||||||
location, absoluteOffset, this.errors);
|
location, absoluteOffset, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a string of text into "raw" text segments and expressions present in interpolations in
|
||||||
|
* the string.
|
||||||
|
* Returns `null` if there are no interpolations, otherwise a
|
||||||
|
* `SplitInterpolation` with splits that look like
|
||||||
|
* <raw text> <expression> <raw text> ... <raw text> <expression> <raw text>
|
||||||
|
*/
|
||||||
splitInterpolation(
|
splitInterpolation(
|
||||||
input: string, location: string,
|
input: string, location: string,
|
||||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation
|
||||||
|null {
|
|null {
|
||||||
const regexp = _getInterpolateRegExp(interpolationConfig);
|
|
||||||
const parts = input.split(regexp);
|
|
||||||
if (parts.length <= 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const strings: string[] = [];
|
const strings: string[] = [];
|
||||||
const expressions: string[] = [];
|
const expressions: string[] = [];
|
||||||
const offsets: number[] = [];
|
const offsets: number[] = [];
|
||||||
const stringSpans: {start: number, end: number}[] = [];
|
const stringSpans: {start: number, end: number}[] = [];
|
||||||
const expressionSpans: {start: number, end: number}[] = [];
|
const expressionSpans: {start: number, end: number}[] = [];
|
||||||
let offset = 0;
|
let i = 0;
|
||||||
for (let i = 0; i < parts.length; i++) {
|
let atInterpolation = false;
|
||||||
const part: string = parts[i];
|
let extendLastString = false;
|
||||||
if (i % 2 === 0) {
|
let {start: interpStart, end: interpEnd} = interpolationConfig;
|
||||||
// fixed string
|
while (i < input.length) {
|
||||||
|
if (!atInterpolation) {
|
||||||
|
// parse until starting {{
|
||||||
|
const start = i;
|
||||||
|
i = input.indexOf(interpStart, i);
|
||||||
|
if (i === -1) {
|
||||||
|
i = input.length;
|
||||||
|
}
|
||||||
|
const part = input.substring(start, i);
|
||||||
strings.push(part);
|
strings.push(part);
|
||||||
const start = offset;
|
stringSpans.push({start, end: i});
|
||||||
offset += part.length;
|
|
||||||
stringSpans.push({start, end: offset});
|
atInterpolation = true;
|
||||||
} else if (part.trim().length > 0) {
|
|
||||||
const start = offset;
|
|
||||||
offset += interpolationConfig.start.length;
|
|
||||||
expressions.push(part);
|
|
||||||
offsets.push(offset);
|
|
||||||
offset += part.length + interpolationConfig.end.length;
|
|
||||||
expressionSpans.push({start, end: offset});
|
|
||||||
} else {
|
} else {
|
||||||
this._reportError(
|
// parse from starting {{ to ending }}
|
||||||
'Blank expressions are not allowed in interpolated strings', input,
|
const fullStart = i;
|
||||||
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
|
const exprStart = fullStart + interpStart.length;
|
||||||
location);
|
const exprEnd = input.indexOf(interpEnd, exprStart);
|
||||||
expressions.push('$implicit');
|
if (exprEnd === -1) {
|
||||||
offsets.push(offset);
|
// Could not find the end of the interpolation; do not parse an expression.
|
||||||
expressionSpans.push({start: offset, end: offset});
|
// Instead we should extend the content on the last raw string.
|
||||||
|
atInterpolation = false;
|
||||||
|
extendLastString = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const fullEnd = exprEnd + interpEnd.length;
|
||||||
|
|
||||||
|
const part = input.substring(exprStart, exprEnd);
|
||||||
|
if (part.trim().length > 0) {
|
||||||
|
expressions.push(part);
|
||||||
|
} else {
|
||||||
|
this._reportError(
|
||||||
|
'Blank expressions are not allowed in interpolated strings', input,
|
||||||
|
`at column ${i} in`, location);
|
||||||
|
expressions.push('$implicit');
|
||||||
|
}
|
||||||
|
offsets.push(exprStart);
|
||||||
|
expressionSpans.push({start: fullStart, end: fullEnd});
|
||||||
|
|
||||||
|
i = fullEnd;
|
||||||
|
atInterpolation = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
|
if (!atInterpolation) {
|
||||||
|
// If we are now at a text section, add the remaining content as a raw string.
|
||||||
|
if (extendLastString) {
|
||||||
|
strings[strings.length - 1] += input.substring(i);
|
||||||
|
stringSpans[stringSpans.length - 1].end = input.length;
|
||||||
|
} else {
|
||||||
|
strings.push(input.substring(i));
|
||||||
|
stringSpans.push({start: i, end: input.length});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expressions.length === 0 ?
|
||||||
|
null :
|
||||||
|
new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
|
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
|
||||||
|
|
|
@ -728,6 +728,13 @@ describe('parser', () => {
|
||||||
expect(parseInterpolation('nothing')).toBe(null);
|
expect(parseInterpolation('nothing')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not parse malformed interpolations as strings', () => {
|
||||||
|
const ast = parseInterpolation('{{a}} {{example}<!--->}')!.ast as Interpolation;
|
||||||
|
expect(ast.strings).toEqual(['', ' {{example}<!--->}']);
|
||||||
|
expect(ast.expressions.length).toEqual(1);
|
||||||
|
expect(ast.expressions[0].name).toEqual('a');
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse no prefix/suffix interpolation', () => {
|
it('should parse no prefix/suffix interpolation', () => {
|
||||||
const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
|
const ast = parseInterpolation('{{a}}')!.ast as Interpolation;
|
||||||
expect(ast.strings).toEqual(['', '']);
|
expect(ast.strings).toEqual(['', '']);
|
||||||
|
|
Loading…
Reference in New Issue