feat(html_parser): change HtmlElementAst to store both the start and the end positions

This commit is contained in:
vsavkin 2016-03-23 13:43:28 -07:00 committed by Victor Savkin
parent a1880c3576
commit 17c8ec8a5d
5 changed files with 104 additions and 76 deletions

View File

@ -19,7 +19,8 @@ export class HtmlAttrAst implements HtmlAst {
export class HtmlElementAst implements HtmlAst { export class HtmlElementAst implements HtmlAst {
constructor(public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[], constructor(public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[],
public sourceSpan: ParseSourceSpan) {} public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); } visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
} }

View File

@ -154,11 +154,12 @@ class TreeBuilder {
selfClosing = false; selfClosing = false;
} }
var end = this.peek.sourceSpan.start; var end = this.peek.sourceSpan.start;
var el = new HtmlElementAst(fullName, attrs, [], let span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
new ParseSourceSpan(startTagToken.sourceSpan.start, end)); var el = new HtmlElementAst(fullName, attrs, [], span, span, null);
this._pushElement(el); this._pushElement(el);
if (selfClosing) { if (selfClosing) {
this._popElement(fullName); this._popElement(fullName);
el.endSourceSpan = span;
} }
} }
@ -173,7 +174,8 @@ class TreeBuilder {
var tagDef = getHtmlTagDefinition(el.name); var tagDef = getHtmlTagDefinition(el.name);
var parentEl = this._getParentElement(); var parentEl = this._getParentElement();
if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) { if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) {
var newParent = new HtmlElementAst(tagDef.parentToAdd, [], [el], el.sourceSpan); var newParent = new HtmlElementAst(tagDef.parentToAdd, [], [el], el.sourceSpan,
el.startSourceSpan, el.endSourceSpan);
this._addToParent(newParent); this._addToParent(newParent);
this.elementStack.push(newParent); this.elementStack.push(newParent);
this.elementStack.push(el); this.elementStack.push(el);
@ -187,6 +189,8 @@ class TreeBuilder {
var fullName = var fullName =
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement()); getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
if (getHtmlTagDefinition(fullName).isVoid) { if (getHtmlTagDefinition(fullName).isVoid) {
this.errors.push( this.errors.push(
HtmlTreeError.create(fullName, endTagToken.sourceSpan, HtmlTreeError.create(fullName, endTagToken.sourceSpan,

View File

@ -50,7 +50,8 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
this.visitingTemplateEl = ast.name.toLowerCase() == 'template'; this.visitingTemplateEl = ast.name.toLowerCase() == 'template';
let attrs = ast.attrs.map(attr => attr.visit(this, null)); let attrs = ast.attrs.map(attr => attr.visit(this, null));
let children = ast.children.map(child => child.visit(this, null)); let children = ast.children.map(child => child.visit(this, null));
return new HtmlElementAst(ast.name, attrs, children, ast.sourceSpan); return new HtmlElementAst(ast.name, attrs, children, ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan);
} }
visitAttr(originalAst: HtmlAttrAst, context: any): HtmlAttrAst { visitAttr(originalAst: HtmlAttrAst, context: any): HtmlAttrAst {

View File

@ -0,0 +1,78 @@
import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from 'angular2/src/compiler/html_parser';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {BaseException} from 'angular2/src/facade/exceptions';
export function humanizeDom(parseResult: HtmlParseTreeResult): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new _Humanizer(false);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
export function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new _Humanizer(true);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements HtmlAstVisitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(ast: HtmlElementAst, context: any): any {
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.attrs);
htmlVisitAll(this, ast.children);
this.elDepth--;
return null;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
this.result.push(res);
return null;
}
visitText(ast: HtmlTextAst, context: any): any {
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
visitComment(ast: HtmlCommentAst, context: any): any {
var res = this._appendContext(ast, [HtmlCommentAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -20,9 +20,8 @@ import {
HtmlCommentAst, HtmlCommentAst,
htmlVisitAll htmlVisitAll
} from 'angular2/src/compiler/html_ast'; } from 'angular2/src/compiler/html_ast';
import {ParseError, ParseLocation, ParseSourceSpan} from 'angular2/src/compiler/parse_util'; import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
import {BaseException} from 'angular2/src/facade/exceptions';
export function main() { export function main() {
describe('HtmlParser', () => { describe('HtmlParser', () => {
@ -51,6 +50,7 @@ export function main() {
}); });
}); });
describe('elements', () => { describe('elements', () => {
it('should parse root level elements', () => { it('should parse root level elements', () => {
expect(humanizeDom(parser.parse('<div></div>', 'TestComp'))) expect(humanizeDom(parser.parse('<div></div>', 'TestComp')))
@ -253,6 +253,16 @@ export function main() {
[HtmlTextAst, '\na\n', 1, '\na\n'], [HtmlTextAst, '\na\n', 1, '\na\n'],
]); ]);
}); });
it('should set the start and end source spans', () => {
let node = <HtmlElementAst>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
expect(node.endSourceSpan.start.offset).toEqual(6);
expect(node.endSourceSpan.end.offset).toEqual(12);
});
}); });
describe('errors', () => { describe('errors', () => {
@ -299,33 +309,7 @@ export function main() {
}); });
} }
function humanizeDom(parseResult: HtmlParseTreeResult): any[] { export function humanizeErrors(errors: ParseError[]): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new Humanizer(false);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new Humanizer(true);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function humanizeErrors(errors: ParseError[]): any[] {
return errors.map(error => { return errors.map(error => {
if (error instanceof HtmlTreeError) { if (error instanceof HtmlTreeError) {
// Parser errors // Parser errors
@ -334,44 +318,4 @@ function humanizeErrors(errors: ParseError[]): any[] {
// Tokenizer errors // Tokenizer errors
return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.span.start)]; return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.span.start)];
}); });
} }
class Humanizer implements HtmlAstVisitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(ast: HtmlElementAst, context: any): any {
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.attrs);
htmlVisitAll(this, ast.children);
this.elDepth--;
return null;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
this.result.push(res);
return null;
}
visitText(ast: HtmlTextAst, context: any): any {
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
visitComment(ast: HtmlCommentAst, context: any): any {
var res = this._appendContext(ast, [HtmlCommentAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}