feat(i18n): xtb serializer

This commit is contained in:
Victor Berchet
2016-07-21 13:56:58 -07:00
parent 1b77604ee2
commit 0eee1d5de3
71 changed files with 3152 additions and 4240 deletions

View File

@ -1,9 +1,17 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {beforeEach, ddescribe, describe, expect, it} from '../../../core/testing/testing_internal';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../../src/html_parser/html_ast';
import * as html from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
export function main() {
describe('HtmlAst serilaizer', () => {
describe('Node serilaizer', () => {
var parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
@ -52,35 +60,37 @@ export function main() {
});
}
class _SerializerVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any {
return `<${ast.name}${this._visitAll(ast.attrs, ' ')}>${this._visitAll(ast.children)}</${ast.name}>`;
class _SerializerVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
return `<${element.name}${this._visitAll(element.attrs, ' ')}>${this._visitAll(element.children)}</${element.name}>`;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return `${ast.name}="${ast.value}"`; }
visitText(ast: HtmlTextAst, context: any): any { return ast.value; }
visitComment(ast: HtmlCommentAst, context: any): any { return `<!--${ast.value}-->`; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
return `{${ast.switchValue}, ${ast.type},${this._visitAll(ast.cases)}}`;
visitAttribute(attribute: html.Attribute, context: any): any {
return `${attribute.name}="${attribute.value}"`;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
return ` ${ast.value} {${this._visitAll(ast.expression)}}`;
visitText(text: html.Text, context: any): any { return text.value; }
visitComment(comment: html.Comment, context: any): any { return `<!--${comment.value}-->`; }
visitExpansion(expansion: html.Expansion, context: any): any {
return `{${expansion.switchValue}, ${expansion.type},${this._visitAll(expansion.cases)}}`;
}
private _visitAll(ast: HtmlAst[], join: string = ''): string {
if (ast.length == 0) {
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
return ` ${expansionCase.value} {${this._visitAll(expansionCase.expression)}}`;
}
private _visitAll(nodes: html.Node[], join: string = ''): string {
if (nodes.length == 0) {
return '';
}
return join + ast.map(a => a.visit(this, null)).join(join);
return join + nodes.map(a => a.visit(this, null)).join(join);
}
}
const serializerVisitor = new _SerializerVisitor();
export function serializeAst(ast: HtmlAst[]): string[] {
return ast.map(a => a.visit(serializerVisitor, null));
export function serializeAst(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
}

View File

@ -0,0 +1,85 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BaseException} from '../../src/facade/exceptions';
import * as html from '../../src/html_parser/ast';
import {ParseTreeResult} from '../../src/html_parser/html_parser';
import {ParseLocation} from '../../src/parse_util';
export function humanizeDom(parseResult: ParseTreeResult, addSourceSpan: boolean = false): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
}
export function humanizeDomSourceSpans(parseResult: ParseTreeResult): any[] {
return humanizeDom(parseResult, true);
}
export function humanizeNodes(nodes: html.Node[], addSourceSpan: boolean = false): any[] {
var humanizer = new _Humanizer(addSourceSpan);
html.visitAll(humanizer, nodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements html.Visitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(element: html.Element, context: any): any {
var res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
this.result.push(res);
html.visitAll(this, element.attrs);
html.visitAll(this, element.children);
this.elDepth--;
}
visitAttribute(attribute: html.Attribute, context: any): any {
var res = this._appendContext(attribute, [html.Attribute, attribute.name, attribute.value]);
this.result.push(res);
}
visitText(text: html.Text, context: any): any {
var res = this._appendContext(text, [html.Text, text.value, this.elDepth]);
this.result.push(res);
}
visitComment(comment: html.Comment, context: any): any {
var res = this._appendContext(comment, [html.Comment, comment.value, this.elDepth]);
this.result.push(res);
}
visitExpansion(expansion: html.Expansion, context: any): any {
var res = this._appendContext(
expansion, [html.Expansion, expansion.switchValue, expansion.type, this.elDepth++]);
this.result.push(res);
html.visitAll(this, expansion.cases);
this.elDepth--;
}
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
var res =
this._appendContext(expansionCase, [html.ExpansionCase, expansionCase.value, this.elDepth]);
this.result.push(res);
}
private _appendContext(ast: html.Node, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -1,85 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BaseException} from '../../src/facade/exceptions';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../../src/html_parser/html_ast';
import {HtmlParseTreeResult} from '../../src/html_parser/html_parser';
import {ParseLocation} from '../../src/parse_util';
export function humanizeDom(
parseResult: HtmlParseTreeResult, addSourceSpan: boolean = false): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
}
export function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
return humanizeDom(parseResult, true);
}
export function humanizeNodes(nodes: HtmlAst[], addSourceSpan: boolean = false): any[] {
var humanizer = new _Humanizer(addSourceSpan);
htmlVisitAll(humanizer, nodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements HtmlAstVisitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(ast: HtmlElementAst, context: any): any {
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.attrs);
htmlVisitAll(this, ast.children);
this.elDepth--;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
this.result.push(res);
}
visitText(ast: HtmlTextAst, context: any): any {
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
this.result.push(res);
}
visitComment(ast: HtmlCommentAst, context: any): any {
var res = this._appendContext(ast, [HtmlCommentAst, ast.value, this.elDepth]);
this.result.push(res);
}
visitExpansion(ast: HtmlExpansionAst, context: any): any {
var res =
this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.cases);
this.elDepth--;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value, this.elDepth]);
this.result.push(res);
}
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -1,801 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {HtmlToken, HtmlTokenError, HtmlTokenType, tokenizeHtml} from '../../src/html_parser/html_lexer';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '0:4'],
[HtmlTokenType.EOF, '0:8'],
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '1:1'],
[HtmlTokenType.EOF, '1:5'],
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5'],
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5'],
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
[HtmlTokenType.COMMENT_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.COMMENT_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
[HtmlTokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
]);
});
it('should accept comments finishing by too many dashes (even number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, ' test --'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
it('should accept comments finishing by too many dashes (odd number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, ' test -'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
[HtmlTokenType.DOC_TYPE, 'doctype html'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
[HtmlTokenType.DOC_TYPE, '<!doctype html>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
[HtmlTokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[HtmlTokenType.CDATA_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.CDATA_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[HtmlTokenType.CDATA_START, '<![CDATA['],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.CDATA_END, ']]>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
[HtmlTokenType.CDATA_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, 'ns1', 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END_VOID],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<test'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with interpolation', () => {
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, '{{v}}'],
[HtmlTokenType.ATTR_NAME, null, 'b'],
[HtmlTokenType.ATTR_VALUE, 's{{m}}e'],
[HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.ATTR_VALUE, 's{{m//c}}e'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, 'ns1', 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, '(ns1:a)'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'AA'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, '&amp'],
[HtmlTokenType.ATTR_NAME, null, 'b'],
[HtmlTokenType.ATTR_VALUE, 'c&&d'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b && c &'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 't\ne\ns\nt'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<t'],
[HtmlTokenType.ATTR_NAME, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, null, 'test'],
[HtmlTokenType.EOF],
]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'ns1', 'test'],
[HtmlTokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
[HtmlTokenType.TAG_CLOSE, null, 'test'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
[HtmlTokenType.TAG_CLOSE, '</test>'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&b'],
[HtmlTokenType.EOF],
]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;')).toEqual([
[HtmlTokenType.TEXT, 'AA'],
[HtmlTokenType.EOF],
]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;')).toEqual([
[HtmlTokenType.TEXT, 'A'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&amp;b'],
[HtmlTokenType.EOF, ''],
]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
HtmlTokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
]]);
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:3']
]);
expect(tokenizeAndHumanizeErrors('&#xasdf;')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:4']
]);
expect(tokenizeAndHumanizeErrors('&#xABC')).toEqual([
[HtmlTokenType.TEXT, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a')).toEqual([
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.EOF],
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a }}b{{ c // comment }}'],
[HtmlTokenType.EOF],
]);
});
it('should parse interpolation with custom markers', () => {
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[HtmlTokenType.TEXT, '{% a %}'],
[HtmlTokenType.EOF],
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[HtmlTokenType.TEXT, 't\ne\ns\nt'],
[HtmlTokenType.EOF],
]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[HtmlTokenType.TEXT, 'a&b'],
[HtmlTokenType.EOF],
]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
[HtmlTokenType.TEXT, 'a && b &'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.EOF, ''],
]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a < b ? c : d }}'],
[HtmlTokenType.EOF],
]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'a<b'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
[HtmlTokenType.TEXT, '< a>'],
[HtmlTokenType.EOF],
]);
});
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a '],
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.ATTR_NAME, null, '&&'],
[HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.TEXT, ' d }}'],
[HtmlTokenType.EOF],
]);
});
it('should be able to escape {', () => {
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
[HtmlTokenType.TEXT, '{{ "{" }}'],
[HtmlTokenType.EOF],
]);
});
it('should be able to escape {{', () => {
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
[HtmlTokenType.TEXT, '{{ "{{" }}'],
[HtmlTokenType.EOF],
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, '&amp;'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<script'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</script>'],
[HtmlTokenType.EOF, ''],
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, '&'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<title'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</title>'],
[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 forms with elements in it', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', 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 containing an interpolation', () => {
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', () => {
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_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.TEXT, ' '],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF],
]);
});
});
describe('errors', () => {
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
HtmlTokenType.RAW_TEXT,
'Unexpected character "EOF" (Do you have an unescaped "{" in your template?).',
'0:21',
]]);
});
it('should include 2 lines of context in message', () => {
let src = '111\n222\n333\nE\n444\n555\n666\n';
let file = new ParseSourceFile(src, 'file://');
let location = new ParseLocation(file, 12, 123, 456);
let span = new ParseSourceSpan(location, location);
let error = new HtmlTokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'İ'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, ''],
]);
});
});
});
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): HtmlToken[] {
var tokenizeResult = tokenizeHtml(input, 'someUrl', tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
const errorString = tokenizeResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input).map(
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
return tokenizeHtml(input, 'someUrl', tokenizeExpansionForms)
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
}

View File

@ -7,12 +7,12 @@
*/
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import {HtmlTokenType} from '../../src/html_parser/html_lexer';
import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '../../src/html_parser/html_parser';
import * as html from '../../src/html_parser/ast';
import {HtmlParser, ParseTreeResult, TreeError} from '../../src/html_parser/html_parser';
import {TokenType} from '../../src/html_parser/lexer';
import {ParseError} from '../../src/parse_util';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './ast_spec_utils';
export function main() {
describe('HtmlParser', () => {
@ -23,24 +23,24 @@ export function main() {
describe('parse', () => {
describe('text nodes', () => {
it('should parse root level text nodes', () => {
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a', 0]]);
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[html.Text, 'a', 0]]);
});
it('should parse text nodes inside regular elements', () => {
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]
[html.Element, 'div', 0], [html.Text, 'a', 1]
]);
});
it('should parse text nodes inside template elements', () => {
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]
[html.Element, 'template', 0], [html.Text, 'a', 1]
]);
});
it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp'))).toEqual([
[HtmlTextAst, 'text', 0]
[html.Text, 'text', 0]
]);
});
});
@ -48,27 +48,27 @@ export function main() {
describe('elements', () => {
it('should parse root level elements', () => {
expect(humanizeDom(parser.parse('<div></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0]
[html.Element, 'div', 0]
]);
});
it('should parse elements inside of regular elements', () => {
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlElementAst, 'span', 1]
[html.Element, 'div', 0], [html.Element, 'span', 1]
]);
});
it('should parse elements inside of template elements', () => {
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlElementAst, 'span', 1]]);
.toEqual([[html.Element, 'template', 0], [html.Element, 'span', 1]]);
});
it('should support void elements', () => {
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', 'TestComp')))
.toEqual([
[HtmlElementAst, 'link', 0],
[HtmlAttrAst, 'rel', 'author license'],
[HtmlAttrAst, 'href', '/about'],
[html.Element, 'link', 0],
[html.Attribute, 'rel', 'author license'],
[html.Attribute, 'href', '/about'],
]);
});
@ -87,30 +87,30 @@ export function main() {
it('should close void elements on text nodes', () => {
expect(humanizeDom(parser.parse('<p>before<br>after</p>', 'TestComp'))).toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, 'before', 1],
[HtmlElementAst, 'br', 1],
[HtmlTextAst, 'after', 1],
[html.Element, 'p', 0],
[html.Text, 'before', 1],
[html.Element, 'br', 1],
[html.Text, 'after', 1],
]);
});
it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '1', 2],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '2', 2],
[html.Element, 'div', 0],
[html.Element, 'p', 1],
[html.Text, '1', 2],
[html.Element, 'p', 1],
[html.Text, '2', 2],
]);
});
it('should support nested elements', () => {
expect(humanizeDom(parser.parse('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlElementAst, 'li', 1],
[HtmlElementAst, 'ul', 2],
[HtmlElementAst, 'li', 3],
[html.Element, 'ul', 0],
[html.Element, 'li', 1],
[html.Element, 'ul', 2],
[html.Element, 'li', 3],
]);
});
@ -120,19 +120,19 @@ export function main() {
'<table><thead><tr head></tr></thead><tr noparent></tr><tbody><tr body></tr></tbody><tfoot><tr foot></tr></tfoot></table>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'head', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'noparent', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'body', ''],
[HtmlElementAst, 'tfoot', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'foot', ''],
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'head', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'noparent', ''],
[html.Element, 'tbody', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'body', ''],
[html.Element, 'tfoot', 1],
[html.Element, 'tr', 2],
[html.Attribute, 'foot', ''],
]);
});
@ -140,10 +140,10 @@ export function main() {
expect(humanizeDom(parser.parse(
'<table><ng-container><tr></tr></ng-container></table>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'ng-container', 2],
[HtmlElementAst, 'tr', 3],
[html.Element, 'table', 0],
[html.Element, 'tbody', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
@ -152,42 +152,43 @@ export function main() {
'<table><thead><ng-container><tr></tr></ng-container></thead></table>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'ng-container', 2],
[HtmlElementAst, 'tr', 3],
[html.Element, 'table', 0],
[html.Element, 'thead', 1],
[html.Element, 'ng-container', 2],
[html.Element, 'tr', 3],
]);
});
it('should not add the requiredParent when the parent is a template', () => {
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0],
[HtmlElementAst, 'tr', 1],
[html.Element, 'template', 0],
[html.Element, 'tr', 1],
]);
});
// https://github.com/angular/angular/issues/5967
it('should not add the requiredParent to a template root element', () => {
expect(humanizeDom(parser.parse('<tr></tr>', 'TestComp'))).toEqual([
[HtmlElementAst, 'tr', 0],
[html.Element, 'tr', 0],
]);
});
it('should support explicit mamespace', () => {
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp'))).toEqual([
[HtmlElementAst, ':myns:div', 0]
[html.Element, ':myns:div', 0]
]);
});
it('should support implicit mamespace', () => {
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:svg', 0]
[html.Element, ':svg:svg', 0]
]);
});
it('should propagate the namespace', () => {
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp'))).toEqual([
[HtmlElementAst, ':myns:div', 0], [HtmlElementAst, ':myns:p', 1]
[html.Element, ':myns:div', 0],
[html.Element, ':myns:p', 1],
]);
});
@ -202,13 +203,13 @@ export function main() {
it('should support self closing void elements', () => {
expect(humanizeDom(parser.parse('<input />', 'TestComp'))).toEqual([
[HtmlElementAst, 'input', 0]
[html.Element, 'input', 0]
]);
});
it('should support self closing foreign elements', () => {
expect(humanizeDom(parser.parse('<math />', 'TestComp'))).toEqual([
[HtmlElementAst, ':math:math', 0]
[html.Element, ':math:math', 0]
]);
});
@ -217,13 +218,13 @@ export function main() {
'<p>\n</p><textarea>\n</textarea><pre>\n\n</pre><listing>\n\n</listing>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'textarea', 0],
[HtmlElementAst, 'pre', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'listing', 0],
[HtmlTextAst, '\n', 1],
[html.Element, 'p', 0],
[html.Text, '\n', 1],
[html.Element, 'textarea', 0],
[html.Element, 'pre', 0],
[html.Text, '\n', 1],
[html.Element, 'listing', 0],
[html.Text, '\n', 1],
]);
});
@ -232,33 +233,37 @@ export function main() {
describe('attributes', () => {
it('should parse attributes on regular elements case sensitive', () => {
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'kEy', 'v'],
[HtmlAttrAst, 'key2', 'v2'],
[html.Element, 'div', 0],
[html.Attribute, 'kEy', 'v'],
[html.Attribute, 'key2', 'v2'],
]);
});
it('should parse attributes without values', () => {
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp'))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'k', '']
[html.Element, 'div', 0],
[html.Attribute, 'k', ''],
]);
});
it('should parse attributes on svg elements case sensitive', () => {
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:svg', 0], [HtmlAttrAst, 'viewBox', '0']
[html.Element, ':svg:svg', 0],
[html.Attribute, 'viewBox', '0'],
]);
});
it('should parse attributes on template elements', () => {
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp'))).toEqual([
[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']
[html.Element, 'template', 0],
[html.Attribute, 'k', 'v'],
]);
});
it('should support namespace', () => {
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp'))).toEqual([
[HtmlElementAst, ':svg:use', 0], [HtmlAttrAst, ':xlink:href', 'Port']
[html.Element, ':svg:use', 0],
[html.Attribute, ':xlink:href', 'Port'],
]);
});
});
@ -266,7 +271,8 @@ export function main() {
describe('comments', () => {
it('should preserve comments', () => {
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp'))).toEqual([
[HtmlCommentAst, 'comment', 0], [HtmlElementAst, 'div', 0]
[html.Comment, 'comment', 0],
[html.Element, 'div', 0],
]);
});
});
@ -278,54 +284,54 @@ export function main() {
'TestComp', true);
expect(humanizeDom(parsed)).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlTextAst, 'before', 1],
[HtmlExpansionAst, 'messages.length', 'plural', 1],
[HtmlExpansionCaseAst, '=0', 2],
[HtmlExpansionCaseAst, '=1', 2],
[HtmlTextAst, 'after', 1],
[html.Element, 'div', 0],
[html.Text, 'before', 1],
[html.Expansion, 'messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, 'after', 1],
]);
let cases = (<any>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 ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have ', 0],
[html.Element, 'b', 0],
[html.Text, 'no', 1],
[html.Text, ' messages', 0],
]);
expect(humanizeDom(new HtmlParseTreeResult(cases[1].expression, [
]))).toEqual([[HtmlTextAst, 'One {{message}}', 0]]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, [
]))).toEqual([[html.Text, 'One {{message}}', 0]]);
});
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', 0],
[HtmlExpansionCaseAst, '=0', 1],
[html.Expansion, 'messages.length', 'plural', 0],
[html.ExpansionCase, '=0', 1],
]);
let firstCase = (<any>parsed.rootNodes[0]).cases[0];
expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))).toEqual([
[HtmlExpansionAst, 'p.gender', 'gender', 0],
[HtmlExpansionCaseAst, '=m', 1],
[HtmlTextAst, ' ', 0],
expect(humanizeDom(new ParseTreeResult(firstCase.expression, []))).toEqual([
[html.Expansion, 'p.gender', 'gender', 0],
[html.ExpansionCase, '=m', 1],
[html.Text, ' ', 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']
[null, 'Invalid ICU message. 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']
[null, 'Invalid ICU message. Missing \'}\'.', '0:29']
]);
});
@ -342,17 +348,17 @@ export function main() {
expect(humanizeDomSourceSpans(parser.parse(
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[HtmlAttrAst, '[prop]', 'v1', '[prop]="v1"'],
[HtmlAttrAst, '(e)', 'do()', '(e)="do()"'],
[HtmlAttrAst, 'attr', 'v2', 'attr="v2"'],
[HtmlAttrAst, 'noValue', '', 'noValue'],
[HtmlTextAst, '\na\n', 1, '\na\n'],
[html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[html.Attribute, '[prop]', 'v1', '[prop]="v1"'],
[html.Attribute, '(e)', 'do()', '(e)="do()"'],
[html.Attribute, 'attr', 'v2', 'attr="v2"'],
[html.Attribute, 'noValue', '', 'noValue'],
[html.Text, '\na\n', 1, '\na\n'],
]);
});
it('should set the start and end source spans', () => {
let node = <HtmlElementAst>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
let node = <html.Element>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
@ -360,6 +366,16 @@ export function main() {
expect(node.endSourceSpan.start.offset).toEqual(6);
expect(node.endSourceSpan.end.offset).toEqual(12);
});
it('should support expansion form', () => {
expect(humanizeDomSourceSpans(
parser.parse('<div>{count, plural, =0 {msg}}</div>', 'TestComp', true)))
.toEqual([
[html.Element, 'div', 0, '<div>'],
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
]);
});
});
describe('errors', () => {
@ -403,7 +419,7 @@ export function main() {
let errors = parser.parse('<!-err--><div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors)).toEqual([
[HtmlTokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
[TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
['p', 'Unexpected closing tag "p"', '0:14']
]);
});
@ -414,7 +430,7 @@ export function main() {
export function humanizeErrors(errors: ParseError[]): any[] {
return errors.map(e => {
if (e instanceof HtmlTreeError) {
if (e instanceof TreeError) {
// Parser errors
return [<any>e.elementName, e.msg, humanizeLineColumn(e.span.start)];
}

View File

@ -7,12 +7,12 @@
*/
import {ddescribe, describe, expect, iit, it} from '../../../core/testing/testing_internal';
import {ExpansionResult, expandNodes} from '../../src/html_parser/expander';
import {HtmlAttrAst, HtmlElementAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import * as html from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {ExpansionResult, expandNodes} from '../../src/html_parser/icu_ast_expander';
import {ParseError} from '../../src/parse_util';
import {humanizeNodes} from './html_ast_spec_utils';
import {humanizeNodes} from './ast_spec_utils';
export function main() {
describe('Expander', () => {
@ -26,13 +26,13 @@ export function main() {
const res = expand(`{messages.length, plural,=0 {zero<b>bold</b>}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '=0'],
[HtmlTextAst, 'zero', 2],
[HtmlElementAst, 'b', 2],
[HtmlTextAst, 'bold', 3],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Text, 'zero', 2],
[html.Element, 'b', 2],
[html.Text, 'bold', 3],
]);
});
@ -40,23 +40,23 @@ export function main() {
const res = expand(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '=0'],
[HtmlElementAst, 'ng-container', 2],
[HtmlAttrAst, '[ngSwitch]', 'p.gender'],
[HtmlElementAst, 'template', 3],
[HtmlAttrAst, 'ngSwitchCase', '=m'],
[HtmlTextAst, 'm', 4],
[HtmlTextAst, ' ', 2],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngPlural]', 'messages.length'],
[html.Element, 'template', 1],
[html.Attribute, 'ngPluralCase', '=0'],
[html.Element, 'ng-container', 2],
[html.Attribute, '[ngSwitch]', 'p.gender'],
[html.Element, 'template', 3],
[html.Attribute, 'ngSwitchCase', '=m'],
[html.Text, 'm', 4],
[html.Text, ' ', 2],
]);
});
it('should correctly set source code positions', () => {
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
const container: HtmlElementAst = <HtmlElementAst>nodes[0];
const container: html.Element = <html.Element>nodes[0];
expect(container.sourceSpan.start.col).toEqual(0);
expect(container.sourceSpan.end.col).toEqual(42);
expect(container.startSourceSpan.start.col).toEqual(0);
@ -68,7 +68,7 @@ export function main() {
expect(switchExp.sourceSpan.start.col).toEqual(1);
expect(switchExp.sourceSpan.end.col).toEqual(16);
const template: HtmlElementAst = <HtmlElementAst>container.children[0];
const template: html.Element = <html.Element>container.children[0];
expect(template.sourceSpan.start.col).toEqual(25);
expect(template.sourceSpan.end.col).toEqual(41);
@ -76,7 +76,7 @@ export function main() {
expect(switchCheck.sourceSpan.start.col).toEqual(25);
expect(switchCheck.sourceSpan.end.col).toEqual(28);
const b: HtmlElementAst = <HtmlElementAst>template.children[0];
const b: html.Element = <html.Element>template.children[0];
expect(b.sourceSpan.start.col).toEqual(29);
expect(b.endSourceSpan.end.col).toEqual(40);
});
@ -85,11 +85,11 @@ export function main() {
const res = expand(`{person.gender, gender,=male {m}}`);
expect(humanizeNodes(res.nodes)).toEqual([
[HtmlElementAst, 'ng-container', 0],
[HtmlAttrAst, '[ngSwitch]', 'person.gender'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngSwitchCase', '=male'],
[HtmlTextAst, 'm', 2],
[html.Element, 'ng-container', 0],
[html.Attribute, '[ngSwitch]', 'person.gender'],
[html.Element, 'template', 1],
[html.Attribute, 'ngSwitchCase', '=male'],
[html.Text, 'm', 2],
]);
});

View File

@ -0,0 +1,803 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
import {getHtmlTagDefinition} from '../../src/html_parser/html_tags';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import * as lex from '../../src/html_parser/lexer';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '0:4'],
[lex.TokenType.EOF, '0:8'],
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '0:2'],
[lex.TokenType.TEXT, '0:3'],
[lex.TokenType.TAG_CLOSE, '1:1'],
[lex.TokenType.EOF, '1:5'],
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '0:0'],
[lex.TokenType.TAG_OPEN_END, '1:0'],
[lex.TokenType.TEXT, '1:1'],
[lex.TokenType.TAG_CLOSE, '2:1'],
[lex.TokenType.EOF, '2:5'],
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.COMMENT_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
[lex.TokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
]);
});
it('should accept comments finishing by too many dashes (even number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test --'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
it('should accept comments finishing by too many dashes (odd number)', () => {
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
[lex.TokenType.COMMENT_START, '<!--'],
[lex.TokenType.RAW_TEXT, ' test -'],
[lex.TokenType.COMMENT_END, '-->'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, 'doctype html'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
[lex.TokenType.DOC_TYPE, '<!doctype html>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
[lex.TokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.CDATA_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
[lex.TokenType.CDATA_START, '<![CDATA['],
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[lex.TokenType.CDATA_END, ']]>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
[lex.TokenType.CDATA_START, 'Unexpected character "a"', '0:3']
]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, 'ns1', 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END_VOID],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'test'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<test'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with interpolation', () => {
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '{{v}}'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 's{{m}}e'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.ATTR_VALUE, 's{{m//c}}e'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, 'ns1', 'a'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, '(ns1:a)'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'AA'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, '&amp'],
[lex.TokenType.ATTR_NAME, null, 'b'],
[lex.TokenType.ATTR_VALUE, 'c&&d'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 'b && c &'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 't'],
[lex.TokenType.ATTR_NAME, null, 'a'],
[lex.TokenType.ATTR_VALUE, 't\ne\ns\nt'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<t'],
[lex.TokenType.ATTR_NAME, 'a'],
[lex.TokenType.ATTR_VALUE, 'b'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
[lex.TokenType.TAG_CLOSE, 'ns1', 'test'],
[lex.TokenType.EOF],
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
[lex.TokenType.TAG_CLOSE, null, 'test'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
[lex.TokenType.TAG_CLOSE, '</test>'],
[lex.TokenType.EOF, ''],
]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;')).toEqual([
[lex.TokenType.TEXT, 'AA'],
[lex.TokenType.EOF],
]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;')).toEqual([
[lex.TokenType.TEXT, 'A'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&amp;b'],
[lex.TokenType.EOF, ''],
]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
lex.TokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
]]);
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:3']
]);
expect(tokenizeAndHumanizeErrors('&#xasdf;')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
]);
expect(tokenizeAndHumanizeErrors('&#xABC')).toEqual([
[lex.TokenType.TEXT, 'Unexpected character "EOF"', '0:6']
]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
[lex.TokenType.TEXT, '{{ a }}b{{ c // comment }}'],
[lex.TokenType.EOF],
]);
});
it('should parse interpolation with custom markers', () => {
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[lex.TokenType.TEXT, '{% a %}'],
[lex.TokenType.EOF],
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[lex.TokenType.TEXT, 't\ne\ns\nt'],
[lex.TokenType.EOF],
]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b')).toEqual([
[lex.TokenType.TEXT, 'a&b'],
[lex.TokenType.EOF],
]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
[lex.TokenType.TEXT, 'a && b &'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.EOF, ''],
]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a < b ? c : d }}'],
[lex.TokenType.EOF],
]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'a<b'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
[lex.TokenType.TEXT, '< a>'],
[lex.TokenType.EOF],
]);
});
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
[lex.TokenType.TEXT, '{{ a '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.ATTR_NAME, null, '&&'],
[lex.TokenType.ATTR_NAME, null, 'c'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, ' d }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {', () => {
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{" }}'],
[lex.TokenType.EOF],
]);
});
it('should be able to escape {{', () => {
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
[lex.TokenType.TEXT, '{{ "{{" }}'],
[lex.TokenType.EOF],
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, '&amp;'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'script'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'script'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<script'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</script>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, '&'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, null, 'title'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[lex.TokenType.TAG_CLOSE, null, 'title'],
[lex.TokenType.EOF],
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<title'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, '</title>'],
[lex.TokenType.EOF, ''],
]);
});
});
describe('expansion forms', () => {
it('should parse an expansion form', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, '=5'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'five'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_CASE_VALUE, 'foo'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'bar'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion form with text elements surrounding it', () => {
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
[lex.TokenType.TEXT, 'before'],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, 'after'],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms with elements in it', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four '],
[lex.TokenType.TAG_OPEN_START, null, 'b'],
[lex.TokenType.TAG_OPEN_END],
[lex.TokenType.TEXT, 'a'],
[lex.TokenType.TAG_CLOSE, null, 'b'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse an expansion forms containing an interpolation', () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'four {{a}}'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
it('should parse nested expansion forms', () => {
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
.toEqual([
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'one.two'],
[lex.TokenType.RAW_TEXT, 'three'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.EXPANSION_FORM_START],
[lex.TokenType.RAW_TEXT, 'xx'],
[lex.TokenType.RAW_TEXT, 'yy'],
[lex.TokenType.EXPANSION_CASE_VALUE, '=x'],
[lex.TokenType.EXPANSION_CASE_EXP_START],
[lex.TokenType.TEXT, 'one'],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.TEXT, ' '],
[lex.TokenType.EXPANSION_CASE_EXP_END],
[lex.TokenType.EXPANSION_FORM_END],
[lex.TokenType.EOF],
]);
});
});
describe('errors', () => {
it('should report unescaped "{" on error', () => {
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
lex.TokenType.RAW_TEXT,
`Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`,
'0:21',
]]);
});
it('should include 2 lines of context in message', () => {
let src = '111\n222\n333\nE\n444\n555\n666\n';
let file = new ParseSourceFile(src, 'file://');
let location = new ParseLocation(file, 12, 123, 456);
let span = new ParseSourceSpan(location, location);
let error = new lex.TokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
[lex.TokenType.TAG_OPEN_START, '<p'],
[lex.TokenType.TAG_OPEN_END, '>'],
[lex.TokenType.TEXT, 'İ'],
[lex.TokenType.TAG_CLOSE, '</p>'],
[lex.TokenType.EOF, ''],
]);
});
});
});
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): lex.Token[] {
var tokenizeResult = lex.tokenize(
input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
const errorString = tokenizeResult.errors.join('\n');
throw new Error(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input).map(
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
return lex.tokenize(input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms)
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
}

View File

@ -10,261 +10,248 @@ import {ExtractionResult, extractAstMessages} from '@angular/compiler/src/i18n/e
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {serializeAst} from '../html_parser/html_ast_serializer_spec'
import {serializeAst} from '../html_parser/ast_serializer_spec';
export function main() {
ddescribe(
'MessageExtractor',
() => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text<span>nested</span></div>')).toEqual([
[['text', '<span>nested</span>'], 'm', 'd'],
]);
});
describe('MessageExtractor', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text<span>nested</span></div>')).toEqual([
[['text', '<span>nested</span>'], 'm', 'd'],
]);
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], 'meaning2', ''],
[['message3'], '', ''],
]);
});
it('should extract siblings', () => {
expect(
extract(
`<!-- i18n -->text<p>html<b>nested</b></p>{count, plural, =0 {<span>html</span>}}{{interp}}<!-- /i18n -->`))
.toEqual([
[['{count, plural, =0 {<span>html</span>}}'], '', ''],
[
[
'text', '<p>html<b>nested</b></p>', '{count, plural, =0 {<span>html</span>}}',
'{{interp}}'
],
'', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
]);
});
it('should not create a message for empty blocks',
() => { expect(extract(`<!-- i18n: meaning1|desc1 --><!-- /i18n -->`)).toEqual([]); });
});
describe('ICU messages', () => {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
.toEqual([
[['message1'], 'meaning1', 'desc1'],
[['message2'], 'meaning2', ''],
[['message3'], '', ''],
]);
});
// one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
it('should extract siblings', () => {
expect(
extract(
`<!-- i18n -->text<p>html<b>nested</b></p>{count, plural, =0 {<span>html</span>}}{{interp}}<!-- /i18n -->`))
.toEqual([
[['{count, plural, =0 {<span>html</span>}}'], '', ''],
[
[
'text', '<p>html<b>nested</b></p>', '{count, plural, =0 {<span>html</span>}}',
'{{interp}}'
],
'', ''
],
]);
});
it('should ignore other comments', () => {
expect(extract(`<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`))
.toEqual([
[['message1'], 'meaning1', 'desc1'],
]);
});
it('should not create a message for empty blocks',
() => { expect(extract(`<!-- i18n: meaning1|desc1 --><!-- /i18n -->`)).toEqual([]); });
});
describe('ICU messages', () => {
it('should extract ICU messages from translatable elements', () => {
// single message when ICU is the only children
expect(extract('<div i18n="m|d">{count, plural, =0 {text}}</div>')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
]);
// one message for the element content and one message for the ICU
expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
[['{count, plural, =0 {text}}'], '', ''],
]);
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
]);
// one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {text}}'], '', ''],
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
]);
});
});
it('should extract ICU messages from translatable block', () => {
// single message when ICU is the only children
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {text}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {text}}'], 'm', 'd'],
it('should not extract ICU messages outside of i18n sections',
() => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, gender, =m {m}} }}</div>'))
.toEqual([
[['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'],
]);
});
});
// one message for the block content and one message for the ICU
expect(extract('<!-- i18n:m|d -->before{count, plural, =0 {text}}after<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {text}}'], '', ''],
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
]);
});
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should not extract ICU messages outside of i18n sections',
() => { expect(extract('{count, plural, =0 {text}}')).toEqual([]); });
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
[['title="msg"'], 'm', 'd'],
]);
});
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 { {sex, gender, =m {m}} }}</div>'))
.toEqual([
[['{count, plural, =0 {{sex, gender, =m {m}} }}'], 'm', 'd'],
]);
});
});
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
it('should extract from attributes in translatable block', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
]);
});
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'], '', ''],
]);
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
});
it('should extract from attributes in translatable block', () => {
expect(
extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['<p><b i18n-title="m|d" title="msg"></b></p>'], '', ''],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['title="msg"'], 'm', 'd'],
[['{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'], '', ''],
]);
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
]);
});
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['title="msg"'], 'm', 'd'],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['title="bb"'], '', ''],
]);
});
});
});
describe('errors', () => {
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['title="bb"'], '', ''],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
describe('errors', () => {
describe('elements', () => {
it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
[
'Could not mark an element as translatable inside a translatable section',
'<b i18n>'
],
]);
});
});
describe('blocks', () => {
it('should report nested blocks', () => {
expect(extractErrors(`<!-- i18n --><!-- i18n --><!-- /i18n --><!-- /i18n -->`))
.toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report unclosed blocks', () => {
expect(extractErrors(`<!-- i18n -->`)).toEqual([
['Unclosed block', '<!--'],
]);
});
it('should report translatable blocks in translatable elements', () => {
expect(extractErrors(`<p i18n><!-- i18n --><!-- /i18n --></p>`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report translatable blocks in implicit elements', () => {
expect(extractErrors(`<p><!-- i18n --><!-- /i18n --></p>`, ['p'])).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<!--'],
]);
});
});
describe('implicit elements', () => {
it('should report nested implicit elements', () => {
expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable element', () => {
expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
});
it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
]);
});
});
describe('blocks', () => {
it('should report nested blocks', () => {
expect(extractErrors(`<!-- i18n --><!-- i18n --><!-- /i18n --><!-- /i18n -->`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report unclosed blocks', () => {
expect(extractErrors(`<!-- i18n -->`)).toEqual([
['Unclosed block', '<!--'],
]);
});
it('should report translatable blocks in translatable elements', () => {
expect(extractErrors(`<p i18n><!-- i18n --><!-- /i18n --></p>`)).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report translatable blocks in implicit elements', () => {
expect(extractErrors(`<p><!-- i18n --><!-- /i18n --></p>`, ['p'])).toEqual([
['Could not start a block inside a translatable section', '<!--'],
['Trying to close an unopened block', '<!--'],
]);
});
it('should report when start and end of a block are not at the same level', () => {
expect(extractErrors(`<!-- i18n --><p><!-- /i18n --></p>`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<p>'],
]);
expect(extractErrors(`<p><!-- i18n --></p><!-- /i18n -->`)).toEqual([
['I18N blocks should not cross element boundaries', '<!--'],
['Unclosed block', '<!--'],
]);
});
});
describe('implicit elements', () => {
it('should report nested implicit elements', () => {
expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable element', () => {
expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
it('should report implicit element in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'],
]);
});
});
});
});
}
function getExtractionResult(
html: string, implicitTags: string[], implicitAttrs:
{[k: string]: string[]}): ExtractionResult {
html: string, implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): ExtractionResult {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
@ -275,8 +262,8 @@ function getExtractionResult(
}
function extract(
html: string, implicitTags: string[] = [], implicitAttrs:
{[k: string]: string[]} = {}): [string[], string, string][] {
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
const messages = getExtractionResult(html, implicitTags, implicitAttrs).messages;
// clang-format off
@ -287,8 +274,7 @@ function extract(
}
function extractErrors(
html: string, implicitTags: string[] = [], implicitAttrs:
{[k: string]: string[]} = {}): any[] {
html: string, implicitTags: string[] = [], implicitAttrs: {[k: string]: string[]} = {}): any[] {
const errors = getExtractionResult(html, implicitTags, implicitAttrs).errors;
return errors.map((e): [string, string] => [e.msg, e.span.toString()]);

View File

@ -1,298 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Lexer as ExpressionLexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser as ExpressionParser} from '@angular/compiler/src/expression_parser/parser';
import {I18nHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {Message, id} from '@angular/compiler/src/i18n/message';
import {deserializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {ParseError} from '@angular/compiler/src/parse_util';
import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
import {StringMapWrapper} from '../../src/facade/collection';
import {HtmlAttrAst, HtmlElementAst, HtmlTextAst} from '../../src/html_parser/html_ast';
import {HtmlParseTreeResult, HtmlParser} from '../../src/html_parser/html_parser';
import {InterpolationConfig} from '../../src/html_parser/interpolation_config';
import {humanizeDom} from '../html_parser/html_ast_spec_utils';
export function main() {
describe('I18nHtmlParser', () => {
function parse(
template: string, messages: {[key: string]: string}, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {},
interpolation?: InterpolationConfig): HtmlParseTreeResult {
let htmlParser = new HtmlParser();
let msgs = '';
StringMapWrapper.forEach(
messages, (v: string, k: string) => msgs += `<msg id="${k}">${v}</msg>`);
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
const expParser = new ExpressionParser(new ExpressionLexer());
return new I18nHtmlParser(
htmlParser, expParser, res.content, res.messages, implicitTags, implicitAttrs)
.parse(template, 'someurl', true, interpolation);
}
it('should delegate to the provided parser when no i18n', () => {
expect(humanizeDom(parse('<div>a</div>', {}))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]
]);
});
describe('interpolation', () => {
it('should handle interpolation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null))] =
'<ph name="INTERPOLATION_1"/> or <ph name="INTERPOLATION_0"/>';
expect(humanizeDom(parse('<div value=\'{{a}} and {{b}}\' i18n-value></div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]);
});
it('should handle interpolation with config', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null))] =
'<ph name="INTERPOLATION_1"/> or <ph name="INTERPOLATION_0"/>';
expect(humanizeDom(parse(
'<div value=\'{%a%} and {%b%}\' i18n-value></div>', translations, [], {},
InterpolationConfig.fromArray(['{%', '%}']))))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{%b%} or {%a%}'],
]);
});
it('should handle interpolation with custom placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="SECOND"/>', null, null))] =
'<ph name="SECOND"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="SECOND")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="SECOND")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should handle interpolation with duplicate placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="FIRST_1"/>', null, null))] =
'<ph name="FIRST_1"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="FIRST")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="FIRST")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should support interpolation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="e0">a</ph><ph name="e2"><ph name="t3">b<ph name="INTERPOLATION_0"/></ph></ph>',
null, null))] =
'<ph name="e2"><ph name="t3"><ph name="INTERPOLATION_0"/>B</ph></ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b{{i}}</b></div>', translations))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, '{{i}}B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
});
describe('html', () => {
it('should handle nested html', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b</b></div>', translations))).toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, 'B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
it('should i18n attributes of placeholder elements', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph>', null, null))] = '<ph name="e0">A</ph>';
translations[id(new Message('b', null, null))] = 'B';
expect(humanizeDom(parse('<div i18n><a value="b" i18n-value>a</a></div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'a', 1],
[HtmlAttrAst, 'value', 'B'],
[HtmlTextAst, 'A', 2],
]);
});
it('should preserve non-i18n attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', null, null))] = 'another message';
expect(humanizeDom(parse('<div i18n value="b">message</div>', translations))).toEqual([
[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'b'],
[HtmlTextAst, 'another message', 1]
]);
});
it('should replace attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', 'meaning', null))] = 'another message';
expect(
humanizeDom(parse(
'<div value=\'some message\' i18n-value=\'meaning|comment\'></div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should replace elements with the i18n attr', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', 'meaning', null))] = 'another message';
expect(humanizeDom(parse('<div i18n=\'meaning|desc\'>message</div>', translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'another message', 1]]);
});
});
it('should extract from partitions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
translations[id(new Message('message2', 'meaning2', null))] = 'another message2';
let res = parse(
`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n --><!-- i18n: meaning2|desc2 -->message2<!-- /i18n -->`,
translations);
expect(humanizeDom(res)).toEqual([
[HtmlTextAst, 'another message1', 0],
[HtmlTextAst, 'another message2', 0],
]);
});
it('should preserve original positions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
let res =
(<any>parse('<div i18n><a>a</a><b>b</b></div>', translations).rootNodes[0]).children;
expect(res[0].sourceSpan.start.offset).toEqual(18);
expect(res[1].sourceSpan.start.offset).toEqual(10);
});
describe('errors', () => {
it('should error when giving an invalid template', () => {
expect(humanizeErrors(parse('<a>a</b>', {}).errors)).toEqual([
'Unexpected closing tag "b"'
]);
});
it('should error when no matching message (attr)', () => {
let mid = id(new Message('some message', null, null));
expect(humanizeErrors(parse('<div value=\'some message\' i18n-value></div>', {}).errors))
.toEqual([`Cannot find message for id '${mid}', content 'some message'.`]);
});
it('should error when no matching message (text)', () => {
let mid = id(new Message('some message', null, null));
expect(humanizeErrors(parse('<div i18n>some message</div>', {}).errors)).toEqual([
`Cannot find message for id '${mid}', content 'some message'.`
]);
});
it('should error when a non-placeholder element appears in translation', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = '<a>a</a>';
expect(humanizeErrors(parse('<div i18n>some message</div>', translations).errors)).toEqual([
`Unexpected tag "a". Only "ph" tags are allowed.`
]);
});
it('should error when a placeholder element does not have the name attribute', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = '<ph>a</ph>';
expect(humanizeErrors(parse('<div i18n>some message</div>', translations).errors)).toEqual([
`Missing "name" attribute.`
]);
});
it('should error when the translation refers to an invalid expression', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('hi <ph name="INTERPOLATION_0"/>', null, null))] =
'hi <ph name="INTERPOLATION_99"/>';
expect(
humanizeErrors(parse('<div value=\'hi {{a}}\' i18n-value></div>', translations).errors))
.toEqual(['Invalid interpolation name \'INTERPOLATION_99\'']);
});
});
describe('implicit translation', () => {
it('should support attributes', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', null, null))] = 'another message';
expect(humanizeDom(parse('<i18n-el value=\'some message\'></i18n-el>', translations, [], {
'i18n-el': ['value']
}))).toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should support attributes with meaning and description', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('some message', 'meaning', 'description'))] = 'another message';
expect(humanizeDom(parse(
'<i18n-el value=\'some message\' i18n-value=\'meaning|description\'></i18n-el>',
translations, [], {'i18n-el': ['value']})))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it('should support elements', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', null, null))] = 'another message';
expect(humanizeDom(parse('<i18n-el>message</i18n-el>', translations, ['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
it('should support elements with meaning and description', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', 'meaning', 'description'))] = 'another message';
expect(humanizeDom(parse(
'<i18n-el i18n=\'meaning|description\'>message</i18n-el>', translations,
['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
});
});
}
function humanizeErrors(errors: ParseError[]): string[] {
return errors.map(error => error.msg);
}

View File

@ -6,25 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/
import {serializeAst} from '@angular/compiler/src/i18n/catalog';
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {extractI18nMessages} from '@angular/compiler/src/i18n/i18n_parser';
import {ddescribe, describe, expect, it} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/html_parser/interpolation_config';
import {serializeAst} from '../../src/i18n/message_bundle';
export function main() {
ddescribe('I18nParser', () => {
describe('I18nParser', () => {
describe('elements', () => {
it('should extract from elements', () => {
expect(extract('<div i18n="m|d">text</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">text</div>')).toEqual([
[['text'], 'm', 'd'],
]);
});
it('should extract from nested elements', () => {
expect(extract('<div i18n="m|d">text<span><b>nested</b></span></div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">text<span><b>nested</b></span></div>')).toEqual([
[
[
'text',
@ -36,13 +37,13 @@ export function main() {
});
it('should not create a message for empty elements',
() => { expect(extract('<div i18n="m|d"></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div i18n="m|d"></div>')).toEqual([]); });
it('should not create a message for plain elements',
() => { expect(extract('<div></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div></div>')).toEqual([]); });
it('should suppoprt void elements', () => {
expect(extract('<div i18n="m|d"><p><br></p></div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d"><p><br></p></div>')).toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="LINE_BREAK"/></ph name="CLOSE_PARAGRAPH">'
@ -55,25 +56,27 @@ export function main() {
describe('attributes', () => {
it('should extract from attributes outside of translatable section', () => {
expect(extract('<div i18n-title="m|d" title="msg"></div>')).toEqual([
expect(_humanizeMessages('<div i18n-title="m|d" title="msg"></div>')).toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable element', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
]);
expect(_humanizeMessages('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>'))
.toEqual([
[
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
]);
});
it('should extract from attributes in translatable block', () => {
expect(extract('<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
expect(_humanizeMessages(
'<!-- i18n --><p><b i18n-title="m|d" title="msg"></b></p><!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
[
@ -87,7 +90,7 @@ export function main() {
it('should extract from attributes in translatable ICU', () => {
expect(
extract(
_humanizeMessages(
'<!-- i18n -->{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}<!-- /i18n -->'))
.toEqual([
[['msg'], 'm', 'd'],
@ -101,33 +104,35 @@ export function main() {
});
it('should extract from attributes in non translatable ICU', () => {
expect(extract('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
expect(
_humanizeMessages('{count, plural, =0 {<p><b i18n-title="m|d" title="msg"></b></p>}}'))
.toEqual([
[['msg'], 'm', 'd'],
]);
});
it('should not create a message for empty attributes',
() => { expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]); });
() => { expect(_humanizeMessages('<div i18n-title="m|d" title></div>')).toEqual([]); });
});
describe('interpolation', () => {
it('should replace interpolation with placeholder', () => {
expect(extract('<div i18n="m|d">before{{ exp }}after</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">before{{ exp }}after</div>')).toEqual([
[['[before, <ph name="INTERPOLATION"> exp </ph>, after]'], 'm', 'd'],
]);
});
it('should support named interpolation', () => {
expect(extract('<div i18n="m|d">before{{ exp //i18n(ph="teSt") }}after</div>')).toEqual([
[['[before, <ph name="TEST"> exp //i18n(ph="teSt") </ph>, after]'], 'm', 'd'],
]);
})
expect(_humanizeMessages('<div i18n="m|d">before{{ exp //i18n(ph="teSt") }}after</div>'))
.toEqual([
[['[before, <ph name="TEST"> exp //i18n(ph="teSt") </ph>, after]'], 'm', 'd'],
]);
});
});
describe('blocks', () => {
it('should extract from blocks', () => {
expect(extract(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
expect(_humanizeMessages(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`))
.toEqual([
@ -138,7 +143,7 @@ export function main() {
});
it('should extract all siblings', () => {
expect(extract(`<!-- i18n -->text<p>html<b>nested</b></p><!-- /i18n -->`)).toEqual([
expect(_humanizeMessages(`<!-- i18n -->text<p>html<b>nested</b></p><!-- /i18n -->`)).toEqual([
[
[
'text',
@ -152,33 +157,36 @@ export function main() {
describe('ICU messages', () => {
it('should extract as ICU when single child of an element', () => {
expect(extract('<div i18n="m|d">{count, plural, =0 {zero}}</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">{count, plural, =0 {zero}}</div>')).toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of an element', () => {
expect(extract('<div i18n="m|d">b{count, plural, =0 {zero}}a</div>')).toEqual([
expect(_humanizeMessages('<div i18n="m|d">b{count, plural, =0 {zero}}a</div>')).toEqual([
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
[['{count, plural, =0 {[zero]}}'], '', ''],
]);
});
it('should extract as ICU when single child of a block', () => {
expect(extract('<!-- i18n:m|d -->{count, plural, =0 {zero}}<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
expect(_humanizeMessages('<!-- i18n:m|d -->{count, plural, =0 {zero}}<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], 'm', 'd'],
]);
});
it('should extract as ICU + ph when not single child of a block', () => {
expect(extract('<!-- i18n:m|d -->b{count, plural, =0 {zero}}a<!-- /i18n -->')).toEqual([
[['{count, plural, =0 {[zero]}}'], '', ''],
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
]);
expect(_humanizeMessages('<!-- i18n:m|d -->b{count, plural, =0 {zero}}a<!-- /i18n -->'))
.toEqual([
[['{count, plural, =0 {[zero]}}'], '', ''],
[['b', '<ph icu name="ICU">{count, plural, =0 {[zero]}}</ph>', 'a'], 'm', 'd'],
]);
});
it('should not extract nested ICU messages', () => {
expect(extract('<div i18n="m|d">b{count, plural, =0 {{sex, gender, =m {m}}}}a</div>'))
expect(_humanizeMessages(
'<div i18n="m|d">b{count, plural, =0 {{sex, gender, =m {m}}}}a</div>'))
.toEqual([
[
[
@ -194,7 +202,7 @@ export function main() {
describe('implicit elements', () => {
it('should extract from implicit elements', () => {
expect(extract('<b>bold</b><i>italic</i>', ['b'])).toEqual([
expect(_humanizeMessages('<b>bold</b><i>italic</i>', ['b'])).toEqual([
[['bold'], '', ''],
]);
});
@ -202,7 +210,8 @@ export function main() {
describe('implicit attributes', () => {
it('should extract implicit attributes', () => {
expect(extract('<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
expect(_humanizeMessages(
'<b title="bb">bold</b><i title="ii">italic</i>', [], {'b': ['title']}))
.toEqual([
[['bb'], '', ''],
]);
@ -211,7 +220,8 @@ export function main() {
describe('placeholders', () => {
it('should reuse the same placeholder name for tags', () => {
expect(extract('<div i18n="m|d"><p>one</p><p>two</p><p other>three</p></div>')).toEqual([
const html = '<div i18n="m|d"><p>one</p><p>two</p><p other>three</p></div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph tag name="START_PARAGRAPH">one</ph name="CLOSE_PARAGRAPH">',
@ -221,10 +231,16 @@ export function main() {
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'START_PARAGRAPH=<p>, CLOSE_PARAGRAPH=</p>, START_PARAGRAPH_1=<p other>',
]);
});
it('should reuse the same placeholder name for interpolations', () => {
expect(extract('<div i18n="m|d">{{ a }}{{ a }}{{ b }}</div>')).toEqual([
const html = '<div i18n="m|d">{{ a }}{{ a }}{{ b }}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'[<ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION"> a </ph>, <ph name="INTERPOLATION_1"> b </ph>]'
@ -232,46 +248,70 @@ export function main() {
'm', 'd'
],
]);
expect(_humanizePlaceholders(html)).toEqual([
'INTERPOLATION={{ a }}, INTERPOLATION_1={{ b }}',
]);
});
it('should reuse the same placeholder name for icu messages', () => {
expect(
extract(
'<div i18n="m|d">{count, plural, =0 {0}}{count, plural, =0 {0}}{count, plural, =1 {1}}</div>'))
.toEqual([
[
[
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU_1">{count, plural, =1 {[1]}}</ph>',
],
'm', 'd'
],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =1 {[1]}}'], '', ''],
]);
});
const html =
'<div i18n="m|d">{count, plural, =0 {0}}{count, plural, =0 {0}}{count, plural, =1 {1}}</div>';
expect(_humanizeMessages(html)).toEqual([
[
[
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU">{count, plural, =0 {[0]}}</ph>',
'<ph icu name="ICU_1">{count, plural, =1 {[1]}}</ph>',
],
'm', 'd'
],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =0 {[0]}}'], '', ''],
[['{count, plural, =1 {[1]}}'], '', ''],
]);
expect(_humanizePlaceholders(html)).toEqual([
'ICU={count, plural, =0 {0}}, ICU_1={count, plural, =1 {1}}',
'',
'',
'',
]);
});
});
});
}
function extract(
function _humanizeMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): [string[], string, string][] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on
}
function _humanizePlaceholders(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.getOwnPropertyNames(msg.placeholders).map((name) => `${name}=${msg.placeholders[name]}`).join(', '));
// clang-format on
}
function _extractMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): Message[] {
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(html, 'extractor spec', true);
if (parseResult.errors.length > 1) {
throw Error(`unexpected parse errors: ${parseResult.errors.join('\n')}`);
}
const messages = extractI18nMessages(
return extractI18nMessages(
parseResult.rootNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
// clang-format off
// https://github.com/angular/clang-format/issues/35
return messages.map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on
}

View File

@ -6,49 +6,40 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Catalog, strHash} from '@angular/compiler/src/i18n/catalog';
import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/html_parser/interpolation_config';
import Serializable = webdriver.Serializable;
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {serializeAst} from '@angular/compiler/src/i18n/catalog';
import * as i18nAst from '@angular/compiler/src/i18n/i18n_ast';
import {MessageBundle, serializeAst, strHash} from '../../src/i18n/message_bundle';
export function main(): void {
ddescribe('Catalog', () => {
describe('MessageBundle', () => {
describe('Messages', () => {
let messages: MessageBundle;
describe('write', () => {
let catalog: Catalog;
beforeEach(() => { catalog = new Catalog(new HtmlParser, [], {}); });
beforeEach(() => { messages = new MessageBundle(new HtmlParser, [], {}); });
it('should extract the message to the catalog', () => {
catalog.updateFromTemplate(
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeCatalog(catalog)).toEqual([
expect(humanizeMessages(messages)).toEqual([
'a486901=Translate Me',
]);
});
it('should extract the same message with different meaning in different entries', () => {
catalog.updateFromTemplate(
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p>', 'url',
DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeCatalog(catalog)).toEqual([
expect(humanizeMessages(messages)).toEqual([
'a486901=Translate Me',
'8475f2cc=Translate Me',
]);
});
});
describe(
'load', () => {
// TODO
});
describe('strHash', () => {
it('should return a hash value', () => {
// https://github.com/google/closure-library/blob/1fb19a857b96b74e6523f3e9d33080baf25be046/closure/goog/string/string_test.js#L1115
@ -66,16 +57,16 @@ export function main(): void {
}
class _TestSerializer implements Serializer {
write(messageMap: {[k: string]: i18nAst.Message}): string {
write(messageMap: {[id: string]: i18n.Message}): string {
return Object.keys(messageMap)
.map(id => `${id}=${serializeAst(messageMap[id].nodes)}`)
.join('//');
}
load(content: string): {[k: string]: i18nAst.Node[]} { return null; }
load(content: string, url: string, placeholders: {}): {} { return null; }
}
function humanizeCatalog(catalog: Catalog): string[] {
function humanizeMessages(catalog: MessageBundle): string[] {
return catalog.write(new _TestSerializer()).split('//');
}

View File

@ -1,275 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Lexer as ExpressionLexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser as ExpressionParser} from '@angular/compiler/src/expression_parser/parser';
import {Message} from '@angular/compiler/src/i18n/message';
import {MessageExtractor, removeDuplicates} from '@angular/compiler/src/i18n/message_extractor';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/html_parser/html_parser';
export function main() {
describe('MessageExtractor', () => {
let extractor: MessageExtractor;
beforeEach(() => {
const expParser = new ExpressionParser(new ExpressionLexer());
const htmlParser = new HtmlParser();
extractor = new MessageExtractor(htmlParser, expParser, ['i18n-tag'], {'i18n-el': ['trans']});
});
it('should extract from partitions', () => {
let res = extractor.extract(
`
<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2 -->message2<!-- /i18n -->
<!-- i18n -->message3<!-- /i18n -->`,
'someUrl');
expect(res.messages).toEqual([
new Message('message1', 'meaning1', 'desc1'),
new Message('message2', 'meaning2'),
new Message('message3', null),
]);
});
it('should ignore other comments', () => {
let res = extractor.extract(
`
<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`,
'someUrl');
expect(res.messages).toEqual([new Message('message1', 'meaning1', 'desc1')]);
});
describe('ICU messages', () => {
it('should replace icu messages with placeholders', () => {
let res = extractor.extract('<div i18n>{count, plural, =0 {text} }</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {text}}</ph>', null, null)]);
});
it('should replace HTML with placeholders in ICU cases', () => {
let res =
extractor.extract('<div i18n>{count, plural, =0 {<p>html</p>} }</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {<ph name="e1">html</ph>}}</ph>', null, null)]);
});
it('should replace interpolation with placeholders in ICU cases', () => {
let res =
extractor.extract('<div i18n>{count, plural, =0 {{{interpolation}}}}</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {<ph name="t1"><ph name="INTERPOLATION_0"/></ph>}}</ph>',
null, null)]);
});
it('should not replace nested interpolation with placeholders in ICU cases', () => {
let res = extractor.extract(
'<div i18n>{count, plural, =0 {{sex, gender, =m {{{he}}} =f {<b>she</b>}}}}</div>',
'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="x0">{count, plural =0 {{sex, gender =m {<ph name="t2"><ph name="INTERPOLATION_0"/></ph>} =f {<ph name="e3">she</ph>}}}}</ph>',
null, null)]);
});
});
describe('interpolation', () => {
it('should replace interpolation with placeholders (text nodes)', () => {
let res = extractor.extract('<div i18n>Hi {{one}} and {{two}}</div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="t0">Hi <ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/></ph>',
null, null)]);
});
it('should replace interpolation with placeholders (attributes)', () => {
let res =
extractor.extract('<div title=\'Hi {{one}} and {{two}}\' i18n-title></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="INTERPOLATION_0"/> and <ph name="INTERPOLATION_1"/>', null, null)]);
});
it('should replace interpolation with named placeholders if provided (text nodes)', () => {
let res = extractor.extract(
`
<div i18n>Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}</div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="t0">Hi <ph name="FIRST"/> and <ph name="SECOND"/></ph>', null, null)]);
});
it('should replace interpolation with named placeholders if provided (attributes)', () => {
let res = extractor.extract(
`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
});
describe('placehoders', () => {
it('should match named placeholders with extra spacing', () => {
let res = extractor.extract(
`
<div title='Hi {{one // i18n ( ph = "FIRST" )}} and {{two // i18n ( ph = "SECOND" )}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should suffix duplicate placeholder names with numbers', () => {
let res = extractor.extract(
`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="FIRST")}} and {{three //i18n(ph="FIRST")}}'
i18n-title></div>`,
'someurl');
expect(res.messages).toEqual([new Message(
'Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>', null,
null)]);
});
});
describe('html', () => {
it('should extract from elements with the i18n attr', () => {
let res = extractor.extract('<div i18n=\'meaning|desc\'>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
});
it('should extract from elements with the i18n attr without a desc', () => {
let res = extractor.extract('<div i18n=\'meaning\'>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', null)]);
});
it('should extract from elements with the i18n attr without a meaning', () => {
let res = extractor.extract('<div i18n>message</div>', 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract from attributes', () => {
let res = extractor.extract(
`
<div
title1='message1' i18n-title1='meaning1|desc1'
title2='message2' i18n-title2='meaning2|desc2'>
</div>
`,
'someurl');
expect(res.messages).toEqual([
new Message('message1', 'meaning1', 'desc1'), new Message('message2', 'meaning2', 'desc2')
]);
});
it('should handle html content', () => {
let res = extractor.extract(
'<div i18n><div attr="value">zero<div>one</div></div><div>two</div></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="e0">zero<ph name="e2">one</ph></ph><ph name="e4">two</ph>', null, null)]);
});
it('should handle html content with interpolation', () => {
let res =
extractor.extract('<div i18n><div>zero{{a}}<div>{{b}}</div></div></div>', 'someurl');
expect(res.messages).toEqual([new Message(
'<ph name="e0"><ph name="t1">zero<ph name="INTERPOLATION_0"/></ph><ph name="e2"><ph name="t3"><ph name="INTERPOLATION_0"/></ph></ph></ph>',
null, null)]);
});
it('should extract from nested elements', () => {
let res = extractor.extract(
'<div title="message1" i18n-title="meaning1|desc1"><div i18n="meaning2|desc2">message2</div></div>',
'someurl');
expect(res.messages).toEqual([
new Message('message2', 'meaning2', 'desc2'), new Message('message1', 'meaning1', 'desc1')
]);
});
it('should extract messages from attributes in i18n blocks', () => {
let res = extractor.extract(
'<div i18n><div attr="value" i18n-attr="meaning|desc">message</div></div>', 'someurl');
expect(res.messages).toEqual([
new Message('<ph name="e0">message</ph>', null, null),
new Message('value', 'meaning', 'desc')
]);
});
});
it('should remove duplicate messages', () => {
let res = extractor.extract(
`
<!-- i18n: meaning|desc1 -->message<!-- /i18n -->
<!-- i18n: meaning|desc2 -->message<!-- /i18n -->`,
'someUrl');
expect(removeDuplicates(res.messages)).toEqual([
new Message('message', 'meaning', 'desc1'),
]);
});
describe('implicit translation', () => {
it('should extract from elements', () => {
let res = extractor.extract('<i18n-tag>message</i18n-tag>', 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract meaning and description from elements when present', () => {
let res = extractor.extract(
'<i18n-tag i18n=\'meaning|description\'>message</i18n-tag>', 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'description')]);
});
it('should extract from attributes', () => {
let res = extractor.extract(`<i18n-el trans='message'></i18n-el>`, 'someurl');
expect(res.messages).toEqual([new Message('message', null, null)]);
});
it('should extract meaning and description from attributes when present', () => {
let res = extractor.extract(
`<i18n-el trans='message' i18n-trans="meaning|desc"></i18n-el>`, 'someurl');
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
});
});
describe('errors', () => {
it('should error on i18n attributes without matching "real" attributes', () => {
let res = extractor.extract(
`
<div
title1='message1' i18n-title1='meaning1|desc1' i18n-title2='meaning2|desc2'>
</div>`,
'someurl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing attribute \'title2\'.');
});
it('should error when i18n comments are unbalanced', () => {
const res = extractor.extract('<!-- i18n -->message1', 'someUrl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing closing \'i18n\' comment.');
});
it('should error when i18n comments are unbalanced', () => {
const res = extractor.extract('<!-- i18n -->', 'someUrl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Missing closing \'i18n\' comment.');
});
it('should return parse errors when the template is invalid', () => {
let res = extractor.extract('<input&#Besfs', 'someurl');
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Unexpected character "s"');
});
});
});
}

View File

@ -1,23 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Message, id} from '@angular/compiler/src/i18n/message';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
export function main() {
describe('Message', () => {
describe('id', () => {
it('should return a different id for messages with and without the meaning', () => {
let m1 = new Message('content', 'meaning', null);
let m2 = new Message('content', null, null);
expect(id(m1)).toEqual(id(m1));
expect(id(m1)).not.toEqual(id(m2));
});
});
});
}

View File

@ -6,14 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/util';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void {
ddescribe('PlaceholderRegistry', () => {
describe('PlaceholderRegistry', () => {
let reg: PlaceholderRegistry;
beforeEach(() => { reg = new PlaceholderRegistry(); });
@ -34,11 +32,11 @@ export function main(): void {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
});
it('should be case insensitive for tag name', () => {
it('should be case sensitive for tag name', () => {
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH');
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH_1');
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH');
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH_1');
});
it('should generate the same name for the same tag with the same attributes', () => {

View File

@ -6,16 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Catalog} from '@angular/compiler/src/i18n/catalog';
import {XmbSerializer} from '@angular/compiler/src/i18n/serializers/xmb';
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/html_parser/interpolation_config';
import {MessageBundle} from '../../../src/i18n/message_bundle';
export function main(): void {
ddescribe('XMB serializer', () => {
describe('XMB serializer', () => {
const HTML = `
<p>not translatable</p>
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
@ -35,35 +34,18 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new XmbSerializer();
serializer.load(XMB);
const serializer = new Xmb();
serializer.load(XMB, 'url', {});
}).toThrow();
});
});
}
function toXmb(html: string): string {
let catalog = new Catalog(new HtmlParser, [], {});
const serializer = new XmbSerializer();
let catalog = new MessageBundle(new HtmlParser, [], {});
const serializer = new Xmb();
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
return catalog.write(serializer);
}
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
// element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph
// name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></message><message
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</message><message id="b45e58a5" description="d"
// meaning="m">foo</message><message id="18ea85bc">{ count, plural, =0 {{ sex, gender, other {<ph
// name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</message></messagebundle>
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
// element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph
// name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></message><message
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph
// name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</message><message id="18ea85bc">{ count,
// plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply
// nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</message><message id="b45e58a5"
// description="d" meaning="m">foo</message></messagebundle>
}

View File

@ -11,7 +11,7 @@ import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit
import * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void {
ddescribe('XML helper', () => {
describe('XML helper', () => {
it('should serialize XML declaration', () => {
expect(xml.serialize([new xml.Declaration({version: '1.0'})]))
.toEqual('<? xml version="1.0" ?>');

View File

@ -0,0 +1,171 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Xtb} from '@angular/compiler/src/i18n/serializers/xtb';
import {escapeRegExp} from '@angular/core/src/facade/lang';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../../src/html_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/html_parser/interpolation_config';
import {serializeAst} from '../../html_parser/ast_serializer_spec';
export function main(): void {
describe('XTB serializer', () => {
let serializer: Xtb;
function loadAsText(content: string, placeholders: {[id: string]: {[name: string]: string}}):
{[id: string]: string} {
const asAst = serializer.load(content, 'url', placeholders);
let asText: {[id: string]: string} = {};
Object.getOwnPropertyNames(asAst).forEach(
id => { asText[id] = serializeAst(asAst[id]).join(''); });
return asText;
}
beforeEach(() => { serializer = new Xtb(new HtmlParser(), DEFAULT_INTERPOLATION_CONFIG); });
describe('load', () => {
it('should load XTB files without placeholders', () => {
const XTB = `
<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="foo">bar</translation>
</translationbundle>`;
expect(loadAsText(XTB, {})).toEqual({foo: 'bar'});
});
it('should load XTB files with placeholders', () => {
const XTB = `
<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="foo">bar<ph name="PLACEHOLDER"/><ph name="PLACEHOLDER"/></translation>
</translationbundle>`;
expect(loadAsText(XTB, {foo: {PLACEHOLDER: '!'}})).toEqual({foo: 'bar!!'});
});
it('should load complex XTB files', () => {
const XTB = `
<? xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="a">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></translation>
<translation id="b">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}}</translation>
<translation id="c" desc="d" meaning="m">foo</translation>
<translation id="d">{ count, plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>}} }}</translation>
</translationbundle>`;
const PLACEHOLDERS = {
a: {
START_BOLD_TEXT: '<b>',
CLOSE_BOLD_TEXT: '</b>',
INTERPOLATION: '{{ a + b }}',
},
b: {
START_PARAGRAPH: '<p translated=true>',
CLOSE_PARAGRAPH: '</p>',
},
d: {
START_PARAGRAPH: '<p>',
CLOSE_PARAGRAPH: '</p>',
},
};
expect(loadAsText(XTB, PLACEHOLDERS)).toEqual({
a: 'translatable element <b>with placeholders</b> {{ a + b }}',
b: '{ count, plural, =0 {<p translated="true">test</p>}}',
c: 'foo',
d: '{ count, plural, =0 {{ sex, gender, other {<p>deeply nested</p>}} }}',
});
});
});
describe('errors', () => {
it('should throw on nested <translationbundle>', () => {
const XTB =
'<translationbundle><translationbundle></translationbundle></translationbundle>';
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translationbundle> elements can not be nested/);
});
it('should throw on nested <translation>', () => {
const XTB = `
<translationbundle>
<translation id="outer">
<translation id="inner">
</translation>
</translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translation> elements can not be nested/);
});
it('should throw when a <translation> has no id attribute', () => {
const XTB = `
<translationbundle>
<translation></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<translation> misses the "id" attribute/);
});
it('should throw when a placeholder has no name attribute', () => {
const XTB = `
<translationbundle>
<translation id="fail"><ph /></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/<ph> misses the "name" attribute/);
});
it('should throw when a placeholder is not present in the source message', () => {
const XTB = `
<translationbundle>
<translation id="fail"><ph name="UNKNOWN"/></translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
});
});
it('should throw when the translation results in invalid html', () => {
const XTB = `
<translationbundle>
<translation id="fail">foo<ph name="CLOSE_P"/>bar</translation>
</translationbundle>`;
expect(() => {
serializer.load(XTB, 'url', {fail: {CLOSE_P: '</p>'}});
}).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
});
it('should throw on unknown tags', () => {
const XTB = `<what></what>`;
expect(() => {
serializer.load(XTB, 'url', {});
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw when trying to save an xmb file',
() => { expect(() => { serializer.write({}); }).toThrow(); });
});
}

View File

@ -1,121 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Message, id} from '@angular/compiler/src/i18n/message';
import {deserializeXmb, serializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {ParseError, ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlAst} from '../../src/html_parser/html_ast';
export function main() {
describe('Xmb', () => {
describe('Xmb Serialization', () => {
it('should return an empty message bundle for an empty list of messages',
() => { expect(serializeXmb([])).toEqual('<message-bundle></message-bundle>'); });
it('should serialize messages without desc nor meaning', () => {
let m = new Message('content', null, null);
let expected = `<message-bundle><msg id='${id(m)}'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
it('should serialize messages with desc and meaning', () => {
let m = new Message('content', 'meaning', 'description');
let expected =
`<message-bundle><msg id='${id(m)}' desc='description' meaning='meaning'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
it('should escape the desc and meaning', () => {
let m = new Message('content', '"\'&<>"\'&<>', '"\'&<>"\'&<>');
let expected =
`<message-bundle><msg id='${id(m)}' desc='&quot;&apos;&amp;&lt;&gt;&quot;&apos;&amp;&lt;&gt;' meaning='&quot;&apos;&amp;&lt;&gt;&quot;&apos;&amp;&lt;&gt;'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
});
describe('Xmb Deserialization', () => {
it('should parse an empty bundle', () => {
let mb = '<message-bundle></message-bundle>';
expect(deserializeXmb(mb, 'url').messages).toEqual({});
});
it('should parse an non-empty bundle', () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content1</msg>
<msg id="id2">content2</msg>
</message-bundle>
`;
let parsed = deserializeXmb(mb, 'url').messages;
expect(_serialize(parsed['id1'])).toEqual('content1');
expect(_serialize(parsed['id2'])).toEqual('content2');
});
it('should error when cannot parse the content', () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Unexpected closing tag "message-bundle"']);
});
it('should error when cannot find the id attribute', () => {
let mb = `
<message-bundle>
<msg>content</msg>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['"id" attribute is missing']);
});
it('should error on empty content', () => {
let mb = ``;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Missing element "message-bundle"']);
});
it('should error on an invalid element', () => {
let mb = `
<message-bundle>
<invalid>content</invalid>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url');
expect(_serializeErrors(res.errors)).toEqual(['Unexpected element "invalid"']);
});
it('should expand \'ph\' elements', () => {
let mb = `
<message-bundle>
<msg id="id1">a<ph name="i0"/></msg>
</message-bundle>
`;
let res = deserializeXmb(mb, 'url').messages['id1'];
expect((<any>res[1]).name).toEqual('ph');
});
});
});
}
function _serialize(nodes: HtmlAst[]): string {
return (<any>nodes[0]).value;
}
function _serializeErrors(errors: ParseError[]): string[] {
return errors.map(e => e.msg);
}

View File

@ -11,7 +11,7 @@ import {CUSTOM_ELEMENTS_SCHEMA, SecurityContext} from '@angular/core';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/browser_util';
import {HtmlElementAst} from '../../src/html_parser/html_ast';
import {Element} from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {extractSchema} from './schema_extractor';
@ -78,7 +78,7 @@ export function main() {
it('should detect properties on namespaced elements', () => {
const htmlAst = new HtmlParser().parse('<svg:style>', 'TestComp');
const nodeName = (<HtmlElementAst>htmlAst.rootNodes[0]).name;
const nodeName = (<Element>htmlAst.rootNodes[0]).name;
expect(registry.hasProperty(nodeName, 'type', [])).toBeTruthy();
});

View File

@ -16,7 +16,6 @@ import {SchemaMetadata, SecurityContext} from '@angular/core';
import {Console} from '@angular/core/src/console';
import {TestBed} from '@angular/core/testing';
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal';
import {Identifiers, identifierToken} from '../../src/identifiers';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/html_parser/interpolation_config';

View File

@ -7,7 +7,7 @@
*/
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '../../../core/testing/testing_internal';
import {HtmlElementAst} from '../../src/html_parser/html_ast';
import {Element} from '../../src/html_parser/ast';
import {HtmlParser} from '../../src/html_parser/html_parser';
import {PreparsedElement, PreparsedElementType, preparseElement} from '../../src/template_parser/template_preparser';
@ -17,7 +17,7 @@ export function main() {
beforeEach(inject([HtmlParser], (_htmlParser: HtmlParser) => { htmlParser = _htmlParser; }));
function preparse(html: string): PreparsedElement {
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as HtmlElementAst);
return preparseElement(htmlParser.parse(html, 'TestComp').rootNodes[0] as Element);
}
it('should detect script elements', inject([HtmlParser], (htmlParser: HtmlParser) => {