From 43148d82332ee397ca487a5c4dbc889fd3e7c9c3 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 9 Jun 2016 13:48:53 -0700 Subject: [PATCH] feat(HtmlLexer): add support for alphabetic cases --- modules/@angular/compiler/src/html_lexer.ts | 236 +++++++++--------- modules/@angular/compiler/src/html_parser.ts | 2 +- .../@angular/compiler/src/i18n/expander.ts | 2 +- .../@angular/compiler/test/html_lexer_spec.ts | 124 +++++---- .../compiler/test/html_parser_spec.ts | 51 ++-- .../test/i18n/i18n_html_parser_spec.ts | 72 +++--- .../test/i18n/message_extractor_spec.ts | 21 +- 7 files changed, 285 insertions(+), 223 deletions(-) diff --git a/modules/@angular/compiler/src/html_lexer.ts b/modules/@angular/compiler/src/html_lexer.ts index 11a6df1f03..978bbee545 100644 --- a/modules/@angular/compiler/src/html_lexer.ts +++ b/modules/@angular/compiler/src/html_lexer.ts @@ -108,25 +108,24 @@ class ControlFlowError { // See http://www.w3.org/TR/html51/syntax.html#writing class _HtmlTokenizer { - private input: string; - private length: number; + private _input: string; + private _length: number; // Note: this is always lowercase! - private peek: number = -1; - private nextPeek: number = -1; - private index: number = -1; - private line: number = 0; - private column: number = -1; - private currentTokenStart: ParseLocation; - private currentTokenType: HtmlTokenType; - - private expansionCaseStack: any[] /** TODO #9100 */ = []; + private _peek: number = -1; + private _nextPeek: number = -1; + private _index: number = -1; + private _line: number = 0; + private _column: number = -1; + private _currentTokenStart: ParseLocation; + private _currentTokenType: HtmlTokenType; + private _expansionCaseStack: HtmlTokenType[] = []; tokens: HtmlToken[] = []; errors: HtmlTokenError[] = []; constructor(private file: ParseSourceFile, private tokenizeExpansionForms: boolean) { - this.input = file.content; - this.length = file.content.length; + this._input = file.content; + this._length = file.content.length; this._advance(); } @@ -139,7 +138,7 @@ class _HtmlTokenizer { } tokenize(): HtmlTokenizeResult { - while (this.peek !== $EOF) { + while (this._peek !== $EOF) { var start = this._getLocation(); try { if (this._attemptCharCode($LT)) { @@ -156,18 +155,21 @@ class _HtmlTokenizer { } else { this._consumeTagOpen(start); } - } else if (isSpecialFormStart(this.peek, this.nextPeek) && this.tokenizeExpansionForms) { + } else if ( + isExpansionFormStart(this._peek, this._nextPeek) && this.tokenizeExpansionForms) { this._consumeExpansionFormStart(); - } else if (this.peek === $EQ && this.tokenizeExpansionForms) { + } else if ( + isExpansionCaseStart(this._peek) && this._isInExpansionForm() && + this.tokenizeExpansionForms) { this._consumeExpansionCaseStart(); - } else if ( - this.peek === $RBRACE && this.isInExpansionCase() && this.tokenizeExpansionForms) { + } else if (this._peek === $RBRACE && this._isInExpansionCase() && + this.tokenizeExpansionForms) { this._consumeExpansionCaseEnd(); - } else if ( - this.peek === $RBRACE && this.isInExpansionForm() && this.tokenizeExpansionForms) { + } else if (this._peek === $RBRACE && this._isInExpansionForm() && + this.tokenizeExpansionForms) { this._consumeExpansionFormEnd(); } else { @@ -187,7 +189,7 @@ class _HtmlTokenizer { } private _getLocation(): ParseLocation { - return new ParseLocation(this.file, this.index, this.line, this.column); + return new ParseLocation(this.file, this._index, this._line, this._column); } private _getSpan(start?: ParseLocation, end?: ParseLocation): ParseSourceSpan { @@ -204,47 +206,47 @@ class _HtmlTokenizer { if (isBlank(start)) { start = this._getLocation(); } - this.currentTokenStart = start; - this.currentTokenType = type; + this._currentTokenStart = start; + this._currentTokenType = type; } private _endToken(parts: string[], end: ParseLocation = null): HtmlToken { if (isBlank(end)) { end = this._getLocation(); } - var token = new HtmlToken( - this.currentTokenType, parts, new ParseSourceSpan(this.currentTokenStart, end)); + var token = new HtmlToken(this._currentTokenType, parts, + new ParseSourceSpan(this._currentTokenStart, end)); this.tokens.push(token); - this.currentTokenStart = null; - this.currentTokenType = null; + this._currentTokenStart = null; + this._currentTokenType = null; return token; } private _createError(msg: string, span: ParseSourceSpan): ControlFlowError { - var error = new HtmlTokenError(msg, this.currentTokenType, span); - this.currentTokenStart = null; - this.currentTokenType = null; + var error = new HtmlTokenError(msg, this._currentTokenType, span); + this._currentTokenStart = null; + this._currentTokenType = null; return new ControlFlowError(error); } private _advance() { - if (this.index >= this.length) { + if (this._index >= this._length) { throw this._createError(unexpectedCharacterErrorMsg($EOF), this._getSpan()); } - if (this.peek === $LF) { - this.line++; - this.column = 0; - } else if (this.peek !== $LF && this.peek !== $CR) { - this.column++; + if (this._peek === $LF) { + this._line++; + this._column = 0; + } else if (this._peek !== $LF && this._peek !== $CR) { + this._column++; } - this.index++; - this.peek = this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index); - this.nextPeek = - this.index + 1 >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index + 1); + this._index++; + this._peek = this._index >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index); + this._nextPeek = + this._index + 1 >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index + 1); } private _attemptCharCode(charCode: number): boolean { - if (this.peek === charCode) { + if (this._peek === charCode) { this._advance(); return true; } @@ -252,7 +254,7 @@ class _HtmlTokenizer { } private _attemptCharCodeCaseInsensitive(charCode: number): boolean { - if (compareCharCodeCaseInsensitive(this.peek, charCode)) { + if (compareCharCodeCaseInsensitive(this._peek, charCode)) { this._advance(); return true; } @@ -262,22 +264,22 @@ class _HtmlTokenizer { private _requireCharCode(charCode: number) { var location = this._getLocation(); if (!this._attemptCharCode(charCode)) { - throw this._createError( - unexpectedCharacterErrorMsg(this.peek), this._getSpan(location, location)); + throw this._createError(unexpectedCharacterErrorMsg(this._peek), + this._getSpan(location, location)); } } private _attemptStr(chars: string): boolean { - var indexBeforeAttempt = this.index; - var columnBeforeAttempt = this.column; - var lineBeforeAttempt = this.line; + var indexBeforeAttempt = this._index; + var columnBeforeAttempt = this._column; + var lineBeforeAttempt = this._line; for (var i = 0; i < chars.length; i++) { if (!this._attemptCharCode(StringWrapper.charCodeAt(chars, i))) { // If attempting to parse the string fails, we want to reset the parser // to where it was before the attempt - this.index = indexBeforeAttempt; - this.column = columnBeforeAttempt; - this.line = lineBeforeAttempt; + this._index = indexBeforeAttempt; + this._column = columnBeforeAttempt; + this._line = lineBeforeAttempt; return false; } } @@ -296,12 +298,12 @@ class _HtmlTokenizer { private _requireStr(chars: string) { var location = this._getLocation(); if (!this._attemptStr(chars)) { - throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(location)); + throw this._createError(unexpectedCharacterErrorMsg(this._peek), this._getSpan(location)); } } private _attemptCharCodeUntilFn(predicate: Function) { - while (!predicate(this.peek)) { + while (!predicate(this._peek)) { this._advance(); } } @@ -309,24 +311,24 @@ class _HtmlTokenizer { private _requireCharCodeUntilFn(predicate: Function, len: number) { var start = this._getLocation(); this._attemptCharCodeUntilFn(predicate); - if (this.index - start.offset < len) { - throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(start, start)); + if (this._index - start.offset < len) { + throw this._createError(unexpectedCharacterErrorMsg(this._peek), this._getSpan(start, start)); } } private _attemptUntilChar(char: number) { - while (this.peek !== char) { + while (this._peek !== char) { this._advance(); } } private _readChar(decodeEntities: boolean): string { - if (decodeEntities && this.peek === $AMPERSAND) { + if (decodeEntities && this._peek === $AMPERSAND) { return this._decodeEntity(); } else { - var index = this.index; + var index = this._index; this._advance(); - return this.input[index]; + return this._input[index]; } } @@ -337,27 +339,27 @@ class _HtmlTokenizer { let isHex = this._attemptCharCode($x) || this._attemptCharCode($X); let numberStart = this._getLocation().offset; this._attemptCharCodeUntilFn(isDigitEntityEnd); - if (this.peek != $SEMICOLON) { - throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan()); + if (this._peek != $SEMICOLON) { + throw this._createError(unexpectedCharacterErrorMsg(this._peek), this._getSpan()); } this._advance(); - let strNum = this.input.substring(numberStart, this.index - 1); + let strNum = this._input.substring(numberStart, this._index - 1); try { let charCode = NumberWrapper.parseInt(strNum, isHex ? 16 : 10); return StringWrapper.fromCharCode(charCode); } catch (e) { - let entity = this.input.substring(start.offset + 1, this.index - 1); + let entity = this._input.substring(start.offset + 1, this._index - 1); throw this._createError(unknownEntityErrorMsg(entity), this._getSpan(start)); } } else { let startPosition = this._savePosition(); this._attemptCharCodeUntilFn(isNamedEntityEnd); - if (this.peek != $SEMICOLON) { + if (this._peek != $SEMICOLON) { this._restorePosition(startPosition); return '&'; } this._advance(); - let name = this.input.substring(start.offset + 1, this.index - 1); + let name = this._input.substring(start.offset + 1, this._index - 1); let char = (NAMED_ENTITIES as any /** TODO #9100 */)[name]; if (isBlank(char)) { throw this._createError(unknownEntityErrorMsg(name), this._getSpan(start)); @@ -378,10 +380,10 @@ class _HtmlTokenizer { if (this._attemptCharCode(firstCharOfEnd) && attemptEndRest()) { break; } - if (this.index > tagCloseStart.offset) { - parts.push(this.input.substring(tagCloseStart.offset, this.index)); + if (this._index > tagCloseStart.offset) { + parts.push(this._input.substring(tagCloseStart.offset, this._index)); } - while (this.peek !== firstCharOfEnd) { + while (this._peek !== firstCharOfEnd) { parts.push(this._readChar(decodeEntities)); } } @@ -410,25 +412,25 @@ class _HtmlTokenizer { this._beginToken(HtmlTokenType.DOC_TYPE, start); this._attemptUntilChar($GT); this._advance(); - this._endToken([this.input.substring(start.offset + 2, this.index - 1)]); + this._endToken([this._input.substring(start.offset + 2, this._index - 1)]); } private _consumePrefixAndName(): string[] { - var nameOrPrefixStart = this.index; + var nameOrPrefixStart = this._index; var prefix: any /** TODO #9100 */ = null; - while (this.peek !== $COLON && !isPrefixEnd(this.peek)) { + while (this._peek !== $COLON && !isPrefixEnd(this._peek)) { this._advance(); } var nameStart: any /** TODO #9100 */; - if (this.peek === $COLON) { + if (this._peek === $COLON) { this._advance(); - prefix = this.input.substring(nameOrPrefixStart, this.index - 1); - nameStart = this.index; + prefix = this._input.substring(nameOrPrefixStart, this._index - 1); + nameStart = this._index; } else { nameStart = nameOrPrefixStart; } - this._requireCharCodeUntilFn(isNameEnd, this.index === nameStart ? 1 : 0); - var name = this.input.substring(nameStart, this.index); + this._requireCharCodeUntilFn(isNameEnd, this._index === nameStart ? 1 : 0); + var name = this._input.substring(nameStart, this._index); return [prefix, name]; } @@ -436,14 +438,14 @@ class _HtmlTokenizer { let savedPos = this._savePosition(); let lowercaseTagName: any /** TODO #9100 */; try { - if (!isAsciiLetter(this.peek)) { - throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan()); + if (!isAsciiLetter(this._peek)) { + throw this._createError(unexpectedCharacterErrorMsg(this._peek), this._getSpan()); } - var nameStart = this.index; + var nameStart = this._index; this._consumeTagOpenStart(start); - lowercaseTagName = this.input.substring(nameStart, this.index).toLowerCase(); + lowercaseTagName = this._input.substring(nameStart, this._index).toLowerCase(); this._attemptCharCodeUntilFn(isNotWhitespace); - while (this.peek !== $SLASH && this.peek !== $GT) { + while (this._peek !== $SLASH && this._peek !== $GT) { this._consumeAttributeName(); this._attemptCharCodeUntilFn(isNotWhitespace); if (this._attemptCharCode($EQ)) { @@ -502,19 +504,19 @@ class _HtmlTokenizer { private _consumeAttributeValue() { this._beginToken(HtmlTokenType.ATTR_VALUE); var value: any /** TODO #9100 */; - if (this.peek === $SQ || this.peek === $DQ) { - var quoteChar = this.peek; + if (this._peek === $SQ || this._peek === $DQ) { + var quoteChar = this._peek; this._advance(); var parts: any[] /** TODO #9100 */ = []; - while (this.peek !== quoteChar) { + while (this._peek !== quoteChar) { parts.push(this._readChar(true)); } value = parts.join(''); this._advance(); } else { - var valueStart = this.index; + var valueStart = this._index; this._requireCharCodeUntilFn(isNameEnd, 1); - value = this.input.substring(valueStart, this.index); + value = this._input.substring(valueStart, this._index); } this._endToken([this._processCarriageReturns(value)]); } @@ -554,12 +556,10 @@ class _HtmlTokenizer { this._requireCharCode($COMMA); this._attemptCharCodeUntilFn(isNotWhitespace); - this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START); + this._expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START); } private _consumeExpansionCaseStart() { - this._requireCharCode($EQ); - this._beginToken(HtmlTokenType.EXPANSION_CASE_VALUE, this._getLocation()); let value = this._readUntil($LBRACE).trim(); this._endToken([value], this._getLocation()); @@ -570,7 +570,7 @@ class _HtmlTokenizer { this._endToken([], this._getLocation()); this._attemptCharCodeUntilFn(isNotWhitespace); - this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START); + this._expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START); } private _consumeExpansionCaseEnd() { @@ -579,7 +579,7 @@ class _HtmlTokenizer { this._endToken([], this._getLocation()); this._attemptCharCodeUntilFn(isNotWhitespace); - this.expansionCaseStack.pop(); + this._expansionCaseStack.pop(); } private _consumeExpansionFormEnd() { @@ -587,7 +587,7 @@ class _HtmlTokenizer { this._requireCharCode($RBRACE); this._endToken([]); - this.expansionCaseStack.pop(); + this._expansionCaseStack.pop(); } private _consumeText() { @@ -597,7 +597,7 @@ class _HtmlTokenizer { var parts: any[] /** TODO #9100 */ = []; let interpolation = false; - if (this.peek === $LBRACE && this.nextPeek === $LBRACE) { + if (this._peek === $LBRACE && this._nextPeek === $LBRACE) { parts.push(this._readChar(true)); parts.push(this._readChar(true)); interpolation = true; @@ -605,12 +605,12 @@ class _HtmlTokenizer { parts.push(this._readChar(true)); } - while (!this.isTextEnd(interpolation)) { - if (this.peek === $LBRACE && this.nextPeek === $LBRACE) { + while (!this._isTextEnd(interpolation)) { + if (this._peek === $LBRACE && this._nextPeek === $LBRACE) { parts.push(this._readChar(true)); parts.push(this._readChar(true)); interpolation = true; - } else if (this.peek === $RBRACE && this.nextPeek === $RBRACE && interpolation) { + } else if (this._peek === $RBRACE && this._nextPeek === $RBRACE && interpolation) { parts.push(this._readChar(true)); parts.push(this._readChar(true)); interpolation = false; @@ -621,32 +621,32 @@ class _HtmlTokenizer { this._endToken([this._processCarriageReturns(parts.join(''))]); } - private isTextEnd(interpolation: boolean): boolean { - if (this.peek === $LT || this.peek === $EOF) return true; + private _isTextEnd(interpolation: boolean): boolean { + if (this._peek === $LT || this._peek === $EOF) return true; if (this.tokenizeExpansionForms) { - if (isSpecialFormStart(this.peek, this.nextPeek)) return true; - if (this.peek === $RBRACE && !interpolation && - (this.isInExpansionCase() || this.isInExpansionForm())) + if (isExpansionFormStart(this._peek, this._nextPeek)) return true; + if (this._peek === $RBRACE && !interpolation && + (this._isInExpansionCase() || this._isInExpansionForm())) return true; } return false; } private _savePosition(): number[] { - return [this.peek, this.index, this.column, this.line, this.tokens.length]; + return [this._peek, this._index, this._column, this._line, this.tokens.length]; } private _readUntil(char: number): string { - let start = this.index; + let start = this._index; this._attemptUntilChar(char); - return this.input.substring(start, this.index); + return this._input.substring(start, this._index); } private _restorePosition(position: number[]): void { - this.peek = position[0]; - this.index = position[1]; - this.column = position[2]; - this.line = position[3]; + this._peek = position[0]; + this._index = position[1]; + this._column = position[2]; + this._line = position[3]; let nbTokens = position[4]; if (nbTokens < this.tokens.length) { // remove any extra tokens @@ -654,16 +654,16 @@ class _HtmlTokenizer { } } - private isInExpansionCase(): boolean { - return this.expansionCaseStack.length > 0 && - this.expansionCaseStack[this.expansionCaseStack.length - 1] === - HtmlTokenType.EXPANSION_CASE_EXP_START; + private _isInExpansionCase(): boolean { + return this._expansionCaseStack.length > 0 && + this._expansionCaseStack[this._expansionCaseStack.length - 1] === + HtmlTokenType.EXPANSION_CASE_EXP_START; } - private isInExpansionForm(): boolean { - return this.expansionCaseStack.length > 0 && - this.expansionCaseStack[this.expansionCaseStack.length - 1] === - HtmlTokenType.EXPANSION_FORM_START; + private _isInExpansionForm(): boolean { + return this._expansionCaseStack.length > 0 && + this._expansionCaseStack[this._expansionCaseStack.length - 1] === + HtmlTokenType.EXPANSION_FORM_START; } } @@ -692,10 +692,14 @@ function isNamedEntityEnd(code: number): boolean { return code == $SEMICOLON || code == $EOF || !isAsciiLetter(code); } -function isSpecialFormStart(peek: number, nextPeek: number): boolean { +function isExpansionFormStart(peek: number, nextPeek: number): boolean { return peek === $LBRACE && nextPeek != $LBRACE; } +function isExpansionCaseStart(peek: number): boolean { + return peek === $EQ || isAsciiLetter(peek); +} + function isAsciiLetter(code: number): boolean { return code >= $a && code <= $z || code >= $A && code <= $Z; } diff --git a/modules/@angular/compiler/src/html_parser.ts b/modules/@angular/compiler/src/html_parser.ts index 549ae982c0..b9398f240a 100644 --- a/modules/@angular/compiler/src/html_parser.ts +++ b/modules/@angular/compiler/src/html_parser.ts @@ -105,7 +105,7 @@ class TreeBuilder { let switchValue = this._advance(); let type = this._advance(); - let cases: any[] /** TODO #9100 */ = []; + let cases: HtmlExpansionCaseAst[] = []; // read = while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) { diff --git a/modules/@angular/compiler/src/i18n/expander.ts b/modules/@angular/compiler/src/i18n/expander.ts index cf6397de87..ed28256ef4 100644 --- a/modules/@angular/compiler/src/i18n/expander.ts +++ b/modules/@angular/compiler/src/i18n/expander.ts @@ -12,7 +12,7 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, Ht * { messages.length, plural, * =0 {zero} * =1 {one} - * =other {more than one} + * other {more than one} * } * ``` * diff --git a/modules/@angular/compiler/test/html_lexer_spec.ts b/modules/@angular/compiler/test/html_lexer_spec.ts index 40546e7edb..b7f5fa0b84 100644 --- a/modules/@angular/compiler/test/html_lexer_spec.ts +++ b/modules/@angular/compiler/test/html_lexer_spec.ts @@ -485,62 +485,98 @@ export function main() { }); - describe('expansion forms', () => { - it('should parse an expansion form', () => { - expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} }', true)).toEqual([ - [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'], - [HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_CASE_VALUE, '5'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'five'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.EOF] - ]); + describe("expansion forms", () => { + it("should parse an expansion form", () => { + expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true)) + .toEqual([ + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'four'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=5'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'five'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'bar'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); }); - it('should parse an expansion form with text elements surrounding it', () => { - expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([ - [HtmlTokenType.TEXT, 'before'], [HtmlTokenType.EXPANSION_FORM_START], - [HtmlTokenType.RAW_TEXT, 'one.two'], [HtmlTokenType.RAW_TEXT, 'three'], - [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], [HtmlTokenType.EXPANSION_CASE_EXP_START], - [HtmlTokenType.TEXT, 'four'], [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_FORM_END], [HtmlTokenType.TEXT, 'after'], [HtmlTokenType.EOF] - ]); + it("should parse an expansion form with text elements surrounding it", () => { + expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)) + .toEqual([ + [HtmlTokenType.TEXT, "before"], + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'four'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.TEXT, "after"], + [HtmlTokenType.EOF] + ]); }); - it('should parse an expansion forms with elements in it', () => { - expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four a}}', true)).toEqual([ - [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'], - [HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four '], - [HtmlTokenType.TAG_OPEN_START, null, 'b'], [HtmlTokenType.TAG_OPEN_END], - [HtmlTokenType.TEXT, 'a'], [HtmlTokenType.TAG_CLOSE, null, 'b'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.EOF] - ]); + it("should parse an expansion forms with elements in it", () => { + expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four a}}', true)) + .toEqual([ + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'four '], + [HtmlTokenType.TAG_OPEN_START, null, 'b'], + [HtmlTokenType.TAG_OPEN_END], + [HtmlTokenType.TEXT, 'a'], + [HtmlTokenType.TAG_CLOSE, null, 'b'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); }); - it('should parse an expansion forms with interpolation in it', () => { - expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([ - [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'], - [HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four {{a}}'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.EOF] - ]); + it("should parse an expansion forms with interpolation in it", () => { + expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)) + .toEqual([ + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'four {{a}}'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); }); - it('should parse nested expansion forms', () => { + it("should parse nested expansion forms", () => { expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true)) .toEqual([ - [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'], - [HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], [HtmlTokenType.EXPANSION_CASE_EXP_START], - [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'xx'], - [HtmlTokenType.RAW_TEXT, 'yy'], [HtmlTokenType.EXPANSION_CASE_VALUE, 'x'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'one'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'xx'], + [HtmlTokenType.RAW_TEXT, 'yy'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=x'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'one'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], [HtmlTokenType.TEXT, ' '], [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], diff --git a/modules/@angular/compiler/test/html_parser_spec.ts b/modules/@angular/compiler/test/html_parser_spec.ts index ab2050203a..630c0725c9 100644 --- a/modules/@angular/compiler/test/html_parser_spec.ts +++ b/modules/@angular/compiler/test/html_parser_spec.ts @@ -1,9 +1,15 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '@angular/compiler/src/html_ast'; import {HtmlTokenType} from '@angular/compiler/src/html_lexer'; -import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '@angular/compiler/src/html_parser'; -import {ParseError, ParseLocation} from '@angular/compiler/src/parse_util'; -import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing/testing_internal'; - +import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from '@angular/compiler/src/html_parser'; +import { + HtmlElementAst, + HtmlAttrAst, + HtmlTextAst, + HtmlCommentAst, + HtmlExpansionAst, + HtmlExpansionCaseAst +} from '@angular/compiler/src/html_ast'; +import {ParseError} from '@angular/compiler/src/parse_util'; import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils'; export function main() { @@ -232,12 +238,15 @@ export function main() { `
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] - ]); - + 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([ @@ -254,20 +263,20 @@ export function main() { it('should parse out nested expansion forms', () => { let parsed = parser.parse( `{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, 'TestComp', true); - - - expect(humanizeDom(parsed)).toEqual([ - [HtmlExpansionAst, 'messages.length', 'plural'], - [HtmlExpansionCaseAst, '0'], - ]); + expect(humanizeDom(parsed)) + .toEqual([ + [HtmlExpansionAst, 'messages.length', 'plural'], + [HtmlExpansionCaseAst, '=0'], + ]); let firstCase = (parsed.rootNodes[0]).cases[0]; - expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))).toEqual([ - [HtmlExpansionAst, 'p.gender', 'gender'], - [HtmlExpansionCaseAst, 'm'], - [HtmlTextAst, ' ', 0], - ]); + expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))) + .toEqual([ + [HtmlExpansionAst, 'p.gender', 'gender'], + [HtmlExpansionCaseAst, '=m'], + [HtmlTextAst, ' ', 0], + ]); }); it('should error when expansion form is not closed', () => { diff --git a/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts b/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts index 166aa03e9c..e1c2bf0fb1 100644 --- a/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts +++ b/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts @@ -175,41 +175,52 @@ export function main() { it('should handle the plural expansion form', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('zerobold', 'plural_0', null))] = + 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] - ]); + 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 handle nested expansion forms', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('m', 'gender_m', null))] = 'M'; + translations[id(new Message('m', "gender_=m", null))] = 'M'; let res = parse(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, translations); - expect(humanizeDom(res)).toEqual([ - [HtmlElementAst, 'ul', 0], [HtmlAttrAst, '[ngPlural]', 'messages.length'], - [HtmlElementAst, 'template', 1], [HtmlAttrAst, 'ngPluralCase', '0'], - [HtmlElementAst, 'li', 2], + expect(humanizeDom(res)) + .toEqual([ + [HtmlElementAst, 'ul', 0], + [HtmlAttrAst, '[ngPlural]', 'messages.length'], + [HtmlElementAst, 'template', 1], + [HtmlAttrAst, 'ngPluralCase', '=0'], + [HtmlElementAst, 'li', 2], - [HtmlElementAst, 'ul', 3], [HtmlAttrAst, '[ngSwitch]', 'p.gender'], - [HtmlElementAst, 'template', 4], [HtmlAttrAst, 'ngSwitchWhen', 'm'], - [HtmlElementAst, 'li', 5], [HtmlTextAst, 'M', 6], + [HtmlElementAst, 'ul', 3], + [HtmlAttrAst, '[ngSwitch]', 'p.gender'], + [HtmlElementAst, 'template', 4], + [HtmlAttrAst, 'ngSwitchWhen', '=m'], + [HtmlElementAst, 'li', 5], + [HtmlTextAst, 'M', 6], - [HtmlTextAst, ' ', 3] - ]); + [HtmlTextAst, ' ', 3] + ]); }); it('should correctly set source code positions', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('bold', 'plural_0', null))] = + translations[id(new Message('bold', "plural_=0", null))] = 'BOLD'; let nodes = parse(`{messages.length, plural,=0 {bold}}`, translations).rootNodes; @@ -230,15 +241,15 @@ export function main() { expect(switchExp.sourceSpan.end.col).toEqual(16); let template: HtmlElementAst = ul.children[0]; - expect(template.sourceSpan.start.col).toEqual(26); + expect(template.sourceSpan.start.col).toEqual(25); expect(template.sourceSpan.end.col).toEqual(41); let switchCheck = template.attrs[0]; - expect(switchCheck.sourceSpan.start.col).toEqual(26); + expect(switchCheck.sourceSpan.start.col).toEqual(25); expect(switchCheck.sourceSpan.end.col).toEqual(28); let li: HtmlElementAst = template.children[0]; - expect(li.sourceSpan.start.col).toEqual(26); + expect(li.sourceSpan.start.col).toEqual(25); expect(li.sourceSpan.end.col).toEqual(41); let b: HtmlElementAst = li.children[0]; @@ -248,18 +259,19 @@ export function main() { it('should handle other special forms', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('m', 'gender_male', null))] = 'M'; + 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], - ]); + 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', () => { diff --git a/modules/@angular/compiler/test/i18n/message_extractor_spec.ts b/modules/@angular/compiler/test/i18n/message_extractor_spec.ts index f5aefab954..4b0cbb45d8 100644 --- a/modules/@angular/compiler/test/i18n/message_extractor_spec.ts +++ b/modules/@angular/compiler/test/i18n/message_extractor_spec.ts @@ -151,22 +151,23 @@ export function main() { ]); }); - it('should extract messages from special forms', () => { - let res = extractor.extract( - ` + it("should extract messages from expansion forms", () => { + let res = extractor.extract(`
{messages.length, plural, =0 {You have no messages} =1 {You have one message} + other {You have messages} } -
- `, - 'someurl'); + `, + "someurl"); - expect(res.messages).toEqual([ - new Message('You have no messages', 'plural_0', null), - new Message('You have one message', 'plural_1', null) - ]); + expect(res.messages) + .toEqual([ + new Message('You have no messages', "plural_=0", null), + new Message('You have one message', "plural_=1", null), + new Message('You have messages', "plural_other", null), + ]); }); it('should remove duplicate messages', () => {