From 7c9717bba8a45432d9edb6bbebaa132ea1d3c450 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 12 Apr 2016 11:46:39 -0700 Subject: [PATCH] feat(html_parser): support special forms used by i18n { exp, plural, =0 {} } --- modules/angular2/src/compiler/html_parser.ts | 74 ++++++++++++++++++- .../test/compiler/html_parser_spec.ts | 54 +++++++++++++- 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 40385eb105..56b522f559 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -11,7 +11,15 @@ import { 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 {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer'; @@ -32,8 +40,9 @@ export class HtmlParseTreeResult { @Injectable() export class HtmlParser { - parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult { - var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl); + parse(sourceContent: string, sourceUrl: string, + parseExpansionForms: boolean = false): HtmlParseTreeResult { + var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms); var treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build(); return new HtmlParseTreeResult(treeAndErrors.rootNodes, (tokensAndErrors.errors) .concat(treeAndErrors.errors)); @@ -68,6 +77,8 @@ class TreeBuilder { this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) { this._closeVoidElement(); this._consumeText(this._advance()); + } else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) { + this._consumeExpansion(this._advance()); } else { // Skip all other tokens... this._advance(); @@ -105,6 +116,63 @@ class TreeBuilder { 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) { let text = token.parts[0]; if (text.length > 0 && text[0] == '\n') { diff --git a/modules/angular2/test/compiler/html_parser_spec.ts b/modules/angular2/test/compiler/html_parser_spec.ts index 3af628f315..403deb3d4f 100644 --- a/modules/angular2/test/compiler/html_parser_spec.ts +++ b/modules/angular2/test/compiler/html_parser_spec.ts @@ -18,7 +18,9 @@ import { HtmlAttrAst, HtmlTextAst, HtmlCommentAst, - htmlVisitAll + htmlVisitAll, + HtmlExpansionAst, + HtmlExpansionCaseAst } from 'angular2/src/compiler/html_ast'; import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util'; import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils'; @@ -227,7 +229,7 @@ export function main() { .toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]); }); - it('should support mamespace', () => { + it('should support namespace', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) .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(`
before{messages.length, plural, =0 {You have no messages} =1 {One {{message}}}}after
`, + '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 = (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 {}`, 'TestComp', true); + expect(humanizeErrors(p.errors)) + .toEqual([['b', 'Only void and foreign elements can be self closed "b"', '0:30']]); + }); + }); + describe('source spans', () => { it('should store the location', () => { expect(humanizeDomSourceSpans(parser.parse(