X Tutup
Skip to content

Commit 40e457a

Browse files
vsavkinRobert Messerle
authored andcommitted
feat(i18n): add support for nested expansion forms
1 parent aef35b4 commit 40e457a

File tree

9 files changed

+246
-93
lines changed

9 files changed

+246
-93
lines changed

modules/angular2/src/compiler/html_lexer.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,7 @@ class _HtmlTokenizer {
125125
private currentTokenStart: ParseLocation;
126126
private currentTokenType: HtmlTokenType;
127127

128-
private inExpansionCase: boolean = false;
129-
private inExpansionForm: boolean = false;
128+
private expansionCaseStack = [];
130129

131130
tokens: HtmlToken[] = [];
132131
errors: HtmlTokenError[] = [];
@@ -169,10 +168,12 @@ class _HtmlTokenizer {
169168
} else if (this.peek === $EQ && this.tokenizeExpansionForms) {
170169
this._consumeExpansionCaseStart();
171170

172-
} else if (this.peek === $RBRACE && this.inExpansionCase && this.tokenizeExpansionForms) {
171+
} else if (this.peek === $RBRACE && this.isInExpansionCase() &&
172+
this.tokenizeExpansionForms) {
173173
this._consumeExpansionCaseEnd();
174174

175-
} else if (this.peek === $RBRACE && !this.inExpansionCase && this.tokenizeExpansionForms) {
175+
} else if (this.peek === $RBRACE && this.isInExpansionForm() &&
176+
this.tokenizeExpansionForms) {
176177
this._consumeExpansionFormEnd();
177178

178179
} else {
@@ -551,7 +552,7 @@ class _HtmlTokenizer {
551552
this._requireCharCode($COMMA);
552553
this._attemptCharCodeUntilFn(isNotWhitespace);
553554

554-
this.inExpansionForm = true;
555+
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
555556
}
556557

557558
private _consumeExpansionCaseStart() {
@@ -567,7 +568,7 @@ class _HtmlTokenizer {
567568
this._endToken([], this._getLocation());
568569
this._attemptCharCodeUntilFn(isNotWhitespace);
569570

570-
this.inExpansionCase = true;
571+
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
571572
}
572573

573574
private _consumeExpansionCaseEnd() {
@@ -576,15 +577,15 @@ class _HtmlTokenizer {
576577
this._endToken([], this._getLocation());
577578
this._attemptCharCodeUntilFn(isNotWhitespace);
578579

579-
this.inExpansionCase = false;
580+
this.expansionCaseStack.pop();
580581
}
581582

582583
private _consumeExpansionFormEnd() {
583584
this._beginToken(HtmlTokenType.EXPANSION_FORM_END, this._getLocation());
584585
this._requireCharCode($RBRACE);
585586
this._endToken([]);
586587

587-
this.inExpansionForm = false;
588+
this.expansionCaseStack.pop();
588589
}
589590

590591
private _consumeText() {
@@ -622,7 +623,9 @@ class _HtmlTokenizer {
622623
if (this.peek === $LT || this.peek === $EOF) return true;
623624
if (this.tokenizeExpansionForms) {
624625
if (isSpecialFormStart(this.peek, this.nextPeek)) return true;
625-
if (this.peek === $RBRACE && !interpolation && this.inExpansionForm) return true;
626+
if (this.peek === $RBRACE && !interpolation &&
627+
(this.isInExpansionCase() || this.isInExpansionForm()))
628+
return true;
626629
}
627630
return false;
628631
}
@@ -648,6 +651,18 @@ class _HtmlTokenizer {
648651
this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens);
649652
}
650653
}
654+
655+
private isInExpansionCase(): boolean {
656+
return this.expansionCaseStack.length > 0 &&
657+
this.expansionCaseStack[this.expansionCaseStack.length - 1] ===
658+
HtmlTokenType.EXPANSION_CASE_EXP_START;
659+
}
660+
661+
private isInExpansionForm(): boolean {
662+
return this.expansionCaseStack.length > 0 &&
663+
this.expansionCaseStack[this.expansionCaseStack.length - 1] ===
664+
HtmlTokenType.EXPANSION_FORM_START;
665+
}
651666
}
652667

653668
function isNotWhitespace(code: number): boolean {

modules/angular2/src/compiler/html_parser.ts

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -124,40 +124,9 @@ class TreeBuilder {
124124

125125
// read =
126126
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(<HtmlTreeError[]>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));
127+
let expCase = this._parseExpansionCase();
128+
if (isBlank(expCase)) return; // error
129+
cases.push(expCase);
161130
}
162131

163132
// read the final }
@@ -173,6 +142,80 @@ class TreeBuilder {
173142
mainSourceSpan, switchValue.sourceSpan));
174143
}
175144

