diff --git a/modules/angular2/src/compiler/directive_normalizer.ts b/modules/angular2/src/compiler/directive_normalizer.ts
index e6d440c9eb..5ffabda9ac 100644
--- a/modules/angular2/src/compiler/directive_normalizer.ts
+++ b/modules/angular2/src/compiler/directive_normalizer.ts
@@ -23,6 +23,8 @@ import {
HtmlAttrAst,
HtmlAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
htmlVisitAll
} from './html_ast';
import {HtmlParser} from './html_parser';
@@ -158,4 +160,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any { return null; }
+ visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
}
diff --git a/modules/angular2/src/compiler/html_ast.ts b/modules/angular2/src/compiler/html_ast.ts
index d1f18b47df..6da9687043 100644
--- a/modules/angular2/src/compiler/html_ast.ts
+++ b/modules/angular2/src/compiler/html_ast.ts
@@ -12,6 +12,24 @@ export class HtmlTextAst implements HtmlAst {
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
+export class HtmlExpansionAst implements HtmlAst {
+ constructor(public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
+ public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
+ visit(visitor: HtmlAstVisitor, context: any): any {
+ return visitor.visitExpansion(this, context);
+ }
+}
+
+export class HtmlExpansionCaseAst implements HtmlAst {
+ constructor(public value: string, public expression: HtmlAst[],
+ public sourceSpan: ParseSourceSpan, public valueSourceSpan: ParseSourceSpan,
+ public expSourceSpan: ParseSourceSpan) {}
+
+ visit(visitor: HtmlAstVisitor, context: any): any {
+ return visitor.visitExpansionCase(this, context);
+ }
+}
+
export class HtmlAttrAst implements HtmlAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
@@ -34,6 +52,8 @@ export interface HtmlAstVisitor {
visitAttr(ast: HtmlAttrAst, context: any): any;
visitText(ast: HtmlTextAst, context: any): any;
visitComment(ast: HtmlCommentAst, context: any): any;
+ visitExpansion(ast: HtmlExpansionAst, context: any): any;
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
}
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
diff --git a/modules/angular2/src/compiler/legacy_template.ts b/modules/angular2/src/compiler/legacy_template.ts
index 6adbf78a4a..d26b9e8ac1 100644
--- a/modules/angular2/src/compiler/legacy_template.ts
+++ b/modules/angular2/src/compiler/legacy_template.ts
@@ -14,6 +14,8 @@ import {
HtmlElementAst,
HtmlTextAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
HtmlAst
} from './html_ast';
import {HtmlParser, HtmlParseTreeResult} from './html_parser';
@@ -84,6 +86,13 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; }
+ visitExpansion(ast: HtmlExpansionAst, context: any): any {
+ let cases = ast.cases.map(c => c.visit(this, null));
+ return new HtmlExpansionAst(ast.switchValue, ast.type, cases, ast.sourceSpan);
+ }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
+
private _rewriteLongSyntax(ast: HtmlAttrAst): HtmlAttrAst {
let m = RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name);
let attrName = ast.name;
@@ -211,9 +220,10 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
@Injectable()
export class LegacyHtmlParser extends HtmlParser {
- parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
+ parse(sourceContent: string, sourceUrl: string,
+ parseExpansionForms: boolean = false): HtmlParseTreeResult {
let transformer = new LegacyHtmlAstTransformer();
- let htmlParseTreeResult = super.parse(sourceContent, sourceUrl);
+ let htmlParseTreeResult = super.parse(sourceContent, sourceUrl, parseExpansionForms);
let rootNodes = htmlParseTreeResult.rootNodes.map(node => node.visit(transformer, null));
diff --git a/modules/angular2/src/compiler/template_parser.ts b/modules/angular2/src/compiler/template_parser.ts
index 1c08e15894..1c332773f3 100644
--- a/modules/angular2/src/compiler/template_parser.ts
+++ b/modules/angular2/src/compiler/template_parser.ts
@@ -64,6 +64,8 @@ import {
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
htmlVisitAll
} from './html_ast';
@@ -268,7 +270,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
- visitElement(element: HtmlElementAst, parent: ElementContext): any {
+ visitElement(element: HtmlElementAst, component: ElementContext): any {
var nodeName = element.name;
var preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
@@ -770,6 +772,9 @@ class NonBindableVisitor implements HtmlAstVisitor {
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
}
+ visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
}
class BoundElementOrDirectiveProperty {
diff --git a/modules/angular2/src/i18n/expander.ts b/modules/angular2/src/i18n/expander.ts
new file mode 100644
index 0000000000..6a27dd6373
--- /dev/null
+++ b/modules/angular2/src/i18n/expander.ts
@@ -0,0 +1,95 @@
+import {
+ HtmlAst,
+ HtmlAstVisitor,
+ HtmlElementAst,
+ HtmlAttrAst,
+ HtmlTextAst,
+ HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
+ htmlVisitAll
+} from 'angular2/src/compiler/html_ast';
+
+import {BaseException} from 'angular2/src/facade/exceptions';
+
+/**
+ * Expands special forms into elements.
+ *
+ * For example,
+ *
+ * ```
+ * { messages.length, plural,
+ * =0 {zero}
+ * =1 {one}
+ * =other {more than one}
+ * }
+ * ```
+ *
+ * will be expanded into
+ *
+ * ```
+ *
+ * - zero
+ * - one
+ * - more than one
+ *
+ * ```
+ */
+export class Expander implements HtmlAstVisitor {
+ constructor() {}
+
+ visitElement(ast: HtmlElementAst, context: any): any {
+ return new HtmlElementAst(ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan,
+ ast.startSourceSpan, ast.endSourceSpan);
+ }
+
+ visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
+
+ visitText(ast: HtmlTextAst, context: any): any { return ast; }
+
+ visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
+
+ visitExpansion(ast: HtmlExpansionAst, context: any): any {
+ return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
+ }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
+ throw new BaseException("Should not be reached");
+ }
+}
+
+function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
+ let children = ast.cases.map(
+ c => new HtmlElementAst(
+ `template`,
+ [
+ new HtmlAttrAst("[ngPluralCase]", c.value, c.valueSourceSpan),
+ ],
+ [
+ new HtmlElementAst(
+ `li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
+ c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
+ ],
+ c.sourceSpan, c.sourceSpan, c.sourceSpan));
+ let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan);
+ return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
+ ast.sourceSpan);
+}
+
+function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst {
+ let children = ast.cases.map(
+ c => new HtmlElementAst(
+ `template`,
+ [
+ new HtmlAttrAst("[ngSwitchWhen]", c.value, c.valueSourceSpan),
+ ],
+ [
+ new HtmlElementAst(
+ `li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
+ c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
+ ],
+ c.sourceSpan, c.sourceSpan, c.sourceSpan));
+ let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
+ return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
+ ast.sourceSpan);
+}
\ No newline at end of file
diff --git a/modules/angular2/src/i18n/i18n_html_parser.ts b/modules/angular2/src/i18n/i18n_html_parser.ts
index a009192df8..23b09735a0 100644
--- a/modules/angular2/src/i18n/i18n_html_parser.ts
+++ b/modules/angular2/src/i18n/i18n_html_parser.ts
@@ -7,6 +7,8 @@ import {
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
@@ -14,6 +16,7 @@ import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang'
import {BaseException} from 'angular2/src/facade/exceptions';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message';
+import {Expander} from './expander';
import {
messageFromAttribute,
I18nError,
@@ -119,19 +122,25 @@ export class I18nHtmlParser implements HtmlParser {
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
- parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
+ parse(sourceContent: string, sourceUrl: string,
+ parseExpansionForms: boolean = false): HtmlParseTreeResult {
this.errors = [];
- let res = this._htmlParser.parse(sourceContent, sourceUrl);
+ let res = this._htmlParser.parse(sourceContent, sourceUrl, parseExpansionForms);
if (res.errors.length > 0) {
return res;
} else {
- let nodes = this._recurse(res.rootNodes);
+ let nodes = this._recurse(this._expandNodes(res.rootNodes));
return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
new HtmlParseTreeResult(nodes, []);
}
}
+ private _expandNodes(nodes: HtmlAst[]): HtmlAst[] {
+ let e = new Expander();
+ return htmlVisitAll(e, nodes);
+ }
+
private _processI18nPart(p: Part): HtmlAst[] {
try {
return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
@@ -360,5 +369,9 @@ class _CreateNodeMapping implements HtmlAstVisitor {
return null;
}
+ visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
+
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
}
diff --git a/modules/angular2/src/i18n/message_extractor.ts b/modules/angular2/src/i18n/message_extractor.ts
index a923aae4ac..7da4ee0f79 100644
--- a/modules/angular2/src/i18n/message_extractor.ts
+++ b/modules/angular2/src/i18n/message_extractor.ts
@@ -13,6 +13,7 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message';
+import {Expander} from './expander';
import {
I18nError,
Part,
@@ -121,15 +122,20 @@ export class MessageExtractor {
this.messages = [];
this.errors = [];
- let res = this._htmlParser.parse(template, sourceUrl);
+ let res = this._htmlParser.parse(template, sourceUrl, true);
if (res.errors.length > 0) {
return new ExtractionResult([], res.errors);
} else {
- this._recurse(res.rootNodes);
+ this._recurse(this._expandNodes(res.rootNodes));
return new ExtractionResult(this.messages, this.errors);
}
}
+ private _expandNodes(nodes: HtmlAst[]): HtmlAst[] {
+ let e = new Expander();
+ return htmlVisitAll(e, nodes);
+ }
+
private _extractMessagesFromPart(p: Part): void {
if (p.hasI18n) {
this.messages.push(p.createMessage(this._parser));
diff --git a/modules/angular2/src/i18n/shared.ts b/modules/angular2/src/i18n/shared.ts
index d1ebd8a7a2..d46798f75d 100644
--- a/modules/angular2/src/i18n/shared.ts
+++ b/modules/angular2/src/i18n/shared.ts
@@ -6,6 +6,8 @@ import {
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
@@ -179,6 +181,10 @@ class _StringifyVisitor implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
+ visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
+
private _join(strs: string[], str: string): string {
return strs.filter(s => s.length > 0).join(str);
}
diff --git a/modules/angular2/test/compiler/html_ast_spec_utils.ts b/modules/angular2/test/compiler/html_ast_spec_utils.ts
index a59476917f..af58c224f6 100644
--- a/modules/angular2/test/compiler/html_ast_spec_utils.ts
+++ b/modules/angular2/test/compiler/html_ast_spec_utils.ts
@@ -6,6 +6,8 @@ import {
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
+ HtmlExpansionAst,
+ HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
@@ -70,6 +72,19 @@ class _Humanizer implements HtmlAstVisitor {
return null;
}
+ visitExpansion(ast: HtmlExpansionAst, context: any): any {
+ var res = this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type]);
+ this.result.push(res);
+ htmlVisitAll(this, ast.cases);
+ return null;
+ }
+
+ visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
+ var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value]);
+ this.result.push(res);
+ return null;
+ }
+
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
diff --git a/modules/angular2/test/i18n/i18n_html_parser_spec.ts b/modules/angular2/test/i18n/i18n_html_parser_spec.ts
index 68c505fc57..65ebe42450 100644
--- a/modules/angular2/test/i18n/i18n_html_parser_spec.ts
+++ b/modules/angular2/test/i18n/i18n_html_parser_spec.ts
@@ -42,7 +42,7 @@ export function main() {
let res = deserializeXmb(`${msgs}`, 'someUrl');
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
- .parse(template, "someurl");
+ .parse(template, "someurl", true);
}
it("should delegate to the provided parser when no i18n", () => {
@@ -188,6 +188,82 @@ export function main() {
expect(res[1].sourceSpan.start.offset).toEqual(10);
});
+ it("should handle the plural special form", () => {
+ let translations: {[key: string]: string} = {};
+ translations[id(new Message('zerobold', "plural_0", null))] =
+ 'ZEROBOLD';
+
+ let res = parse(`{messages.length, plural,=0 {zerobold}}`, translations);
+
+ expect(humanizeDom(res))
+ .toEqual([
+ [HtmlElementAst, 'ul', 0],
+ [HtmlAttrAst, '[ngPlural]', 'messages.length'],
+ [HtmlElementAst, 'template', 1],
+ [HtmlAttrAst, '[ngPluralCase]', '0'],
+ [HtmlElementAst, 'li', 2],
+ [HtmlTextAst, 'ZERO', 3],
+ [HtmlElementAst, 'b', 3],
+ [HtmlTextAst, 'BOLD', 4]
+ ]);
+ });
+
+ it("should correctly set source code positions", () => {
+ let translations: {[key: string]: string} = {};
+ translations[id(new Message('bold', "plural_0", null))] =
+ 'BOLD';
+
+ let nodes = parse(`{messages.length, plural,=0 {bold}}`, translations).rootNodes;
+
+ let ul: HtmlElementAst = nodes[0];
+
+ expect(ul.sourceSpan.start.col).toEqual(0);
+ expect(ul.sourceSpan.end.col).toEqual(42);
+
+ expect(ul.startSourceSpan.start.col).toEqual(0);
+ expect(ul.startSourceSpan.end.col).toEqual(42);
+
+ expect(ul.endSourceSpan.start.col).toEqual(0);
+ expect(ul.endSourceSpan.end.col).toEqual(42);
+
+ let switchExp = ul.attrs[0];
+ expect(switchExp.sourceSpan.start.col).toEqual(1);
+ expect(switchExp.sourceSpan.end.col).toEqual(16);
+
+ let template: HtmlElementAst = ul.children[0];
+ expect(template.sourceSpan.start.col).toEqual(26);
+ expect(template.sourceSpan.end.col).toEqual(41);
+
+ let switchCheck = template.attrs[0];
+ expect(switchCheck.sourceSpan.start.col).toEqual(26);
+ expect(switchCheck.sourceSpan.end.col).toEqual(28);
+
+ let li: HtmlElementAst = template.children[0];
+ expect(li.sourceSpan.start.col).toEqual(26);
+ expect(li.sourceSpan.end.col).toEqual(41);
+
+ let b: HtmlElementAst = li.children[0];
+ expect(b.sourceSpan.start.col).toEqual(29);
+ expect(b.sourceSpan.end.col).toEqual(32);
+ });
+
+ it("should handle other special forms", () => {
+ let translations: {[key: string]: string} = {};
+ translations[id(new Message('m', "gender_male", null))] = 'M';
+
+ let res = parse(`{person.gender, gender,=male {m}}`, translations);
+
+ expect(humanizeDom(res))
+ .toEqual([
+ [HtmlElementAst, 'ul', 0],
+ [HtmlAttrAst, '[ngSwitch]', 'person.gender'],
+ [HtmlElementAst, 'template', 1],
+ [HtmlAttrAst, '[ngSwitchWhen]', 'male'],
+ [HtmlElementAst, 'li', 2],
+ [HtmlTextAst, 'M', 3],
+ ]);
+ });
+
describe("errors", () => {
it("should error when giving an invalid template", () => {
expect(humanizeErrors(parse("a", {}).errors))
diff --git a/modules/angular2/test/i18n/message_extractor_spec.ts b/modules/angular2/test/i18n/message_extractor_spec.ts
index f7f8541ad5..430b624161 100644
--- a/modules/angular2/test/i18n/message_extractor_spec.ts
+++ b/modules/angular2/test/i18n/message_extractor_spec.ts
@@ -176,6 +176,24 @@ export function main() {
]);
});
+ it("should extract messages from special forms", () => {
+ let res = extractor.extract(`
+
+ {messages.length, plural,
+ =0 {You have no messages}
+ =1 {You have one message}
+ }
+
+ `,
+ "someurl");
+
+ expect(res.messages)
+ .toEqual([
+ new Message('You have no messages', "plural_0", null),
+ new Message('You have one message', "plural_1", null)
+ ]);
+ });
+
it("should remove duplicate messages", () => {
let res = extractor.extract(`
message