import {AbstractMessageParser} from './abstract-message-parser';
import {ParsedMessage} from './parsed-message';
import {DOMUtilities} from './dom-utilities';
import {ParsedMessagePartStartTag} from './parsed-message-part-start-tag';
import {ParsedMessagePartEndTag} from './parsed-message-part-end-tag';
import {ParsedMessagePartPlaceholder} from './parsed-message-part-placeholder';
import {TagMapping} from './tag-mapping';
import {ParsedMessagePartEmptyTag} from './parsed-message-part-empty-tag';
import {ParsedMessagePartICUMessageRef} from './parsed-message-part-icu-message-ref';
import {ParsedMessagePart, ParsedMessagePartType} from './parsed-message-part';
import {ParsedMessagePartText} from './parsed-message-part-text';
/**
* Created by roobm on 10.05.2017.
* A message parser for XMB
*/
export class XmbMessageParser extends AbstractMessageParser {
/**
* Handle this element node.
* This is called before the children are done.
* @param elementNode elementNode
* @param message message to be altered
* @return true, if children should be processed too, false otherwise (children ignored then)
*/
protected processStartElement(elementNode: Element, message: ParsedMessage): boolean {
const tagName = elementNode.tagName;
if (tagName === 'ph') {
// There are 4 different usages of ph element:
// 1. placeholders are like INTERPOLATION
// or INTERPOLATION_1
// 2. start tags:
// <a>
// 3. empty tags:
// <img>
// 4. ICU:
// ICU
const name = elementNode.getAttribute('name');
if (!name) {
return true; // should not happen
}
if (name.startsWith('INTERPOLATION')) {
const index = this.parsePlaceholderIndexFromName(name);
message.addPlaceholder(index, null);
return false; // ignore children
} else if (name.startsWith('START_')) {
const tag = this.parseTagnameFromPhElement(elementNode);
const idcounter = this.parseIdCountFromName(name);
if (tag) {
message.addStartTag(tag, idcounter);
}
return false; // ignore children
} else if (name.startsWith('CLOSE_')) {
const tag = this.parseTagnameFromPhElement(elementNode);
if (tag) {
message.addEndTag(tag);
}
return false; // ignore children
} else if (new TagMapping().isEmptyTagPlaceholderName(name)) {
const emptyTagName = new TagMapping().getTagnameFromEmptyTagPlaceholderName(name);
const idcounter = this.parseIdCountFromName(name);
message.addEmptyTag(emptyTagName, idcounter);
return false; // ignore children
} else if (name.startsWith('ICU')) {
const index = this.parseICUMessageIndexFromName(name);
message.addICUMessageRef(index, null);
return false; // ignore children
}
} else if (tagName === 'source') {
// ignore source
return false;
}
return true;
}
/**
* Return the ICU message content of the node, if it is an ICU Message.
* @param node node
* @return message or null, if it is no ICU Message.
*/
protected getICUMessageText(node: Node): string {
const children = node.childNodes;
if (children.length === 0) {
return null;
}
let firstChild = null;
// find first child that is no source element.
let i;
for (i = 0; i < children.length; i++) {
const child = children.item(i);
if (child.nodeType !== child.ELEMENT_NODE || ( child).tagName !== 'source') {
firstChild = child;
break;
}
}
if (firstChild && firstChild.nodeType === firstChild.TEXT_NODE) {
if (this.isICUMessageStart(firstChild.textContent)) {
const messageText = DOMUtilities.getXMLContent( node);
if (i > 0) {
// drop elements
const reSource: RegExp = new RegExp(']*>.*', 'g');
return messageText.replace(reSource, '');
} else {
return messageText;
}
} else {
return null;
}
} else {
return null;
}
}
/**
* Handle end of this element node.
* This is called after all children are processed.
* @param elementNode elementNode
* @param message message to be altered
*/
protected processEndElement(elementNode: Element, message: ParsedMessage) {
}
/**
* Parse id attribute of x element as placeholder index.
* id can be "INTERPOLATION" or "INTERPOLATION_n"
* @param name name
* @return id as number
*/
private parsePlaceholderIndexFromName(name: string): number {
let indexString = '';
if (name === 'INTERPOLATION') {
indexString = '0';
} else {
indexString = name.substring('INTERPOLATION_'.length);
}
return Number.parseInt(indexString, 10);
}
/**
* Parse id attribute of x element as ICU message ref index.
* id can be "ICU" or "ICU_n"
* @param name name
* @return id as number
*/
private parseICUMessageIndexFromName(name: string): number {
let indexString = '';
if (name === 'ICU') {
indexString = '0';
} else {
indexString = name.substring('ICU_'.length);
}
return Number.parseInt(indexString, 10);
}
/**
* Parse the tag name from a ph element.
* It contained in the subelements value and enclosed in <>.
* Example: <b>
* @param phElement phElement
*/
private parseTagnameFromPhElement(phElement: Element): string {
const exElement = DOMUtilities.getFirstElementByTagName(phElement, 'ex');
if (exElement) {
const value = DOMUtilities.getPCDATA(exElement);
if (!value || !value.startsWith('<') || !value.endsWith('>')) {
// oops
return null;
}
if (value.charAt(1) === '/') {
return value.substring(2, value.length - 1);
} else {
return value.substring(1, value.length - 1);
}
} else {
return null;
}
}
protected addXmlRepresentationToRoot(message: ParsedMessage, rootElem: Element) {
message.parts().forEach((part) => {
const child = this.createXmlRepresentationOfPart(part, rootElem);
if (child) {
rootElem.appendChild(child);
}
});
}
protected createXmlRepresentationOfPart(part: ParsedMessagePart, rootElem: Element): Node {
switch (part.type) {
case ParsedMessagePartType.TEXT:
return this.createXmlRepresentationOfTextPart( part, rootElem);
case ParsedMessagePartType.START_TAG:
return this.createXmlRepresentationOfStartTagPart((part), rootElem);
case ParsedMessagePartType.END_TAG:
return this.createXmlRepresentationOfEndTagPart((part), rootElem);
case ParsedMessagePartType.EMPTY_TAG:
return this.createXmlRepresentationOfEmptyTagPart((part), rootElem);
case ParsedMessagePartType.PLACEHOLDER:
return this.createXmlRepresentationOfPlaceholderPart((part), rootElem);
case ParsedMessagePartType.ICU_MESSAGE_REF:
return this.createXmlRepresentationOfICUMessageRefPart((part), rootElem);
}
}
/**
* the xml used for start tag in the message.
* Returns an -Element with attribute name and subelement ex
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfStartTagPart(part: ParsedMessagePartStartTag, rootElem: Element): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
const tagMapping = new TagMapping();
const nameAttrib = tagMapping.getStartTagPlaceholderName(part.tagName(), part.idCounter());
phElem.setAttribute('name', nameAttrib);
const exElem = rootElem.ownerDocument.createElement('ex');
exElem.appendChild(rootElem.ownerDocument.createTextNode('<' + part.tagName() + '>'));
phElem.appendChild(exElem);
return phElem;
}
/**
* the xml used for end tag in the message.
* Returns an -Element with attribute name and subelement ex
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfEndTagPart(part: ParsedMessagePartEndTag, rootElem: Element): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
const tagMapping = new TagMapping();
const nameAttrib = tagMapping.getCloseTagPlaceholderName(part.tagName());
phElem.setAttribute('name', nameAttrib);
const exElem = rootElem.ownerDocument.createElement('ex');
exElem.appendChild(rootElem.ownerDocument.createTextNode('' + part.tagName() + '>'));
phElem.appendChild(exElem);
return phElem;
}
/**
* the xml used for empty tag in the message.
* Returns an -Element with attribute name and subelement ex
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfEmptyTagPart(part: ParsedMessagePartEmptyTag, rootElem: Element): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
const tagMapping = new TagMapping();
const nameAttrib = tagMapping.getEmptyTagPlaceholderName(part.tagName(), part.idCounter());
phElem.setAttribute('name', nameAttrib);
const exElem = rootElem.ownerDocument.createElement('ex');
exElem.appendChild(rootElem.ownerDocument.createTextNode('<' + part.tagName() + '>'));
phElem.appendChild(exElem);
return phElem;
}
/**
* the xml used for placeholder in the message.
* Returns an -Element with attribute name and subelement ex
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfPlaceholderPart(part: ParsedMessagePartPlaceholder, rootElem: Element): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
let nameAttrib = 'INTERPOLATION';
if (part.index() > 0) {
nameAttrib = 'INTERPOLATION_' + part.index().toString(10);
}
phElem.setAttribute('name', nameAttrib);
const exElem = rootElem.ownerDocument.createElement('ex');
exElem.appendChild(rootElem.ownerDocument.createTextNode(nameAttrib));
phElem.appendChild(exElem);
return phElem;
}
/**
* the xml used for icu message refs in the message.
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfICUMessageRefPart(part: ParsedMessagePartICUMessageRef, rootElem: Element): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
let nameAttrib = 'ICU';
if (part.index() > 0) {
nameAttrib = 'ICU_' + part.index().toString(10);
}
phElem.setAttribute('name', nameAttrib);
const exElem = rootElem.ownerDocument.createElement('ex');
exElem.appendChild(rootElem.ownerDocument.createTextNode(nameAttrib));
phElem.appendChild(exElem);
return phElem;
}
}