145+
private _parseExpansionCase(): HtmlExpansionCaseAst {
146+
let value = this._advance();
147+
148+
// read {
149+
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
150+
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
151+
`Invalid expansion form. Missing '{'.,`));
152+
return null;
153+
}
154+
155+
// read until }
156+
let start = this._advance();
157+
158+
let exp = this._collectExpansionExpTokens(start);
159+
if (isBlank(exp)) return null;
160+
161+
let end = this._advance();
162+
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
163+
164+
// parse everything in between { and }
165+
let parsedExp = new TreeBuilder(exp).build();
166+
if (parsedExp.errors.length > 0) {
167+
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
168+
return null;
169+
}
170+
171+
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
172+
let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
173+
return new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan,
174+
value.sourceSpan, expSourceSpan);
175+
}
176+
177+
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
178+
let exp = [];
179+
let expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
180+
181+
while (true) {
182+
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START ||
183+
this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) {
184+
expansionFormStack.push(this.peek.type);
185+
}
186+
187+
if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) {
188+
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) {
189+
expansionFormStack.pop();
190+
if (expansionFormStack.length == 0) return exp;
191+
192+
} else {
193+
this.errors.push(
194+
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
195+
return null;
196+
}
197+
}
198+
199+
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
200+
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
201+
expansionFormStack.pop();
202+
} else {
203+
this.errors.push(
204+
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
205+
return null;
206+
}
207+
}
208+
209+
if (this.peek.type === HtmlTokenType.EOF) {
210+
this.errors.push(
211+
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
212+
return null;
213+
}
214+
215+
exp.push(this._advance());
216+
}
217+
}
218+
176219
private _consumeText(token: HtmlToken) {
177220
let text = token.parts[0];
178221
if (text.length > 0 && text[0] == '\n') {
@@ -321,3 +364,7 @@ function getElementFullName(prefix: string, localName: string,
321364

322365
return mergeNsAndName(prefix, localName);
323366
}
367+
368+
function lastOnStack(stack: any[], element: any): boolean {
369+
return stack.length > 0 && stack[stack.length - 1] === element;
370+
}

modules/angular2/src/compiler/template_parser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,13 @@ class TemplateParseVisitor implements HtmlAstVisitor {
211211
}
212212
}
213213

214+
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
215+
216+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
217+
214218
visitText(ast: HtmlTextAst, component: Component): any {
215219
var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR);
220+
216221
var expr = this._parseInterpolation(ast.value, ast.sourceSpan);
217222
if (isPresent(expr)) {
218223
return new BoundTextAst(expr, ngContentIndex, ast.sourceSpan);
@@ -714,7 +719,6 @@ class NonBindableVisitor implements HtmlAstVisitor {
714719
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
715720
}
716721
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
717-
718722
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
719723
}
720724

modules/angular2/src/i18n/expander.ts

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212

1313
import {BaseException} from 'angular2/src/facade/exceptions';
1414

15+
1516
/**
1617
* Expands special forms into elements.
1718
*
@@ -35,7 +36,18 @@ import {BaseException} from 'angular2/src/facade/exceptions';
3536
* </ul>
3637
* ```
3738
*/
38-
export class Expander implements HtmlAstVisitor {
39+
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
40+
let e = new _Expander();
41+
let n = htmlVisitAll(e, nodes);
42+
return new ExpansionResult(n, e.expanded);
43+
}
44+
45+
export class ExpansionResult {
46+
constructor(public nodes: HtmlAst[], public expanded: boolean) {}
47+
}
48+
49+
class _Expander implements HtmlAstVisitor {
50+
expanded: boolean = false;
3951
constructor() {}
4052

4153
visitElement(ast: HtmlElementAst, context: any): any {
@@ -50,6 +62,7 @@ export class Expander implements HtmlAstVisitor {
5062
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
5163

5264
visitExpansion(ast: HtmlExpansionAst, context: any): any {
65+
this.expanded = true;
5366
return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
5467
}
5568

@@ -59,36 +72,44 @@ export class Expander implements HtmlAstVisitor {
5972
}
6073

6174
function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
62-
let children = ast.cases.map(
63-
c => new HtmlElementAst(
64-
`template`,
65-
[
66-
new HtmlAttrAst("[ngPluralCase]", c.value, c.valueSourceSpan),
67-
],
68-
[
69-
new HtmlElementAst(
70-
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
71-
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
72-
],
73-
c.sourceSpan, c.sourceSpan, c.sourceSpan));
75+
let children = ast.cases.map(c => {
76+
let expansionResult = expandNodes(c.expression);
77+
let i18nAttrs = expansionResult.expanded ?
78+
[] :
79+
[new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)];
80+
81+
return new HtmlElementAst(`template`,
82+
[
83+
new HtmlAttrAst("ngPluralCase", c.value, c.valueSourceSpan),
84+
],
85+
[
86+
new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
87+
c.sourceSpan, c.sourceSpan, c.sourceSpan)
88+
],
89+
c.sourceSpan, c.sourceSpan, c.sourceSpan);
90+
});
7491
let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan);
7592
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
7693
ast.sourceSpan);
7794
}
7895

7996
function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst {
80-
let children = ast.cases.map(
81-
c => new HtmlElementAst(
82-
`template`,
83-
[
84-
new HtmlAttrAst("[ngSwitchWhen]", c.value, c.valueSourceSpan),
85-
],
86-
[
87-
new HtmlElementAst(
88-
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
89-
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
90-
],
91-
c.sourceSpan, c.sourceSpan, c.sourceSpan));
97+
let children = ast.cases.map(c => {
98+
let expansionResult = expandNodes(c.expression);
99+
let i18nAttrs = expansionResult.expanded ?
100+
[] :
101+
[new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)];
102+
103+
return new HtmlElementAst(`template`,
104+
[
105+
new HtmlAttrAst("ngSwitchWhen", c.value, c.valueSourceSpan),
106+
],
107+
[
108+
new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
109+
c.sourceSpan, c.sourceSpan, c.sourceSpan)
110+
],
111+
c.sourceSpan, c.sourceSpan, c.sourceSpan);
112+
});
92113
let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
93114
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
94115
ast.sourceSpan);

0 commit comments

Comments
 (0)
X Tutup