import {AbstractMessageParser} from './abstract-message-parser';
import {ParsedMessage} from './parsed-message';
import {ParsedMessagePartStartTag} from './parsed-message-part-start-tag';
import {ParsedMessagePartEndTag} from './parsed-message-part-end-tag';
import {ParsedMessagePartPlaceholder} from './parsed-message-part-placeholder';
import {ParsedMessagePartText} from './parsed-message-part-text';
import {ParsedMessagePartType} from './parsed-message-part';
import {TagMapping} from './tag-mapping';
import {ParsedMessagePartEmptyTag} from './parsed-message-part-empty-tag';
import {ParsedMessagePartICUMessageRef} from './parsed-message-part-icu-message-ref';
/**
* Created by roobm on 10.05.2017.
* A message parser for XLIFF 2.0
*/
export class Xliff2MessageParser 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') {
// placeholder are like
// They contain the id and also a name (number in the example)
// TODO make some use of the name (but it is not available in XLIFF 1.2)
// ICU message are handled with the same tag
// Before 4.3.2 they did not have an equiv and disp (Bug #17344):
// e.g.
// Beginning with 4.3.2 they do have an equiv ICU and disp:
// e.g.
// and empty tags have equiv other then INTERPOLATION:
// e.g.
// or
let isInterpolation = false;
let isICU = false;
let isEmptyTag = false;
const equiv = elementNode.getAttribute('equiv');
const disp = elementNode.getAttribute('disp');
let indexString = null;
let index = 0;
let emptyTagName = null;
if (!equiv) {
// old ICU syntax, fixed with #17344
isICU = true;
indexString = elementNode.getAttribute('id');
index = Number.parseInt(indexString, 10);
} else if (equiv.startsWith('ICU')) {
// new ICU syntax, fixed with #17344
isICU = true;
if (equiv === 'ICU') {
indexString = '0';
} else {
indexString = equiv.substring('ICU_'.length);
}
index = Number.parseInt(indexString, 10);
} else if (equiv.startsWith('INTERPOLATION')) {
isInterpolation = true;
if (equiv === 'INTERPOLATION') {
indexString = '0';
} else {
indexString = equiv.substring('INTERPOLATION_'.length);
}
index = Number.parseInt(indexString, 10);
} else if (new TagMapping().isEmptyTagPlaceholderName(equiv)) {
isEmptyTag = true;
emptyTagName = new TagMapping().getTagnameFromEmptyTagPlaceholderName(equiv);
} else {
return true;
}
if (isInterpolation) {
message.addPlaceholder(index, disp);
} else if (isICU) {
message.addICUMessageRef(index, disp);
} else if (isEmptyTag) {
message.addEmptyTag(emptyTagName, this.parseIdCountFromName(equiv));
}
} else if (tagName === 'pc') {
// pc example: IMPORTANT
const embeddedTagName = this.tagNameFromPCElement(elementNode);
if (embeddedTagName) {
message.addStartTag(embeddedTagName, this.parseIdCountFromName(elementNode.getAttribute('equivStart')));
}
}
return true;
}
/**
* 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) {
const tagName = elementNode.tagName;
if (tagName === 'pc') {
// pc example: IMPORTANT
const embeddedTagName = this.tagNameFromPCElement(elementNode);
if (embeddedTagName) {
message.addEndTag(embeddedTagName);
}
return;
}
}
private tagNameFromPCElement(pcNode: Element): string {
let dispStart = pcNode.getAttribute('dispStart');
if (dispStart.startsWith('<')) {
dispStart = dispStart.substring(1);
}
if (dispStart.endsWith('>')) {
dispStart = dispStart.substring(0, dispStart.length - 1);
}
return dispStart;
}
/**
* reimplemented here, because XLIFF 2.0 uses a deeper xml model.
* So we cannot simply replace the message parts by xml parts.
* @param message message
* @param rootElem rootElem
*/
protected addXmlRepresentationToRoot(message: ParsedMessage, rootElem: Element) {
const stack = [{element: rootElem, tagName: 'root'}];
let id = 0;
message.parts().forEach((part) => {
switch (part.type) {
case ParsedMessagePartType.TEXT:
stack[stack.length - 1].element.appendChild(
this.createXmlRepresentationOfTextPart( part, rootElem));
break;
case ParsedMessagePartType.PLACEHOLDER:
stack[stack.length - 1].element.appendChild(
this.createXmlRepresentationOfPlaceholderPart( part, rootElem, id++));
break;
case ParsedMessagePartType.ICU_MESSAGE_REF:
stack[stack.length - 1].element.appendChild(
this.createXmlRepresentationOfICUMessageRefPart( part, rootElem));
break;
case ParsedMessagePartType.START_TAG:
const newTagElem = this.createXmlRepresentationOfStartTagPart( part, rootElem, id++);
stack[stack.length - 1].element.appendChild(newTagElem);
stack.push({element: newTagElem, tagName: ( part).tagName()});
break;
case ParsedMessagePartType.END_TAG:
const closeTagName = ( part).tagName();
if (stack.length <= 1 || stack[stack.length - 1].tagName !== closeTagName) {
// oops, not well formed
throw new Error('unexpected close tag ' + closeTagName);
}
stack.pop();
break;
case ParsedMessagePartType.EMPTY_TAG:
const emptyTagElem = this.createXmlRepresentationOfEmptyTagPart( part, rootElem, id++);
stack[stack.length - 1].element.appendChild(emptyTagElem);
break;
}
});
if (stack.length !== 1) {
// oops, not well closed tags
throw new Error('missing close tag ' + stack[stack.length - 1].tagName);
}
}
/**
* the xml used for start tag in the message.
* Returns an empty pc-Element.
* e.g.
* Text content will be added later.
* @param part part
* @param rootElem rootElem
* @param id id number in xliff2
*/
protected createXmlRepresentationOfStartTagPart(part: ParsedMessagePartStartTag, rootElem: Element, id: number): Node {
const tagMapping = new TagMapping();
const pcElem = rootElem.ownerDocument.createElement('pc');
const tagName = part.tagName();
const equivStart = tagMapping.getStartTagPlaceholderName(tagName, part.idCounter());
const equivEnd = tagMapping.getCloseTagPlaceholderName(tagName);
const dispStart = '<' + tagName + '>';
const dispEnd = '' + tagName + '>';
pcElem.setAttribute('id', id.toString(10));
pcElem.setAttribute('equivStart', equivStart);
pcElem.setAttribute('equivEnd', equivEnd);
pcElem.setAttribute('type', this.getTypeForTag(tagName));
pcElem.setAttribute('dispStart', dispStart);
pcElem.setAttribute('dispEnd', dispEnd);
return pcElem;
}
/**
* the xml used for end tag in the message.
* Not used here, because content is child of start tag.
* @param part part
* @param rootElem rootElem
*/
protected createXmlRepresentationOfEndTagPart(part: ParsedMessagePartEndTag, rootElem: Element): Node {
// not used
return null;
}
/**
* the xml used for empty tag in the message.
* Returns an empty ph-Element.
* e.g.
* @param part part
* @param rootElem rootElem
* @param id id number in xliff2
*/
protected createXmlRepresentationOfEmptyTagPart(part: ParsedMessagePartEmptyTag, rootElem: Element, id: number): Node {
const tagMapping = new TagMapping();
const phElem = rootElem.ownerDocument.createElement('ph');
const tagName = part.tagName();
const equiv = tagMapping.getEmptyTagPlaceholderName(tagName, part.idCounter());
const disp = '<' + tagName + '/>';
phElem.setAttribute('id', id.toString(10));
phElem.setAttribute('equiv', equiv);
phElem.setAttribute('type', this.getTypeForTag(tagName));
phElem.setAttribute('disp', disp);
return phElem;
}
private getTypeForTag(tag: string): string {
switch (tag.toLowerCase()) {
case 'br':
case 'b':
case 'i':
case 'u':
return 'fmt';
case 'img':
return 'image';
case 'a':
return 'link';
default:
return 'other';
}
}
/**
* the xml used for placeholder in the message.
* Returns e.g.
* @param part part
* @param rootElem rootElem
* @param id id number in xliff2
*/
protected createXmlRepresentationOfPlaceholderPart(part: ParsedMessagePartPlaceholder, rootElem: Element, id: number): Node {
const phElem = rootElem.ownerDocument.createElement('ph');
let equivAttrib = 'INTERPOLATION';
if (part.index() > 0) {
equivAttrib = 'INTERPOLATION_' + part.index().toString(10);
}
phElem.setAttribute('id', id.toString(10));
phElem.setAttribute('equiv', equivAttrib);
const disp = part.disp();
if (disp) {
phElem.setAttribute('disp', disp);
}
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 equivAttrib = 'ICU';
if (part.index() > 0) {
equivAttrib = 'ICU_' + part.index().toString(10);
}
phElem.setAttribute('id', part.index().toString(10));
phElem.setAttribute('equiv', equivAttrib);
const disp = part.disp();
if (disp) {
phElem.setAttribute('disp', disp);
}
return phElem;
}
}