refactor(i18n): misc updates

This commit is contained in:
Victor Berchet 2016-08-01 14:43:20 -07:00
parent df44e3e425
commit e811a5d97f
10 changed files with 115 additions and 59 deletions

View File

@ -11,9 +11,10 @@ import {I18nError} from './parse_util';
const _I18N_ATTR = 'i18n'; const _I18N_ATTR = 'i18n';
const _I18N_ATTR_PREFIX = 'i18n-'; const _I18N_ATTR_PREFIX = 'i18n-';
const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/;
/** /**
* Extract translatable message from an html AST as a list of html AST nodes * Extract translatable messages from an html AST as a list of html AST nodes
*/ */
export function extractAstMessages( export function extractAstMessages(
sourceAst: html.Node[], implicitTags: string[], sourceAst: html.Node[], implicitTags: string[],
@ -40,19 +41,14 @@ class _ExtractVisitor implements html.Visitor {
// {<icu message>} // {<icu message>}
private _inIcu = false; private _inIcu = false;
private _sectionStartIndex: number; private _msgCountAtSectionStart: number;
private _errors: I18nError[]; private _errors: I18nError[];
constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {} constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
extract(nodes: html.Node[]): ExtractionResult { extract(nodes: html.Node[]): ExtractionResult {
this._init();
const messages: Message[] = []; const messages: Message[] = [];
this._inI18nBlock = false;
this._inI18nNode = false;
this._depth = 0;
this._inIcu = false;
this._sectionStartIndex = void 0;
this._errors = [];
nodes.forEach(node => node.visit(this, messages)); nodes.forEach(node => node.visit(this, messages));
@ -105,13 +101,13 @@ class _ExtractVisitor implements html.Visitor {
this._inI18nBlock = true; this._inI18nBlock = true;
this._blockStartDepth = this._depth; this._blockStartDepth = this._depth;
this._blockChildren = []; this._blockChildren = [];
this._blockMeaningAndDesc = comment.value.replace(/^i18n:?/, '').trim(); this._blockMeaningAndDesc = comment.value.replace(_I18N_COMMENT_PREFIX_REGEXP, '').trim();
this._startSection(messages); this._openTranslatableSection(comment, messages);
} }
} else { } else {
if (isClosing) { if (isClosing) {
if (this._depth == this._blockStartDepth) { if (this._depth == this._blockStartDepth) {
this._endSection(messages, this._blockChildren); this._closeTranslatableSection(comment, messages, this._blockChildren);
this._inI18nBlock = false; this._inI18nBlock = false;
this._addMessage(messages, this._blockChildren, this._blockMeaningAndDesc); this._addMessage(messages, this._blockChildren, this._blockMeaningAndDesc);
} else { } else {
@ -129,18 +125,15 @@ class _ExtractVisitor implements html.Visitor {
this._mayBeAddBlockChildren(el); this._mayBeAddBlockChildren(el);
this._depth++; this._depth++;
const wasInI18nNode = this._inI18nNode; const wasInI18nNode = this._inI18nNode;
let useSection = false;
// Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU // Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU
// message // message
const i18nAttr = _getI18nAttr(el); const i18nAttr = _getI18nAttr(el);
const isImplicitI18n = const isImplicitI18n = this._implicitTags.some((tag: string): boolean => el.name === tag);
this._implicitTags.some((tagName: string): boolean => el.name === tagName);
if (!(this._inI18nNode || this._inIcu || this._inI18nBlock)) { if (!(this._inI18nNode || this._inIcu || this._inI18nBlock)) {
if (i18nAttr) { if (i18nAttr) {
this._inI18nNode = true; this._inI18nNode = true;
this._addMessage(messages, el.children, i18nAttr.value); this._addMessage(messages, el.children, i18nAttr.value);
useSection = true;
} else if (isImplicitI18n) { } else if (isImplicitI18n) {
this._inI18nNode = true; this._inI18nNode = true;
this._addMessage(messages, el.children); this._addMessage(messages, el.children);
@ -155,10 +148,11 @@ class _ExtractVisitor implements html.Visitor {
this._extractFromAttributes(el, messages); this._extractFromAttributes(el, messages);
if (useSection) { if (i18nAttr || isImplicitI18n) {
this._startSection(messages); // Start a section when the content is translatable
this._openTranslatableSection(el, messages);
html.visitAll(this, el.children, messages); html.visitAll(this, el.children, messages);
this._endSection(messages, el.children); this._closeTranslatableSection(el, messages, el.children);
} else { } else {
html.visitAll(this, el.children, messages); html.visitAll(this, el.children, messages);
} }
@ -171,6 +165,15 @@ class _ExtractVisitor implements html.Visitor {
throw new Error('unreachable code'); throw new Error('unreachable code');
} }
private _init(): void {
this._inI18nBlock = false;
this._inI18nNode = false;
this._depth = 0;
this._inIcu = false;
this._msgCountAtSectionStart = void 0;
this._errors = [];
}
private _extractFromAttributes(el: html.Element, messages: Message[]): void { private _extractFromAttributes(el: html.Element, messages: Message[]): void {
const explicitAttrNameToValue: {[k: string]: string} = {}; const explicitAttrNameToValue: {[k: string]: string} = {};
const implicitAttrNames: string[] = this._implicitAttrs[el.name] || []; const implicitAttrNames: string[] = this._implicitAttrs[el.name] || [];
@ -214,20 +217,19 @@ class _ExtractVisitor implements html.Visitor {
/** /**
* Marks the start of a section, see `_endSection` * Marks the start of a section, see `_endSection`
*/ */
private _startSection(messages: Message[]): void { private _openTranslatableSection(node: html.Node, messages: Message[]): void {
if (this._sectionStartIndex !== void 0) { if (this._msgCountAtSectionStart !== void 0) {
throw new Error('Unexpected section start'); this._reportError(node, 'Unexpected section start');
} else {
this._msgCountAtSectionStart = messages.length;
} }
this._sectionStartIndex = messages.length;
} }
/** /**
* Terminates a section. * Terminates a section.
* *
* If a section has only one significant children (comments not significant) then we should not * If a section has only one significant children (comments not significant) then we should not
* keep the message * keep the message from this children:
* from this children:
* *
* `<p i18n="meaning|description">{ICU message}</p>` would produce two messages: * `<p i18n="meaning|description">{ICU message}</p>` would produce two messages:
* - one for the <p> content with meaning and description, * - one for the <p> content with meaning and description,
@ -239,12 +241,14 @@ class _ExtractVisitor implements html.Visitor {
* Note that we should still keep messages extracted from attributes inside the section (ie in the * Note that we should still keep messages extracted from attributes inside the section (ie in the
* ICU message here) * ICU message here)
*/ */
private _endSection(messages: Message[], directChildren: html.Node[]): void { private _closeTranslatableSection(
if (this._sectionStartIndex === void 0) { node: html.Node, messages: Message[], directChildren: html.Node[]): void {
throw new Error('Unexpected section end'); if (this._msgCountAtSectionStart === void 0) {
this._reportError(node, 'Unexpected section end');
return;
} }
const startIndex = this._sectionStartIndex; const startIndex = this._msgCountAtSectionStart;
const significantChildren: number = directChildren.reduce( const significantChildren: number = directChildren.reduce(
(count: number, node: html.Node): number => count + (node instanceof html.Comment ? 0 : 1), (count: number, node: html.Node): number => count + (node instanceof html.Comment ? 0 : 1),
0); 0);
@ -259,7 +263,7 @@ class _ExtractVisitor implements html.Visitor {
} }
} }
this._sectionStartIndex = void 0; this._msgCountAtSectionStart = void 0;
} }
private _reportError(node: html.Node, msg: string): void { private _reportError(node: html.Node, msg: string): void {

View File

@ -36,7 +36,7 @@ export class Icu implements Node {
visit(visitor: Visitor, context?: any): any { return visitor.visitIcu(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitIcu(this, context); }
} }
export class TagPlaceholder { export class TagPlaceholder implements Node {
constructor( constructor(
public tag: string, public attrs: {[k: string]: string}, public startName: string, public tag: string, public attrs: {[k: string]: string}, public startName: string,
public closeName: string, public children: Node[], public isVoid: boolean, public closeName: string, public children: Node[], public isVoid: boolean,
@ -45,13 +45,13 @@ export class TagPlaceholder {
visit(visitor: Visitor, context?: any): any { return visitor.visitTagPlaceholder(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitTagPlaceholder(this, context); }
} }
export class Placeholder { export class Placeholder implements Node {
constructor(public value: string, public name: string = '', public sourceSpan: ParseSourceSpan) {} constructor(public value: string, public name: string = '', public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); }
} }
export class IcuPlaceholder { export class IcuPlaceholder implements Node {
constructor(public value: Icu, public name: string = '', public sourceSpan: ParseSourceSpan) {} constructor(public value: Icu, public name: string = '', public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }

View File

@ -13,10 +13,12 @@ import {getHtmlTagDefinition} from '../ml_parser/html_tags';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util'; import {ParseSourceSpan} from '../parse_util';
import {extractAstMessages} from './extractor'; import {Message as HtmlMessage, extractAstMessages} from './extractor';
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder'; import {PlaceholderRegistry} from './serializers/placeholder';
const _expParser = new ExpressionParser(new ExpressionLexer());
/** /**
* Extract all the i18n messages from a component template. * Extract all the i18n messages from a component template.
*/ */
@ -29,11 +31,19 @@ export function extractI18nMessages(
return []; return [];
} }
const expParser = new ExpressionParser(new ExpressionLexer()); const htmlToI18n = getHtmlToI18nConverter(interpolationConfig);
const visitor = new _I18nVisitor(expParser, interpolationConfig);
return extractionResult.messages.map( return extractionResult.messages.map(htmlToI18n);
(msg) => visitor.toI18nMessage(msg.nodes, msg.meaning, msg.description)); }
/**
* Returns a function converting html Messages to i18n Messages given an interpolationConfig
*/
export function getHtmlToI18nConverter(interpolationConfig: InterpolationConfig):
(msg: HtmlMessage) => i18n.Message {
const visitor = new _I18nVisitor(_expParser, interpolationConfig);
return (msg: HtmlMessage) => visitor.toI18nMessage(msg.nodes, msg.meaning, msg.description);
} }
class _I18nVisitor implements html.Visitor { class _I18nVisitor implements html.Visitor {

View File

@ -37,14 +37,17 @@ export class MessageBundle {
htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs); htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs);
messages.forEach((message) => { messages.forEach((message) => {
const id = strHash(serializeAst(message.nodes).join('') + `[${message.meaning}]`); this._messageMap[messageDigest(message.nodes, message.meaning)] = message;
this._messageMap[id] = message;
}); });
} }
write(serializer: Serializer): string { return serializer.write(this._messageMap); } write(serializer: Serializer): string { return serializer.write(this._messageMap); }
} }
export function messageDigest(nodes: i18n.Node[], meaning: string): string {
return strHash(serializeNodes(nodes).join('') + `[${meaning}]`);
}
/** /**
* String hash function similar to java.lang.String.hashCode(). * String hash function similar to java.lang.String.hashCode().
* The hash code for a string is computed as * The hash code for a string is computed as
@ -103,6 +106,6 @@ class _SerializerVisitor implements i18n.Visitor {
const serializerVisitor = new _SerializerVisitor(); const serializerVisitor = new _SerializerVisitor();
export function serializeAst(ast: i18n.Node[]): string[] { export function serializeNodes(nodes: i18n.Node[]): string[] {
return ast.map(a => a.visit(serializerVisitor, null)); return nodes.map(a => a.visit(serializerVisitor, null));
} }

View File

@ -21,4 +21,8 @@ export class TranslationBundle {
serializer: Serializer): TranslationBundle { serializer: Serializer): TranslationBundle {
return new TranslationBundle(serializer.load(content, 'url', placeholders)); return new TranslationBundle(serializer.load(content, 'url', placeholders));
} }
get(id: string): html.Node[] { return this._messageMap[id]; }
has(id: string): boolean { return id in this._messageMap; }
} }

View File

@ -10,7 +10,7 @@ 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 {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
import {serializeAst} from '../html_parser/ast_serializer_spec'; import {serializeNodes} from '../html_parser/ast_serializer_spec';
export function main() { export function main() {
describe('MessageExtractor', () => { describe('MessageExtractor', () => {
@ -21,6 +21,24 @@ export function main() {
]); ]);
}); });
it('should extract from elements', () => {
expect(
extract(
'<div i18n="m|d"><span i18n-title="m|d" title="single child">nested</span></div>'))
.toEqual([
[
['<span i18n-title="m|d" title="single child">nested</span>'],
'm',
'd',
],
[
['title="single child"'],
'm',
'd',
],
]);
});
it('should extract from elements', () => { it('should extract from elements', () => {
expect( expect(
extract( extract(
@ -87,6 +105,11 @@ export function main() {
[['{count, plural, =0 {text}}'], 'm', 'd'], [['{count, plural, =0 {text}}'], 'm', 'd'],
]); ]);
// single message when ICU is the only (implicit) children
expect(extract('<div>{count, plural, =0 {text}}</div>', ['div'])).toEqual([
[['{count, plural, =0 {text}}'], '', ''],
]);
// one message for the element content and one message for the ICU // 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([ expect(extract('<div i18n="m|d">before{count, plural, =0 {text}}after</div>')).toEqual([
[['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'], [['before', '{count, plural, =0 {text}}', 'after'], 'm', 'd'],
@ -184,18 +207,24 @@ export function main() {
it('should report nested translatable elements', () => { it('should report nested translatable elements', () => {
expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([ expect(extractErrors(`<p i18n><b i18n></b></p>`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'], ['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
['Unexpected section start', '<b i18n>'],
['Unexpected section end', '<p i18n>'],
]); ]);
}); });
it('should report translatable elements in implicit elements', () => { it('should report translatable elements in implicit elements', () => {
expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([ expect(extractErrors(`<p><b i18n></b></p>`, ['p'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'], ['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
['Unexpected section start', '<b i18n>'],
['Unexpected section end', '<p>'],
]); ]);
}); });
it('should report translatable elements in translatable blocks', () => { it('should report translatable elements in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([ expect(extractErrors(`<!-- i18n --><b i18n></b><!-- /i18n -->`)).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b i18n>'], ['Could not mark an element as translatable inside a translatable section', '<b i18n>'],
['Unexpected section start', '<b i18n>'],
['Unexpected section end', '<!--'],
]); ]);
}); });
}); });
@ -246,18 +275,24 @@ export function main() {
it('should report nested implicit elements', () => { it('should report nested implicit elements', () => {
expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([ expect(extractErrors(`<p><b></b></p>`, ['p', 'b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'], ['Could not mark an element as translatable inside a translatable section', '<b>'],
['Unexpected section start', '<b>'],
['Unexpected section end', '<p>'],
]); ]);
}); });
it('should report implicit element in translatable element', () => { it('should report implicit element in translatable element', () => {
expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([ expect(extractErrors(`<p i18n><b></b></p>`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'], ['Could not mark an element as translatable inside a translatable section', '<b>'],
['Unexpected section start', '<b>'],
['Unexpected section end', '<p i18n>'],
]); ]);
}); });
it('should report implicit element in translatable blocks', () => { it('should report implicit element in translatable blocks', () => {
expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([ expect(extractErrors(`<!-- i18n --><b></b><!-- /i18n -->`, ['b'])).toEqual([
['Could not mark an element as translatable inside a translatable section', '<b>'], ['Could not mark an element as translatable inside a translatable section', '<b>'],
['Unexpected section start', '<b>'],
['Unexpected section end', '<!--'],
]); ]);
}); });
}); });
@ -285,7 +320,7 @@ function extract(
// clang-format off // clang-format off
// https://github.com/angular/clang-format/issues/35 // https://github.com/angular/clang-format/issues/35
return messages.map( return messages.map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][]; message => [serializeNodes(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on // clang-format on
} }

View File

@ -8,9 +8,9 @@
import {Message} from '@angular/compiler/src/i18n/i18n_ast'; import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {extractI18nMessages} from '@angular/compiler/src/i18n/i18n_parser'; import {extractI18nMessages} from '@angular/compiler/src/i18n/i18n_parser';
import {ddescribe, describe, expect, it} from '@angular/core/testing/testing_internal'; import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
import {serializeAst} from '../../src/i18n/message_bundle'; import {serializeNodes} from '../../src/i18n/message_bundle';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
@ -289,7 +289,7 @@ function _humanizeMessages(
// clang-format off // clang-format off
// https://github.com/angular/clang-format/issues/35 // https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map( return _extractMessages(html, implicitTags, implicitAttrs).map(
message => [serializeAst(message.nodes), message.meaning, message.description, ]) as [string[], string, string][]; message => [serializeNodes(message.nodes), message.meaning, message.description, ]) as [string[], string, string][];
// clang-format on // clang-format on
} }

View File

@ -10,7 +10,7 @@ import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer'; 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 {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
import {MessageBundle, serializeAst, strHash} from '../../src/i18n/message_bundle'; import {MessageBundle, serializeNodes, strHash} from '../../src/i18n/message_bundle';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
@ -59,7 +59,7 @@ export function main(): void {
class _TestSerializer implements Serializer { class _TestSerializer implements Serializer {
write(messageMap: {[id: string]: i18n.Message}): string { write(messageMap: {[id: string]: i18n.Message}): string {
return Object.keys(messageMap) return Object.keys(messageMap)
.map(id => `${id}=${serializeAst(messageMap[id].nodes)}`) .map(id => `${id}=${serializeNodes(messageMap[id].nodes)}`)
.join('//'); .join('//');
} }

View File

@ -12,7 +12,7 @@ import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit
import {HtmlParser} from '../../../src/ml_parser/html_parser'; import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
import {serializeAst} from '../../ml_parser/ast_serializer_spec'; import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
export function main(): void { export function main(): void {
describe('XTB serializer', () => { describe('XTB serializer', () => {
@ -22,7 +22,7 @@ export function main(): void {
{[id: string]: string} { {[id: string]: string} {
const asAst = serializer.load(content, 'url', placeholders); const asAst = serializer.load(content, 'url', placeholders);
let asText: {[id: string]: string} = {}; let asText: {[id: string]: string} = {};
Object.keys(asAst).forEach(id => { asText[id] = serializeAst(asAst[id]).join(''); }); Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
return asText; return asText;
} }

View File

@ -11,7 +11,7 @@ import * as html from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
export function main() { export function main() {
describe('Node serilaizer', () => { describe('Node serializer', () => {
var parser: HtmlParser; var parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); }); beforeEach(() => { parser = new HtmlParser(); });
@ -19,31 +19,31 @@ export function main() {
it('should support element', () => { it('should support element', () => {
const html = '<p></p>'; const html = '<p></p>';
const ast = parser.parse(html, 'url'); const ast = parser.parse(html, 'url');
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
it('should support attributes', () => { it('should support attributes', () => {
const html = '<p k="value"></p>'; const html = '<p k="value"></p>';
const ast = parser.parse(html, 'url'); const ast = parser.parse(html, 'url');
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
it('should support text', () => { it('should support text', () => {
const html = 'some text'; const html = 'some text';
const ast = parser.parse(html, 'url'); const ast = parser.parse(html, 'url');
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
it('should support expansion', () => { it('should support expansion', () => {
const html = '{number, plural, =0 {none} =1 {one} other {many}}'; const html = '{number, plural, =0 {none} =1 {one} other {many}}';
const ast = parser.parse(html, 'url', true); const ast = parser.parse(html, 'url', true);
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
it('should support comment', () => { it('should support comment', () => {
const html = '<!--comment-->'; const html = '<!--comment-->';
const ast = parser.parse(html, 'url', true); const ast = parser.parse(html, 'url', true);
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
it('should support nesting', () => { it('should support nesting', () => {
@ -55,7 +55,7 @@ export function main() {
</p> </p>
</div>`; </div>`;
const ast = parser.parse(html, 'url', true); const ast = parser.parse(html, 'url', true);
expect(serializeAst(ast.rootNodes)).toEqual([html]); expect(serializeNodes(ast.rootNodes)).toEqual([html]);
}); });
}); });
} }
@ -91,6 +91,6 @@ class _SerializerVisitor implements html.Visitor {
const serializerVisitor = new _SerializerVisitor(); const serializerVisitor = new _SerializerVisitor();
export function serializeAst(nodes: html.Node[]): string[] { export function serializeNodes(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null)); return nodes.map(node => node.visit(serializerVisitor, null));
} }