feat(compiler): support skipping leading trivia in template source-maps (#30095)
Leading trivia, such as whitespace or comments, is confusing for developers looking at source-mapped templates, since they expect the source-map segment to start after the trivia. This commit adds skipping trivial characters to the lexer; and then implements that in the template parser. PR Close #30095
This commit is contained in:
parent
acaf1aa530
commit
304a12f027
@ -95,6 +95,12 @@ export interface TokenizeOptions {
|
|||||||
* but the new line should increment the current line for source mapping.
|
* but the new line should increment the current line for source mapping.
|
||||||
*/
|
*/
|
||||||
escapedString?: boolean;
|
escapedString?: boolean;
|
||||||
|
/**
|
||||||
|
* An array of characters that should be considered as leading trivia.
|
||||||
|
* Leading trivia are characters that are not important to the developer, and so should not be
|
||||||
|
* included in source-map segments. A common example is whitespace.
|
||||||
|
*/
|
||||||
|
leadingTriviaChars?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tokenize(
|
export function tokenize(
|
||||||
@ -123,11 +129,11 @@ class _Tokenizer {
|
|||||||
private _cursor: CharacterCursor;
|
private _cursor: CharacterCursor;
|
||||||
private _tokenizeIcu: boolean;
|
private _tokenizeIcu: boolean;
|
||||||
private _interpolationConfig: InterpolationConfig;
|
private _interpolationConfig: InterpolationConfig;
|
||||||
|
private _leadingTriviaCodePoints: number[]|undefined;
|
||||||
private _currentTokenStart: CharacterCursor|null = null;
|
private _currentTokenStart: CharacterCursor|null = null;
|
||||||
private _currentTokenType: TokenType|null = null;
|
private _currentTokenType: TokenType|null = null;
|
||||||
private _expansionCaseStack: TokenType[] = [];
|
private _expansionCaseStack: TokenType[] = [];
|
||||||
private _inInterpolation: boolean = false;
|
private _inInterpolation: boolean = false;
|
||||||
|
|
||||||
tokens: Token[] = [];
|
tokens: Token[] = [];
|
||||||
errors: TokenError[] = [];
|
errors: TokenError[] = [];
|
||||||
|
|
||||||
@ -141,6 +147,8 @@ class _Tokenizer {
|
|||||||
options: TokenizeOptions) {
|
options: TokenizeOptions) {
|
||||||
this._tokenizeIcu = options.tokenizeExpansionForms || false;
|
this._tokenizeIcu = options.tokenizeExpansionForms || false;
|
||||||
this._interpolationConfig = options.interpolationConfig || DEFAULT_INTERPOLATION_CONFIG;
|
this._interpolationConfig = options.interpolationConfig || DEFAULT_INTERPOLATION_CONFIG;
|
||||||
|
this._leadingTriviaCodePoints =
|
||||||
|
options.leadingTriviaChars && options.leadingTriviaChars.map(c => c.codePointAt(0) || 0);
|
||||||
const range =
|
const range =
|
||||||
options.range || {endPos: _file.content.length, startPos: 0, startLine: 0, startCol: 0};
|
options.range || {endPos: _file.content.length, startPos: 0, startLine: 0, startCol: 0};
|
||||||
this._cursor = options.escapedString ? new EscapedCharacterCursor(_file, range) :
|
this._cursor = options.escapedString ? new EscapedCharacterCursor(_file, range) :
|
||||||
@ -236,8 +244,9 @@ class _Tokenizer {
|
|||||||
'Programming error - attempted to end a token which has no token type', null,
|
'Programming error - attempted to end a token which has no token type', null,
|
||||||
this._cursor.getSpan(this._currentTokenStart));
|
this._cursor.getSpan(this._currentTokenStart));
|
||||||
}
|
}
|
||||||
const token =
|
const token = new Token(
|
||||||
new Token(this._currentTokenType, parts, this._cursor.getSpan(this._currentTokenStart));
|
this._currentTokenType, parts,
|
||||||
|
this._cursor.getSpan(this._currentTokenStart, this._leadingTriviaCodePoints));
|
||||||
this.tokens.push(token);
|
this.tokens.push(token);
|
||||||
this._currentTokenStart = null;
|
this._currentTokenStart = null;
|
||||||
this._currentTokenType = null;
|
this._currentTokenType = null;
|
||||||
@ -772,7 +781,7 @@ interface CharacterCursor {
|
|||||||
/** Advance the cursor by one parsed character. */
|
/** Advance the cursor by one parsed character. */
|
||||||
advance(): void;
|
advance(): void;
|
||||||
/** Get a span from the marked start point to the current point. */
|
/** Get a span from the marked start point to the current point. */
|
||||||
getSpan(start?: this): ParseSourceSpan;
|
getSpan(start?: this, leadingTriviaCodePoints?: number[]): ParseSourceSpan;
|
||||||
/** Get the parsed characters from the marked start point to the current point. */
|
/** Get the parsed characters from the marked start point to the current point. */
|
||||||
getChars(start: this): string;
|
getChars(start: this): string;
|
||||||
/** The number of characters left before the end of the cursor. */
|
/** The number of characters left before the end of the cursor. */
|
||||||
@ -831,8 +840,14 @@ class PlainCharacterCursor implements CharacterCursor {
|
|||||||
|
|
||||||
init(): void { this.updatePeek(this.state); }
|
init(): void { this.updatePeek(this.state); }
|
||||||
|
|
||||||
getSpan(start?: this): ParseSourceSpan {
|
getSpan(start?: this, leadingTriviaCodePoints?: number[]): ParseSourceSpan {
|
||||||
start = start || this;
|
start = start || this;
|
||||||
|
if (leadingTriviaCodePoints) {
|
||||||
|
start = start.clone() as this;
|
||||||
|
while (this.diff(start) > 0 && leadingTriviaCodePoints.indexOf(start.peek()) !== -1) {
|
||||||
|
start.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ParseSourceSpan(
|
return new ParseSourceSpan(
|
||||||
new ParseLocation(start.file, start.state.offset, start.state.line, start.state.column),
|
new ParseLocation(start.file, start.state.offset, start.state.line, start.state.column),
|
||||||
new ParseLocation(this.file, this.state.offset, this.state.line, this.state.column));
|
new ParseLocation(this.file, this.state.offset, this.state.line, this.state.column));
|
||||||
|
@ -53,6 +53,8 @@ const NG_PROJECT_AS_ATTR_NAME = 'ngProjectAs';
|
|||||||
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
|
const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(
|
||||||
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
|
[['window', R3.resolveWindow], ['document', R3.resolveDocument], ['body', R3.resolveBody]]);
|
||||||
|
|
||||||
|
const LEADING_TRIVIA_CHARS = [' ', '\n', '\r', '\t'];
|
||||||
|
|
||||||
// if (rf & flags) { .. }
|
// if (rf & flags) { .. }
|
||||||
export function renderFlagCheckIfStmt(
|
export function renderFlagCheckIfStmt(
|
||||||
flags: core.RenderFlags, statements: o.Statement[]): o.IfStmt {
|
flags: core.RenderFlags, statements: o.Statement[]): o.IfStmt {
|
||||||
@ -1733,8 +1735,9 @@ export function parseTemplate(
|
|||||||
const {interpolationConfig, preserveWhitespaces} = options;
|
const {interpolationConfig, preserveWhitespaces} = options;
|
||||||
const bindingParser = makeBindingParser(interpolationConfig);
|
const bindingParser = makeBindingParser(interpolationConfig);
|
||||||
const htmlParser = new HtmlParser();
|
const htmlParser = new HtmlParser();
|
||||||
const parseResult =
|
const parseResult = htmlParser.parse(
|
||||||
htmlParser.parse(template, templateUrl, {...options, tokenizeExpansionForms: true});
|
template, templateUrl,
|
||||||
|
{...options, tokenizeExpansionForms: true, leadingTriviaChars: LEADING_TRIVIA_CHARS});
|
||||||
|
|
||||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||||
return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []};
|
return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []};
|
||||||
|
@ -52,6 +52,18 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u
|
|||||||
[lex.TokenType.EOF, '2:5'],
|
[lex.TokenType.EOF, '2:5'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip over leading trivia for source-span start', () => {
|
||||||
|
expect(tokenizeAndHumanizeLineColumn(
|
||||||
|
'<t>\n \t a</t>', {leadingTriviaChars: ['\n', ' ', '\t']}))
|
||||||
|
.toEqual([
|
||||||
|
[lex.TokenType.TAG_OPEN_START, '0:0'],
|
||||||
|
[lex.TokenType.TAG_OPEN_END, '0:2'],
|
||||||
|
[lex.TokenType.TEXT, '1:3'],
|
||||||
|
[lex.TokenType.TAG_CLOSE, '1:4'],
|
||||||
|
[lex.TokenType.EOF, '1:8'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('content ranges', () => {
|
describe('content ranges', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user