From d7e1175df00f8ba27fa95e90e021ca2a34370b48 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sun, 27 Mar 2016 18:30:32 -0700 Subject: [PATCH] feat(i18n): implement xmb deserialization --- modules/angular2/src/i18n/xmb_serializer.ts | 87 +++++++++++++- .../angular2/test/i18n/xmb_serializer_spec.ts | 108 +++++++++++++++--- 2 files changed, 180 insertions(+), 15 deletions(-) diff --git a/modules/angular2/src/i18n/xmb_serializer.ts b/modules/angular2/src/i18n/xmb_serializer.ts index d6b1d7e857..2dfc09244f 100644 --- a/modules/angular2/src/i18n/xmb_serializer.ts +++ b/modules/angular2/src/i18n/xmb_serializer.ts @@ -1,12 +1,95 @@ -import {isPresent} from 'angular2/src/facade/lang'; +import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/facade/lang'; +import {HtmlAst, HtmlElementAst} from 'angular2/src/compiler/html_ast'; import {Message, id} from './message'; +import {HtmlParser} from 'angular2/src/compiler/html_parser'; +import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util'; + +let _PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\`); +const _ID_ATTR = "id"; +const _MSG_ELEMENT = "msg"; +const _BUNDLE_ELEMENT = "message-bundle"; export function serializeXmb(messages: Message[]): string { let ms = messages.map((m) => _serializeMessage(m)).join(""); return `${ms}`; } +export class XmbDeserializationResult { + constructor(public content: string, public messages: {[key: string]: HtmlAst[]}, + public errors: ParseError[]) {} +} + +export class XmbDeserializationError extends ParseError { + constructor(span: ParseSourceSpan, msg: string) { super(span, msg); } +} + +export function deserializeXmb(content: string, url: string): XmbDeserializationResult { + let parser = new HtmlParser(); + let normalizedContent = _expandPlaceholder(content.trim()); + let parsed = parser.parse(normalizedContent, url); + + if (parsed.errors.length > 0) { + return new XmbDeserializationResult(null, {}, parsed.errors); + } + + if (_checkRootElement(parsed.rootNodes)) { + return new XmbDeserializationResult( + null, {}, [new XmbDeserializationError(null, `Missing element "${_BUNDLE_ELEMENT}"`)]); + } + + let bundleEl = parsed.rootNodes[0]; // test this + let errors = []; + let messages: {[key: string]: HtmlAst[]} = {}; + + _createMessages(bundleEl.children, messages, errors); + + return (errors.length == 0) ? + new XmbDeserializationResult(normalizedContent, messages, []) : + new XmbDeserializationResult(null, <{[key: string]: HtmlAst[]}>{}, errors); +} + +function _checkRootElement(nodes: HtmlAst[]): boolean { + return nodes.length < 1 || !(nodes[0] instanceof HtmlElementAst) || + (nodes[0]).name != _BUNDLE_ELEMENT; +} + +function _createMessages(nodes: HtmlAst[], messages: {[key: string]: HtmlAst[]}, + errors: ParseError[]): void { + nodes.forEach((item) => { + if (item instanceof HtmlElementAst) { + let msg = item; + + if (msg.name != _MSG_ELEMENT) { + errors.push( + new XmbDeserializationError(item.sourceSpan, `Unexpected element "${msg.name}"`)); + return; + } + + let id = _id(msg); + if (isBlank(id)) { + errors.push( + new XmbDeserializationError(item.sourceSpan, `"${_ID_ATTR}" attribute is missing`)); + return; + } + + messages[id] = msg.children; + } + }); +} + +function _id(el: HtmlElementAst): string { + let ids = el.attrs.filter(a => a.name == _ID_ATTR); + return ids.length > 0 ? ids[0].value : null; +} + function _serializeMessage(m: Message): string { let desc = isPresent(m.description) ? ` desc='${m.description}'` : ""; return `${m.content}`; -} \ No newline at end of file +} + +function _expandPlaceholder(input: string): string { + return RegExpWrapper.replaceAll(_PLACEHOLDER_REGEXP, input, (match) => { + let nameWithQuotes = match[2]; + return ``; + }); +} diff --git a/modules/angular2/test/i18n/xmb_serializer_spec.ts b/modules/angular2/test/i18n/xmb_serializer_spec.ts index 5debf72e6f..839e87a828 100644 --- a/modules/angular2/test/i18n/xmb_serializer_spec.ts +++ b/modules/angular2/test/i18n/xmb_serializer_spec.ts @@ -11,25 +11,107 @@ import { xit } from 'angular2/testing_internal'; +import {HtmlAst} from 'angular2/src/compiler/html_ast'; import {Message, id} from 'angular2/src/i18n/message'; -import {serializeXmb} from 'angular2/src/i18n/xmb_serializer'; +import {serializeXmb, deserializeXmb} from 'angular2/src/i18n/xmb_serializer'; +import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util'; export function main() { - describe('Xmb Serialization', () => { - it("should return an empty message bundle for an empty list of messages", - () => { expect(serializeXmb([])).toEqual(""); }); + describe("Xmb", () => { + describe('Xmb Serialization', () => { + it("should return an empty message bundle for an empty list of messages", + () => { expect(serializeXmb([])).toEqual(""); }); - it("should serializeXmb messages without desc", () => { - let m = new Message("content", "meaning", null); - let expected = `content`; - expect(serializeXmb([m])).toEqual(expected); + it("should serializeXmb messages without desc", () => { + let m = new Message("content", "meaning", null); + let expected = `content`; + expect(serializeXmb([m])).toEqual(expected); + }); + + it("should serializeXmb messages with desc", () => { + let m = new Message("content", "meaning", "description"); + let expected = + `content`; + expect(serializeXmb([m])).toEqual(expected); + }); }); - it("should serializeXmb messages with desc", () => { - let m = new Message("content", "meaning", "description"); - let expected = - `content`; - expect(serializeXmb([m])).toEqual(expected); + describe("Xmb Deserialization", () => { + it("should parse an empty bundle", () => { + let mb = ""; + expect(deserializeXmb(mb, "url").messages).toEqual({}); + }); + + it("should parse an non-empty bundle", () => { + let mb = ` + + content1 + content2 + + `; + + 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 = ` + + content + + `; + + 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 = ` + + content + + `; + + 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 = ` + + content + + `; + + let res = deserializeXmb(mb, "url"); + expect(_serializeErrors(res.errors)).toEqual(['Unexpected element "invalid"']); + }); + + it("should expand 'ph' elements", () => { + let mb = ` + + a + + `; + + let res = deserializeXmb(mb, "url").messages["id1"]; + expect((res[1]).name).toEqual("ph"); + }); }); }); } + +function _serialize(nodes: HtmlAst[]): string { + return (nodes[0]).value; +} + +function _serializeErrors(errors: ParseError[]): string[] { + return errors.map(e => e.msg); +} \ No newline at end of file