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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user