feat(html_parser): support special forms used by i18n { exp, plural, =0 {} }
This commit is contained in:
parent
7f297666ca
commit
7c9717bba8
|
@ -11,7 +11,15 @@ import {
|
||||||
|
|
||||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||||
|
|
||||||
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst} from './html_ast';
|
import {
|
||||||
|
HtmlAst,
|
||||||
|
HtmlAttrAst,
|
||||||
|
HtmlTextAst,
|
||||||
|
HtmlCommentAst,
|
||||||
|
HtmlElementAst,
|
||||||
|
HtmlExpansionAst,
|
||||||
|
HtmlExpansionCaseAst
|
||||||
|
} from './html_ast';
|
||||||
|
|
||||||
import {Injectable} from 'angular2/src/core/di';
|
import {Injectable} from 'angular2/src/core/di';
|
||||||
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
|
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
|
||||||
|
@ -32,8 +40,9 @@ export class HtmlParseTreeResult {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HtmlParser {
|
export class HtmlParser {
|
||||||
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
|
parse(sourceContent: string, sourceUrl: string,
|
||||||
var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl);
|
parseExpansionForms: boolean = false): HtmlParseTreeResult {
|
||||||
|
var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms);
|
||||||
var treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
|
var treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
|
||||||
return new HtmlParseTreeResult(treeAndErrors.rootNodes, (<ParseError[]>tokensAndErrors.errors)
|
return new HtmlParseTreeResult(treeAndErrors.rootNodes, (<ParseError[]>tokensAndErrors.errors)
|
||||||
.concat(treeAndErrors.errors));
|
.concat(treeAndErrors.errors));
|
||||||
|
@ -68,6 +77,8 @@ class TreeBuilder {
|
||||||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
|
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
|
||||||
this._closeVoidElement();
|
this._closeVoidElement();
|
||||||
this._consumeText(this._advance());
|
this._consumeText(this._advance());
|
||||||
|
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
|
||||||
|
this._consumeExpansion(this._advance());
|
||||||
} else {
|
} else {
|
||||||
// Skip all other tokens...
|
// Skip all other tokens...
|
||||||
this._advance();
|
this._advance();
|
||||||
|
@ -105,6 +116,63 @@ class TreeBuilder {
|
||||||
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
|
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _consumeExpansion(token: HtmlToken) {
|
||||||
|
let switchValue = this._advance();
|
||||||
|
|
||||||
|
let type = this._advance();
|
||||||
|
let cases = [];
|
||||||
|
|
||||||
|
// read =
|
||||||
|
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
|
||||||
|
let value = this._advance();
|
||||||
|
|
||||||
|
// read {
|
||||||
|
let exp = [];
|
||||||
|
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
|
||||||
|
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
|
||||||
|
`Invalid expansion form. Missing '{'.,`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read until }
|
||||||
|
let start = this._advance();
|
||||||
|
while (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_END) {
|
||||||
|
exp.push(this._advance());
|
||||||
|
if (this.peek.type === HtmlTokenType.EOF) {
|
||||||
|
this.errors.push(
|
||||||
|
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let end = this._advance();
|
||||||
|
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
|
||||||
|
|
||||||
|
// parse everything in between { and }
|
||||||
|
let parsedExp = new TreeBuilder(exp).build();
|
||||||
|
if (parsedExp.errors.length > 0) {
|
||||||
|
this.errors = this.errors.concat(parsedExp.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
|
||||||
|
let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
|
||||||
|
cases.push(new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan,
|
||||||
|
value.sourceSpan, expSourceSpan));
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the final }
|
||||||
|
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
|
||||||
|
this.errors.push(
|
||||||
|
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid expansion form. Missing '}'.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._advance();
|
||||||
|
|
||||||
|
let mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
|
||||||
|
this._addToParent(new HtmlExpansionAst(switchValue.parts[0], type.parts[0], cases,
|
||||||
|
mainSourceSpan, switchValue.sourceSpan));
|
||||||
|
}
|
||||||
|
|
||||||
private _consumeText(token: HtmlToken) {
|
private _consumeText(token: HtmlToken) {
|
||||||
let text = token.parts[0];
|
let text = token.parts[0];
|
||||||
if (text.length > 0 && text[0] == '\n') {
|
if (text.length > 0 && text[0] == '\n') {
|
||||||
|
|
|
@ -18,7 +18,9 @@ import {
|
||||||
HtmlAttrAst,
|
HtmlAttrAst,
|
||||||
HtmlTextAst,
|
HtmlTextAst,
|
||||||
HtmlCommentAst,
|
HtmlCommentAst,
|
||||||
htmlVisitAll
|
htmlVisitAll,
|
||||||
|
HtmlExpansionAst,
|
||||||
|
HtmlExpansionCaseAst
|
||||||
} from 'angular2/src/compiler/html_ast';
|
} from 'angular2/src/compiler/html_ast';
|
||||||
import {ParseError, ParseLocation} 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 {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
|
||||||
|
@ -227,7 +229,7 @@ export function main() {
|
||||||
.toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]);
|
.toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support mamespace', () => {
|
it('should support namespace', () => {
|
||||||
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp')))
|
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp')))
|
||||||
.toEqual([[HtmlElementAst, '@svg:use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]);
|
.toEqual([[HtmlElementAst, '@svg:use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]);
|
||||||
});
|
});
|
||||||
|
@ -240,6 +242,54 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("expansion forms", () => {
|
||||||
|
it("should parse out expansion forms", () => {
|
||||||
|
let parsed = parser.parse(`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
|
||||||
|
'TestComp', true);
|
||||||
|
|
||||||
|
expect(humanizeDom(parsed))
|
||||||
|
.toEqual([
|
||||||
|
[HtmlElementAst, 'div', 0],
|
||||||
|
[HtmlTextAst, 'before', 1],
|
||||||
|
[HtmlExpansionAst, 'messages.length', 'plural'],
|
||||||
|
[HtmlExpansionCaseAst, '0'],
|
||||||
|
[HtmlExpansionCaseAst, '1'],
|
||||||
|
[HtmlTextAst, 'after', 1]
|
||||||
|
]);
|
||||||
|
|
||||||
|
let cases = (<any>parsed.rootNodes[0]).children[1].cases;
|
||||||
|
|
||||||
|
expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, [])))
|
||||||
|
.toEqual([
|
||||||
|
[HtmlTextAst, 'You have ', 0],
|
||||||
|
[HtmlElementAst, 'b', 0],
|
||||||
|
[HtmlTextAst, 'no', 1],
|
||||||
|
[HtmlTextAst, ' messages', 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(humanizeDom(new HtmlParseTreeResult(cases[1].expression, [])))
|
||||||
|
.toEqual([[HtmlTextAst, 'One {{message}}', 0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when expansion form is not closed", () => {
|
||||||
|
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
|
||||||
|
expect(humanizeErrors(p.errors))
|
||||||
|
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:34']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when expansion case is not closed", () => {
|
||||||
|
let p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
|
||||||
|
expect(humanizeErrors(p.errors))
|
||||||
|
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:29']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when invalid html in the case", () => {
|
||||||
|
let p = parser.parse(`{messages.length, plural, =0 {<b/>}`, 'TestComp', true);
|
||||||
|
expect(humanizeErrors(p.errors))
|
||||||
|
.toEqual([['b', 'Only void and foreign elements can be self closed "b"', '0:30']]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('source spans', () => {
|
describe('source spans', () => {
|
||||||
it('should store the location', () => {
|
it('should store the location', () => {
|
||||||
expect(humanizeDomSourceSpans(parser.parse(
|
expect(humanizeDomSourceSpans(parser.parse(
|
||||||
|
|
Loading…
Reference in New Issue