Skip to content

Commit 0a4241b

Browse files
vsavkinRobert Messerle
authored andcommitted
feat(html_parser): support special forms used by i18n { exp, plural, =0 {} }
1 parent 349839f commit 0a4241b

2 files changed

Lines changed: 123 additions & 5 deletions

File tree

modules/angular2/src/compiler/html_parser.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import {
1111

1212
import {ListWrapper} from 'angular2/src/facade/collection';
1313

14-
import {HtmlAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlElementAst} from './html_ast';
14+
import {
15+
HtmlAst,
16+
HtmlAttrAst,
17+
HtmlTextAst,
18+
HtmlCommentAst,
19+
HtmlElementAst,
20+
HtmlExpansionAst,
21+
HtmlExpansionCaseAst
22+
} from './html_ast';
1523

1624
import {Injectable} from 'angular2/src/core/di';
1725
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
@@ -32,8 +40,9 @@ export class HtmlParseTreeResult {
3240

3341
@Injectable()
3442
export class HtmlParser {
35-
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
36-
var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl);
43+
parse(sourceContent: string, sourceUrl: string,
44+
parseExpansionForms: boolean = false): HtmlParseTreeResult {
45+
var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms);
3746
var treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
3847
return new HtmlParseTreeResult(treeAndErrors.rootNodes, (<ParseError[]>tokensAndErrors.errors)
3948
.concat(treeAndErrors.errors));
@@ -68,6 +77,8 @@ class TreeBuilder {
6877
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
6978
this._closeVoidElement();
7079
this._consumeText(this._advance());
80+
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
81+
this._consumeExpansion(this._advance());
7182
} else {
7283
// Skip all other tokens...
7384
this._advance();
@@ -105,6 +116,63 @@ class TreeBuilder {
105116
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
106117
}
107118

119+
private _consumeExpansion(token: HtmlToken) {
120+
let switchValue = this._advance();
121+
122+
let type = this._advance();
123+
let cases = [];
124+
125+
// read =
126+
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
127+
let value = this._advance();
128+
129+
// read {
130+
let exp = [];
131+
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
132+
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
133+
`Invalid expansion form. Missing '{'.,`));
134+
return;
135+
}
136+
137+
// read until }
138+
let start = this._advance();
139+
while (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_END) {
140+
exp.push(this._advance());
141+
if (this.peek.type === HtmlTokenType.EOF) {
142+
this.errors.push(
143+
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
144+
return;
145+
}
146+
}
147+
let end = this._advance();
148+
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
149+
150+
// parse everything in between { and }
151+
let parsedExp = new TreeBuilder(exp).build();
152+
if (parsedExp.errors.length > 0) {
153+
this.errors = this.errors.concat(parsedExp.errors);
154+
return;
155+
}
156+
157+
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
158+
let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
159+
cases.push(new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan,
160+
value.sourceSpan, expSourceSpan));
161+
}
162+
163+
// read the final }
164+
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
165+
this.errors.push(
166+
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid expansion form. Missing '}'.`));
167+
return;
168+
}
169+
this._advance();
170+
171+
let mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
172+
this._addToParent(new HtmlExpansionAst(switchValue.parts[0], type.parts[0], cases,
173+
mainSourceSpan, switchValue.sourceSpan));
174+
}
175+
108176
private _consumeText(token: HtmlToken) {
109177
let text = token.parts[0];
110178
if (text.length > 0 && text[0] == '\n') {

modules/angular2/test/compiler/html_parser_spec.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import {
1818
HtmlAttrAst,
1919
HtmlTextAst,
2020
HtmlCommentAst,
21-
htmlVisitAll
21+
htmlVisitAll,
22+
HtmlExpansionAst,
23+
HtmlExpansionCaseAst
2224
} from 'angular2/src/compiler/html_ast';
2325
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
2426
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
@@ -227,7 +229,7 @@ export function main() {
227229
.toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]);
228230
});
229231

230-
it('should support mamespace', () => {
232+
it('should support namespace', () => {
231233
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp')))
232234
.toEqual([[HtmlElementAst, '@svg:use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]);
233235
});
@@ -240,6 +242,54 @@ export function main() {
240242
});
241243
});
242244

245+
describe("expansion forms", () => {
246+
it("should parse out expansion forms", () => {
247+
let parsed = parser.parse(`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
248+
'TestComp', true);
249+
250+
expect(humanizeDom(parsed))
251+
.toEqual([
252+
[HtmlElementAst, 'div', 0],
253+
[HtmlTextAst, 'before', 1],
254+
[HtmlExpansionAst, 'messages.length', 'plural'],
255+
[HtmlExpansionCaseAst, '0'],
256+
[HtmlExpansionCaseAst, '1'],
257+
[HtmlTextAst, 'after', 1]
258+
]);
259+
260+
let cases = (<any>parsed.rootNodes[0]).children[1].cases;
261+
262+
expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, [])))
263+
.toEqual([
264+
[HtmlTextAst, 'You have ', 0],
265+
[HtmlElementAst, 'b', 0],
266+
[HtmlTextAst, 'no', 1],
267+
[HtmlTextAst, ' messages', 0],
268+
]);
269+
270+
expect(humanizeDom(new HtmlParseTreeResult(cases[1].expression, [])))
271+
.toEqual([[HtmlTextAst, 'One {{message}}', 0]]);
272+
});
273+
274+
it("should error when expansion form is not closed", () => {
275+
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
276+
expect(humanizeErrors(p.errors))
277+
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:34']]);
278+
});
279+
280+
it("should error when expansion case is not closed", () => {
281+
let p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
282+
expect(humanizeErrors(p.errors))
283+
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:29']]);
284+
});
285+
286+
it("should error when invalid html in the case", () => {
287+
let p = parser.parse(`{messages.length, plural, =0 {<b/>}`, 'TestComp', true);
288+
expect(humanizeErrors(p.errors))
289+
.toEqual([['b', 'Only void and foreign elements can be self closed "b"', '0:30']]);
290+
});
291+
});
292+
243293
describe('source spans', () => {
244294
it('should store the location', () => {
245295
expect(humanizeDomSourceSpans(parser.parse(

0 commit comments

Comments
 (0)