feat(compiler): Added spans to HTML parser errors
Allows using the HTML parser in contexts errors are reported in a development tool such as an editor.
This commit is contained in:
parent
a3d7629134
commit
19a08f3a43
|
@ -34,8 +34,8 @@ export class HtmlToken {
|
|||
}
|
||||
|
||||
export class HtmlTokenError extends ParseError {
|
||||
constructor(errorMsg: string, public tokenType: HtmlTokenType, location: ParseLocation) {
|
||||
super(location, errorMsg);
|
||||
constructor(errorMsg: string, public tokenType: HtmlTokenType, span: ParseSourceSpan) {
|
||||
super(span, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,8 @@ class _HtmlTokenizer {
|
|||
|
||||
private _processCarriageReturns(content: string): string {
|
||||
// http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
|
||||
// In order to keep the original position in the source, we can not pre-process it.
|
||||
// In order to keep the original position in the source, we can not
|
||||
// pre-process it.
|
||||
// Instead CRs are processed right before instantiating the tokens.
|
||||
return StringWrapper.replaceAll(content, CR_OR_CRLF_REGEXP, '\n');
|
||||
}
|
||||
|
@ -168,6 +169,16 @@ class _HtmlTokenizer {
|
|||
return new ParseLocation(this.file, this.index, this.line, this.column);
|
||||
}
|
||||
|
||||
private _getSpan(start?: ParseLocation, end?: ParseLocation): ParseSourceSpan {
|
||||
if (isBlank(start)) {
|
||||
start = this._getLocation();
|
||||
}
|
||||
if (isBlank(end)) {
|
||||
end = this._getLocation();
|
||||
}
|
||||
return new ParseSourceSpan(start, end);
|
||||
}
|
||||
|
||||
private _beginToken(type: HtmlTokenType, start: ParseLocation = null) {
|
||||
if (isBlank(start)) {
|
||||
start = this._getLocation();
|
||||
|
@ -188,8 +199,8 @@ class _HtmlTokenizer {
|
|||
return token;
|
||||
}
|
||||
|
||||
private _createError(msg: string, position: ParseLocation): ControlFlowError {
|
||||
var error = new HtmlTokenError(msg, this.currentTokenType, position);
|
||||
private _createError(msg: string, span: ParseSourceSpan): ControlFlowError {
|
||||
var error = new HtmlTokenError(msg, this.currentTokenType, span);
|
||||
this.currentTokenStart = null;
|
||||
this.currentTokenType = null;
|
||||
return new ControlFlowError(error);
|
||||
|
@ -197,7 +208,7 @@ class _HtmlTokenizer {
|
|||
|
||||
private _advance() {
|
||||
if (this.index >= this.length) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg($EOF), this._getLocation());
|
||||
throw this._createError(unexpectedCharacterErrorMsg($EOF), this._getSpan());
|
||||
}
|
||||
if (this.peek === $LF) {
|
||||
this.line++;
|
||||
|
@ -228,7 +239,8 @@ class _HtmlTokenizer {
|
|||
private _requireCharCode(charCode: number) {
|
||||
var location = this._getLocation();
|
||||
if (!this._attemptCharCode(charCode)) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), location);
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek),
|
||||
this._getSpan(location, location));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,7 +265,7 @@ class _HtmlTokenizer {
|
|||
private _requireStr(chars: string) {
|
||||
var location = this._getLocation();
|
||||
if (!this._attemptStr(chars)) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), location);
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(location));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,7 +279,7 @@ class _HtmlTokenizer {
|
|||
var start = this._getLocation();
|
||||
this._attemptCharCodeUntilFn(predicate);
|
||||
if (this.index - start.offset < len) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), start);
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(start, start));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,7 +307,7 @@ class _HtmlTokenizer {
|
|||
let numberStart = this._getLocation().offset;
|
||||
this._attemptCharCodeUntilFn(isDigitEntityEnd);
|
||||
if (this.peek != $SEMICOLON) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getLocation());
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan());
|
||||
}
|
||||
this._advance();
|
||||
let strNum = this.input.substring(numberStart, this.index - 1);
|
||||
|
@ -304,7 +316,7 @@ class _HtmlTokenizer {
|
|||
return StringWrapper.fromCharCode(charCode);
|
||||
} catch (e) {
|
||||
let entity = this.input.substring(start.offset + 1, this.index - 1);
|
||||
throw this._createError(unknownEntityErrorMsg(entity), start);
|
||||
throw this._createError(unknownEntityErrorMsg(entity), this._getSpan(start));
|
||||
}
|
||||
} else {
|
||||
let startPosition = this._savePosition();
|
||||
|
@ -317,7 +329,7 @@ class _HtmlTokenizer {
|
|||
let name = this.input.substring(start.offset + 1, this.index - 1);
|
||||
let char = NAMED_ENTITIES[name];
|
||||
if (isBlank(char)) {
|
||||
throw this._createError(unknownEntityErrorMsg(name), start);
|
||||
throw this._createError(unknownEntityErrorMsg(name), this._getSpan(start));
|
||||
}
|
||||
return char;
|
||||
}
|
||||
|
@ -394,7 +406,7 @@ class _HtmlTokenizer {
|
|||
let lowercaseTagName;
|
||||
try {
|
||||
if (!isAsciiLetter(this.peek)) {
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getLocation());
|
||||
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan());
|
||||
}
|
||||
var nameStart = this.index;
|
||||
this._consumeTagOpenStart(start);
|
||||
|
|
|
@ -19,13 +19,11 @@ import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util';
|
|||
import {HtmlTagDefinition, getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
|
||||
|
||||
export class HtmlTreeError extends ParseError {
|
||||
static create(elementName: string, location: ParseLocation, msg: string): HtmlTreeError {
|
||||
return new HtmlTreeError(elementName, location, msg);
|
||||
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
|
||||
return new HtmlTreeError(elementName, span, msg);
|
||||
}
|
||||
|
||||
constructor(public elementName: string, location: ParseLocation, msg: string) {
|
||||
super(location, msg);
|
||||
}
|
||||
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
|
||||
}
|
||||
|
||||
export class HtmlParseTreeResult {
|
||||
|
@ -146,7 +144,7 @@ class TreeBuilder {
|
|||
selfClosing = true;
|
||||
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
|
||||
this.errors.push(HtmlTreeError.create(
|
||||
fullName, startTagToken.sourceSpan.start,
|
||||
fullName, startTagToken.sourceSpan,
|
||||
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
|
||||
}
|
||||
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
|
||||
|
@ -189,10 +187,10 @@ class TreeBuilder {
|
|||
|
||||
if (getHtmlTagDefinition(fullName).isVoid) {
|
||||
this.errors.push(
|
||||
HtmlTreeError.create(fullName, endTagToken.sourceSpan.start,
|
||||
HtmlTreeError.create(fullName, endTagToken.sourceSpan,
|
||||
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
|
||||
} else if (!this._popElement(fullName)) {
|
||||
this.errors.push(HtmlTreeError.create(fullName, endTagToken.sourceSpan.start,
|
||||
this.errors.push(HtmlTreeError.create(fullName, endTagToken.sourceSpan,
|
||||
`Unexpected closing tag "${endTagToken.parts[1]}"`));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,20 @@ export class ParseSourceFile {
|
|||
constructor(public content: string, public url: string) {}
|
||||
}
|
||||
|
||||
export abstract class ParseError {
|
||||
constructor(public location: ParseLocation, public msg: string) {}
|
||||
export class ParseSourceSpan {
|
||||
constructor(public start: ParseLocation, public end: ParseLocation) {}
|
||||
|
||||
toString(): string {
|
||||
var source = this.location.file.content;
|
||||
var ctxStart = this.location.offset;
|
||||
return this.start.file.content.substring(this.start.offset, this.end.offset);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ParseError {
|
||||
constructor(public span: ParseSourceSpan, public msg: string) {}
|
||||
|
||||
toString(): string {
|
||||
var source = this.span.start.file.content;
|
||||
var ctxStart = this.span.start.offset;
|
||||
if (ctxStart > source.length - 1) {
|
||||
ctxStart = source.length - 1;
|
||||
}
|
||||
|
@ -44,17 +52,9 @@ export abstract class ParseError {
|
|||
}
|
||||
}
|
||||
|
||||
let context = source.substring(ctxStart, this.location.offset) + '[ERROR ->]' +
|
||||
source.substring(this.location.offset, ctxEnd + 1);
|
||||
let context = source.substring(ctxStart, this.span.start.offset) + '[ERROR ->]' +
|
||||
source.substring(this.span.start.offset, ctxEnd + 1);
|
||||
|
||||
return `${this.msg} ("${context}"): ${this.location}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseSourceSpan {
|
||||
constructor(public start: ParseLocation, public end: ParseLocation) {}
|
||||
|
||||
toString(): string {
|
||||
return this.start.file.content.substring(this.start.offset, this.end.offset);
|
||||
return `${this.msg} ("${context}"): ${this.span.start}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ var TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
|
|||
export const TEMPLATE_TRANSFORMS = CONST_EXPR(new OpaqueToken('TemplateTransforms'));
|
||||
|
||||
export class TemplateParseError extends ParseError {
|
||||
constructor(message: string, location: ParseLocation) { super(location, message); }
|
||||
constructor(message: string, span: ParseSourceSpan) { super(span, message); }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@ -128,7 +128,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
}
|
||||
|
||||
private _reportError(message: string, sourceSpan: ParseSourceSpan) {
|
||||
this.errors.push(new TemplateParseError(message, sourceSpan.start));
|
||||
this.errors.push(new TemplateParseError(message, sourceSpan));
|
||||
}
|
||||
|
||||
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||
|
|
|
@ -581,7 +581,8 @@ export function main() {
|
|||
let src = "111\n222\n333\nE\n444\n555\n666\n";
|
||||
let file = new ParseSourceFile(src, 'file://');
|
||||
let location = new ParseLocation(file, 12, 123, 456);
|
||||
let error = new HtmlTokenError('**ERROR**', null, location);
|
||||
let span = new ParseSourceSpan(location, location);
|
||||
let error = new HtmlTokenError('**ERROR**', null, span);
|
||||
expect(error.toString())
|
||||
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
|
||||
});
|
||||
|
@ -631,7 +632,9 @@ function tokenizeAndHumanizeLineColumn(input: string): any[] {
|
|||
|
||||
function tokenizeAndHumanizeErrors(input: string): any[] {
|
||||
return tokenizeHtml(input, 'someUrl')
|
||||
.errors.map(
|
||||
tokenError =>
|
||||
[<any>tokenError.tokenType, tokenError.msg, humanizeLineColumn(tokenError.location)]);
|
||||
.errors.map(tokenError => [
|
||||
<any>tokenError.tokenType,
|
||||
tokenError.msg,
|
||||
humanizeLineColumn(tokenError.span.start)
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -328,10 +328,10 @@ function humanizeErrors(errors: ParseError[]): any[] {
|
|||
return errors.map(error => {
|
||||
if (error instanceof HtmlTreeError) {
|
||||
// Parser errors
|
||||
return [<any>error.elementName, error.msg, humanizeLineColumn(error.location)];
|
||||
return [<any>error.elementName, error.msg, humanizeLineColumn(error.span.start)];
|
||||
}
|
||||
// Tokenizer errors
|
||||
return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.location)];
|
||||
return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.span.start)];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue