feat(i18n): xtb serializer
This commit is contained in:
@ -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));
|
||||
}
|
85
modules/@angular/compiler/test/html_parser/ast_spec_utils.ts
Normal file
85
modules/@angular/compiler/test/html_parser/ast_spec_utils.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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="AA">')).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="&" b="c&&d">')).toEqual([
|
||||
[HtmlTokenType.TAG_OPEN_START, null, 't'],
|
||||
[HtmlTokenType.ATTR_NAME, null, 'a'],
|
||||
[HtmlTokenType.ATTR_VALUE, '&'],
|
||||
[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&b')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'a&b'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'AA'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'A'],
|
||||
[HtmlTokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'a&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('
sdf;')).toEqual([
|
||||
[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).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&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>&</SCRIPT>`)).toEqual([
|
||||
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
|
||||
[HtmlTokenType.TAG_OPEN_END],
|
||||
[HtmlTokenType.RAW_TEXT, '&'],
|
||||
[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>&</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)]);
|
||||
}
|
@ -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)];
|
||||
}
|
||||
|
@ -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],
|
||||
]);
|
||||
});
|
||||
|
803
modules/@angular/compiler/test/html_parser/lexer_spec.ts
Normal file
803
modules/@angular/compiler/test/html_parser/lexer_spec.ts
Normal 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="AA">')).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="&" b="c&&d">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, '&'],
|
||||
[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&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[lex.TokenType.TEXT, 'AA'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[lex.TokenType.TEXT, 'A'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&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('
sdf;')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).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&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>&</SCRIPT>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, '&'],
|
||||
[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>&</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)]);
|
||||
}
|
@ -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()]);
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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('//');
|
||||
}
|
||||
|
@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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', () => {
|
@ -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><b></ex></ph>with placeholders<ph
|
||||
// name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></message><message
|
||||
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}} }}</message></messagebundle>
|
||||
// <? xml version="1.0" encoding="UTF-8" ?><messagebundle><message id="834fa53b">translatable
|
||||
// element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph
|
||||
// name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></message><message
|
||||
// id="7a2843db">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph
|
||||
// name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}}</message><message id="18ea85bc">{ count,
|
||||
// plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply
|
||||
// nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>}} }}</message><message id="b45e58a5"
|
||||
// description="d" meaning="m">foo</message></messagebundle>
|
||||
}
|
@ -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" ?>');
|
||||
|
171
modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
Normal file
171
modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
Normal 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><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></translation>
|
||||
<translation id="b">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></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(); });
|
||||
});
|
||||
}
|
@ -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='"'&<>"'&<>' meaning='"'&<>"'&<>'>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);
|
||||
}
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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) => {
|
||||
|
Reference in New Issue
Block a user