import { map, forEach, isString, filter, assign } from 'min-dash'; import { isSimple as isSimpleType } from 'moddle/lib/types'; import { parseName as parseNameNs } from 'moddle/lib/ns'; import { hasLowerCaseAlias, serializeAsType, serializeAsProperty, DEFAULT_NS_MAP, XSI_TYPE } from './common'; var XML_PREAMBLE = '\n'; var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g; var ESCAPE_CHARS = /<|>|&/g; export function Namespaces(parent) { var prefixMap = {}; var uriMap = {}; var used = {}; var wellknown = []; var custom = []; // API this.byUri = function(uri) { return uriMap[uri] || ( parent && parent.byUri(uri) ); }; this.add = function(ns, isWellknown) { uriMap[ns.uri] = ns; if (isWellknown) { wellknown.push(ns); } else { custom.push(ns); } this.mapPrefix(ns.prefix, ns.uri); }; this.uriByPrefix = function(prefix) { return prefixMap[prefix || 'xmlns']; }; this.mapPrefix = function(prefix, uri) { prefixMap[prefix || 'xmlns'] = uri; }; this.logUsed = function(ns) { var uri = ns.uri; used[uri] = this.byUri(uri); }; this.getUsed = function(ns) { function isUsed(ns) { return used[ns.uri]; } var allNs = [].concat(wellknown, custom); return allNs.filter(isUsed); }; } function lower(string) { return string.charAt(0).toLowerCase() + string.slice(1); } function nameToAlias(name, pkg) { if (hasLowerCaseAlias(pkg)) { return lower(name); } else { return name; } } function inherits(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); } function nsName(ns) { if (isString(ns)) { return ns; } else { return (ns.prefix ? ns.prefix + ':' : '') + ns.localName; } } function getNsAttrs(namespaces) { return map(namespaces.getUsed(), function(ns) { var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : ''); return { name: name, value: ns.uri }; }); } function getElementNs(ns, descriptor) { if (descriptor.isGeneric) { return assign({ localName: descriptor.ns.localName }, ns); } else { return assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns); } } function getPropertyNs(ns, descriptor) { return assign({ localName: descriptor.ns.localName }, ns); } function getSerializableProperties(element) { var descriptor = element.$descriptor; return filter(descriptor.properties, function(p) { var name = p.name; if (p.isVirtual) { return false; } // do not serialize defaults if (!element.hasOwnProperty(name)) { return false; } var value = element[name]; // do not serialize default equals if (value === p.default) { return false; } // do not serialize null properties if (value === null) { return false; } return p.isMany ? value.length : true; }); } var ESCAPE_ATTR_MAP = { '\n': '#10', '\n\r': '#10', '"': '#34', '\'': '#39', '<': '#60', '>': '#62', '&': '#38' }; var ESCAPE_MAP = { '<': 'lt', '>': 'gt', '&': 'amp' }; function escape(str, charPattern, replaceMap) { // ensure we are handling strings here str = isString(str) ? str : '' + str; return str.replace(charPattern, function(s) { return '&' + replaceMap[s] + ';'; }); } /** * Escape a string attribute to not contain any bad values (line breaks, '"', ...) * * @param {String} str the string to escape * @return {String} the escaped string */ function escapeAttr(str) { return escape(str, ESCAPE_ATTR_CHARS, ESCAPE_ATTR_MAP); } function escapeBody(str) { return escape(str, ESCAPE_CHARS, ESCAPE_MAP); } function filterAttributes(props) { return filter(props, function(p) { return p.isAttr; }); } function filterContained(props) { return filter(props, function(p) { return !p.isAttr; }); } function ReferenceSerializer(tagName) { this.tagName = tagName; } ReferenceSerializer.prototype.build = function(element) { this.element = element; return this; }; ReferenceSerializer.prototype.serializeTo = function(writer) { writer .appendIndent() .append('<' + this.tagName + '>' + this.element.id + '') .appendNewLine(); }; function BodySerializer() {} BodySerializer.prototype.serializeValue = BodySerializer.prototype.serializeTo = function(writer) { writer.append( this.escape ? escapeBody(this.value) : this.value ); }; BodySerializer.prototype.build = function(prop, value) { this.value = value; if (prop.type === 'String' && value.search(ESCAPE_CHARS) !== -1) { this.escape = true; } return this; }; function ValueSerializer(tagName) { this.tagName = tagName; } inherits(ValueSerializer, BodySerializer); ValueSerializer.prototype.serializeTo = function(writer) { writer .appendIndent() .append('<' + this.tagName + '>'); this.serializeValue(writer); writer .append('') .appendNewLine(); }; function ElementSerializer(parent, propertyDescriptor) { this.body = []; this.attrs = []; this.parent = parent; this.propertyDescriptor = propertyDescriptor; } ElementSerializer.prototype.build = function(element) { this.element = element; var elementDescriptor = element.$descriptor, propertyDescriptor = this.propertyDescriptor; var otherAttrs, properties; var isGeneric = elementDescriptor.isGeneric; if (isGeneric) { otherAttrs = this.parseGeneric(element); } else { otherAttrs = this.parseNsAttributes(element); } if (propertyDescriptor) { this.ns = this.nsPropertyTagName(propertyDescriptor); } else { this.ns = this.nsTagName(elementDescriptor); } // compute tag name this.tagName = this.addTagName(this.ns); if (!isGeneric) { properties = getSerializableProperties(element); this.parseAttributes(filterAttributes(properties)); this.parseContainments(filterContained(properties)); } this.parseGenericAttributes(element, otherAttrs); return this; }; ElementSerializer.prototype.nsTagName = function(descriptor) { var effectiveNs = this.logNamespaceUsed(descriptor.ns); return getElementNs(effectiveNs, descriptor); }; ElementSerializer.prototype.nsPropertyTagName = function(descriptor) { var effectiveNs = this.logNamespaceUsed(descriptor.ns); return getPropertyNs(effectiveNs, descriptor); }; ElementSerializer.prototype.isLocalNs = function(ns) { return ns.uri === this.ns.uri; }; /** * Get the actual ns attribute name for the given element. * * @param {Object} element * @param {Boolean} [element.inherited=false] * * @return {Object} nsName */ ElementSerializer.prototype.nsAttributeName = function(element) { var ns; if (isString(element)) { ns = parseNameNs(element); } else { ns = element.ns; } // return just local name for inherited attributes if (element.inherited) { return { localName: ns.localName }; } // parse + log effective ns var effectiveNs = this.logNamespaceUsed(ns); // LOG ACTUAL namespace use this.getNamespaces().logUsed(effectiveNs); // strip prefix if same namespace like parent if (this.isLocalNs(effectiveNs)) { return { localName: ns.localName }; } else { return assign({ localName: ns.localName }, effectiveNs); } }; ElementSerializer.prototype.parseGeneric = function(element) { var self = this, body = this.body; var attributes = []; forEach(element, function(val, key) { var nonNsAttr; if (key === '$body') { body.push(new BodySerializer().build({ type: 'String' }, val)); } else if (key === '$children') { forEach(val, function(child) { body.push(new ElementSerializer(self).build(child)); }); } else if (key.indexOf('$') !== 0) { nonNsAttr = self.parseNsAttribute(element, key, val); if (nonNsAttr) { attributes.push({ name: key, value: val }); } } }); return attributes; }; ElementSerializer.prototype.parseNsAttribute = function(element, name, value) { var model = element.$model; var nameNs = parseNameNs(name); var ns; // parse xmlns:foo="http://foo.bar" if (nameNs.prefix === 'xmlns') { ns = { prefix: nameNs.localName, uri: value }; } // parse xmlns="http://foo.bar" if (!nameNs.prefix && nameNs.localName === 'xmlns') { ns = { uri: value }; } if (!ns) { return { name: name, value: value }; } if (model && model.getPackage(value)) { // register well known namespace this.logNamespace(ns, true, true); } else { // log custom namespace directly as used var actualNs = this.logNamespaceUsed(ns, true); this.getNamespaces().logUsed(actualNs); } }; /** * Parse namespaces and return a list of left over generic attributes * * @param {Object} element * @return {Array} */ ElementSerializer.prototype.parseNsAttributes = function(element, attrs) { var self = this; var genericAttrs = element.$attrs; var attributes = []; // parse namespace attributes first // and log them. push non namespace attributes to a list // and process them later forEach(genericAttrs, function(value, name) { var nonNsAttr = self.parseNsAttribute(element, name, value); if (nonNsAttr) { attributes.push(nonNsAttr); } }); return attributes; }; ElementSerializer.prototype.parseGenericAttributes = function(element, attributes) { var self = this; forEach(attributes, function(attr) { // do not serialize xsi:type attribute // it is set manually based on the actual implementation type if (attr.name === XSI_TYPE) { return; } try { self.addAttribute(self.nsAttributeName(attr.name), attr.value); } catch (e) { console.warn( 'missing namespace information for ', attr.name, '=', attr.value, 'on', element, e); } }); }; ElementSerializer.prototype.parseContainments = function(properties) { var self = this, body = this.body, element = this.element; forEach(properties, function(p) { var value = element.get(p.name), isReference = p.isReference, isMany = p.isMany; if (!isMany) { value = [ value ]; } if (p.isBody) { body.push(new BodySerializer().build(p, value[0])); } else if (isSimpleType(p.type)) { forEach(value, function(v) { body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v)); }); } else if (isReference) { forEach(value, function(v) { body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v)); }); } else { // allow serialization via type // rather than element name var asType = serializeAsType(p), asProperty = serializeAsProperty(p); forEach(value, function(v) { var serializer; if (asType) { serializer = new TypeSerializer(self, p); } else if (asProperty) { serializer = new ElementSerializer(self, p); } else { serializer = new ElementSerializer(self); } body.push(serializer.build(v)); }); } }); }; ElementSerializer.prototype.getNamespaces = function(local) { var namespaces = this.namespaces, parent = this.parent, parentNamespaces; if (!namespaces) { parentNamespaces = parent && parent.getNamespaces(); if (local || !parentNamespaces) { this.namespaces = namespaces = new Namespaces(parentNamespaces); } else { namespaces = parentNamespaces; } } return namespaces; }; ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) { var namespaces = this.getNamespaces(local); var nsUri = ns.uri, nsPrefix = ns.prefix; var existing = namespaces.byUri(nsUri); if (!existing) { namespaces.add(ns, wellknown); } namespaces.mapPrefix(nsPrefix, nsUri); return ns; }; ElementSerializer.prototype.logNamespaceUsed = function(ns, local) { var element = this.element, model = element.$model, namespaces = this.getNamespaces(local); // ns may be // // * prefix only // * prefix:uri // * localName only var prefix = ns.prefix, uri = ns.uri, newPrefix, idx, wellknownUri; // handle anonymous namespaces (elementForm=unqualified), cf. #23 if (!prefix && !uri) { return { localName: ns.localName }; } wellknownUri = DEFAULT_NS_MAP[prefix] || model && (model.getPackage(prefix) || {}).uri; uri = uri || wellknownUri || namespaces.uriByPrefix(prefix); if (!uri) { throw new Error('no namespace uri given for prefix <' + prefix + '>'); } ns = namespaces.byUri(uri); if (!ns) { newPrefix = prefix; idx = 1; // find a prefix that is not mapped yet while (namespaces.uriByPrefix(newPrefix)) { newPrefix = prefix + '_' + idx++; } ns = this.logNamespace({ prefix: newPrefix, uri: uri }, wellknownUri === uri); } if (prefix) { namespaces.mapPrefix(prefix, uri); } return ns; }; ElementSerializer.prototype.parseAttributes = function(properties) { var self = this, element = this.element; forEach(properties, function(p) { var value = element.get(p.name); if (p.isReference) { if (!p.isMany) { value = value.id; } else { var values = []; forEach(value, function(v) { values.push(v.id); }); // IDREFS is a whitespace-separated list of references. value = values.join(' '); } } self.addAttribute(self.nsAttributeName(p), value); }); }; ElementSerializer.prototype.addTagName = function(nsTagName) { var actualNs = this.logNamespaceUsed(nsTagName); this.getNamespaces().logUsed(actualNs); return nsName(nsTagName); }; ElementSerializer.prototype.addAttribute = function(name, value) { var attrs = this.attrs; if (isString(value)) { value = escapeAttr(value); } attrs.push({ name: name, value: value }); }; ElementSerializer.prototype.serializeAttributes = function(writer) { var attrs = this.attrs, namespaces = this.namespaces; if (namespaces) { attrs = getNsAttrs(namespaces).concat(attrs); } forEach(attrs, function(a) { writer .append(' ') .append(nsName(a.name)).append('="').append(a.value).append('"'); }); }; ElementSerializer.prototype.serializeTo = function(writer) { var firstBody = this.body[0], indent = firstBody && firstBody.constructor !== BodySerializer; writer .appendIndent() .append('<' + this.tagName); this.serializeAttributes(writer); writer.append(firstBody ? '>' : ' />'); if (firstBody) { if (indent) { writer .appendNewLine() .indent(); } forEach(this.body, function(b) { b.serializeTo(writer); }); if (indent) { writer .unindent() .appendIndent(); } writer.append(''); } writer.appendNewLine(); }; /** * A serializer for types that handles serialization of data types */ function TypeSerializer(parent, propertyDescriptor) { ElementSerializer.call(this, parent, propertyDescriptor); } inherits(TypeSerializer, ElementSerializer); TypeSerializer.prototype.parseNsAttributes = function(element) { // extracted attributes var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element); var descriptor = element.$descriptor; // only serialize xsi:type if necessary if (descriptor.name === this.propertyDescriptor.type) { return attributes; } var typeNs = this.typeNs = this.nsTagName(descriptor); this.getNamespaces().logUsed(this.typeNs); // add xsi:type attribute to represent the elements // actual type var pkg = element.$model.getPackage(typeNs.uri), typePrefix = (pkg.xml && pkg.xml.typePrefix) || ''; this.addAttribute( this.nsAttributeName(XSI_TYPE), (typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName ); return attributes; }; TypeSerializer.prototype.isLocalNs = function(ns) { return ns.uri === (this.typeNs || this.ns).uri; }; function SavingWriter() { this.value = ''; this.write = function(str) { this.value += str; }; } function FormatingWriter(out, format) { var indent = ['']; this.append = function(str) { out.write(str); return this; }; this.appendNewLine = function() { if (format) { out.write('\n'); } return this; }; this.appendIndent = function() { if (format) { out.write(indent.join(' ')); } return this; }; this.indent = function() { indent.push(''); return this; }; this.unindent = function() { indent.pop(); return this; }; } /** * A writer for meta-model backed document trees * * @param {Object} options output options to pass into the writer */ export function Writer(options) { options = assign({ format: false, preamble: true }, options || {}); function toXML(tree, writer) { var internalWriter = writer || new SavingWriter(); var formatingWriter = new FormatingWriter(internalWriter, options.format); if (options.preamble) { formatingWriter.append(XML_PREAMBLE); } new ElementSerializer().build(tree).serializeTo(formatingWriter); if (!writer) { return internalWriter.value; } } return { toXML: toXML }; }