UNPKG

moddle-xml

Version:

XML import/export for documents described with moddle

1,858 lines (1,403 loc) 42.3 kB
'use strict'; var minDash = require('min-dash'); var saxen = require('saxen'); var moddle = require('moddle'); function hasLowerCaseAlias(pkg) { return pkg.xml && pkg.xml.tagAlias === 'lowerCase'; } var DEFAULT_NS_MAP = { 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xml': 'http://www.w3.org/XML/1998/namespace' }; var SERIALIZE_PROPERTY = 'property'; function getSerialization(element) { return element.xml && element.xml.serialize; } function getSerializationType(element) { const type = getSerialization(element); return type !== SERIALIZE_PROPERTY && (type || null); } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function aliasToName(aliasNs, pkg) { if (!hasLowerCaseAlias(pkg)) { return aliasNs.name; } return aliasNs.prefix + ':' + capitalize(aliasNs.localName); } /** * Un-prefix a potentially prefixed type name. * * @param {NsName} nameNs * @param {Object} [pkg] * * @return {string} */ function prefixedToName(nameNs, pkg) { var name = nameNs.name, localName = nameNs.localName; var typePrefix = pkg && pkg.xml && pkg.xml.typePrefix; if (typePrefix && localName.indexOf(typePrefix) === 0) { return nameNs.prefix + ':' + localName.slice(typePrefix.length); } else { return name; } } function normalizeTypeName(name, nsMap, model) { // normalize against actual NS const nameNs = moddle.parseNameNS(name, nsMap.xmlns); const normalizedName = `${ nsMap[nameNs.prefix] || nameNs.prefix }:${ nameNs.localName }`; const normalizedNameNs = moddle.parseNameNS(normalizedName); // determine actual type name, based on package-defined prefix var pkg = model.getPackage(normalizedNameNs.prefix); return prefixedToName(normalizedNameNs, pkg); } function error(message) { return new Error(message); } /** * Get the moddle descriptor for a given instance or type. * * @param {ModdleElement|Function} element * * @return {Object} the moddle descriptor */ function getModdleDescriptor(element) { return element.$descriptor; } /** * A parse context. * * @class * * @param {Object} options * @param {ElementHandler} options.rootHandler the root handler for parsing a document * @param {boolean} [options.lax=false] whether or not to ignore invalid elements */ function Context(options) { /** * @property {ElementHandler} rootHandler */ /** * @property {Boolean} lax */ minDash.assign(this, options); this.elementsById = {}; this.references = []; this.warnings = []; /** * Add an unresolved reference. * * @param {Object} reference */ this.addReference = function(reference) { this.references.push(reference); }; /** * Add a processed element. * * @param {ModdleElement} element */ this.addElement = function(element) { if (!element) { throw error('expected element'); } var elementsById = this.elementsById; var descriptor = getModdleDescriptor(element); var idProperty = descriptor.idProperty, id; if (idProperty) { id = element.get(idProperty.name); if (id) { // for QName validation as per http://www.w3.org/TR/REC-xml/#NT-NameChar if (!/^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i.test(id)) { throw new Error('illegal ID <' + id + '>'); } if (elementsById[id]) { throw error('duplicate ID <' + id + '>'); } elementsById[id] = element; } } }; /** * Add an import warning. * * @param {Object} warning * @param {String} warning.message * @param {Error} [warning.error] */ this.addWarning = function(warning) { this.warnings.push(warning); }; } function BaseHandler() {} BaseHandler.prototype.handleEnd = function() {}; BaseHandler.prototype.handleText = function() {}; BaseHandler.prototype.handleNode = function() {}; /** * A simple pass through handler that does nothing except for * ignoring all input it receives. * * This is used to ignore unknown elements and * attributes. */ function NoopHandler() { } NoopHandler.prototype = Object.create(BaseHandler.prototype); NoopHandler.prototype.handleNode = function() { return this; }; function BodyHandler() {} BodyHandler.prototype = Object.create(BaseHandler.prototype); BodyHandler.prototype.handleText = function(text) { this.body = (this.body || '') + text; }; function ReferenceHandler(property, context) { this.property = property; this.context = context; } ReferenceHandler.prototype = Object.create(BodyHandler.prototype); ReferenceHandler.prototype.handleNode = function(node) { if (this.element) { throw error('expected no sub nodes'); } else { this.element = this.createReference(node); } return this; }; ReferenceHandler.prototype.handleEnd = function() { this.element.id = this.body; }; ReferenceHandler.prototype.createReference = function(node) { return { property: this.property.ns.name, id: '' }; }; function ValueHandler(propertyDesc, element) { this.element = element; this.propertyDesc = propertyDesc; } ValueHandler.prototype = Object.create(BodyHandler.prototype); ValueHandler.prototype.handleEnd = function() { var value = this.body || '', element = this.element, propertyDesc = this.propertyDesc; value = moddle.coerceType(propertyDesc.type, value); if (propertyDesc.isMany) { element.get(propertyDesc.name).push(value); } else { element.set(propertyDesc.name, value); } }; function BaseElementHandler() {} BaseElementHandler.prototype = Object.create(BodyHandler.prototype); BaseElementHandler.prototype.handleNode = function(node) { var parser = this, element = this.element; if (!element) { element = this.element = this.createElement(node); this.context.addElement(element); } else { parser = this.handleChild(node); } return parser; }; /** * @class Reader.ElementHandler * */ function ElementHandler(model, typeName, context) { this.model = model; this.type = model.getType(typeName); this.context = context; } ElementHandler.prototype = Object.create(BaseElementHandler.prototype); ElementHandler.prototype.addReference = function(reference) { this.context.addReference(reference); }; ElementHandler.prototype.handleText = function(text) { var element = this.element, descriptor = getModdleDescriptor(element), bodyProperty = descriptor.bodyProperty; if (!bodyProperty) { throw error('unexpected body text <' + text + '>'); } BodyHandler.prototype.handleText.call(this, text); }; ElementHandler.prototype.handleEnd = function() { var value = this.body, element = this.element, descriptor = getModdleDescriptor(element), bodyProperty = descriptor.bodyProperty; if (bodyProperty && value !== undefined) { value = moddle.coerceType(bodyProperty.type, value); element.set(bodyProperty.name, value); } }; /** * Create an instance of the model from the given node. * * @param {Element} node the xml node */ ElementHandler.prototype.createElement = function(node) { var attributes = node.attributes, Type = this.type, descriptor = getModdleDescriptor(Type), context = this.context, instance = new Type({}), model = this.model, propNameNs; minDash.forEach(attributes, function(value, name) { var prop = descriptor.propertiesByName[name], values; if (prop && prop.isReference) { if (!prop.isMany) { context.addReference({ element: instance, property: prop.ns.name, id: value }); } else { // IDREFS: parse references as whitespace-separated list values = value.split(' '); minDash.forEach(values, function(v) { context.addReference({ element: instance, property: prop.ns.name, id: v }); }); } } else { if (prop) { value = moddle.coerceType(prop.type, value); } else if (name === 'xmlns') { name = ':' + name; } else { propNameNs = moddle.parseNameNS(name, descriptor.ns.prefix); // check whether attribute is defined in a well-known namespace // if that is the case we emit a warning to indicate potential misuse if (model.getPackage(propNameNs.prefix)) { context.addWarning({ message: 'unknown attribute <' + name + '>', element: instance, property: name, value: value }); } } instance.set(name, value); } }); return instance; }; ElementHandler.prototype.getPropertyForNode = function(node) { var name = node.name; var nameNs = moddle.parseNameNS(name); var type = this.type, model = this.model, descriptor = getModdleDescriptor(type); var propertyName = nameNs.name, property = descriptor.propertiesByName[propertyName]; // search for properties by name first if (property && !property.isAttr) { const serializationType = getSerializationType(property); if (serializationType) { const elementTypeName = node.attributes[serializationType]; // type is optional, if it does not exists the // default type is assumed if (elementTypeName) { // convert the prefix used to the mapped form, but also // take possible type prefixes from XML // into account, i.e.: xsi:type="t{ActualType}", const normalizedTypeName = normalizeTypeName(elementTypeName, node.ns, model); const elementType = model.getType(normalizedTypeName); return minDash.assign({}, property, { effectiveType: getModdleDescriptor(elementType).name }); } } // search for properties by name first return property; } var pkg = model.getPackage(nameNs.prefix); if (pkg) { const elementTypeName = aliasToName(nameNs, pkg); const elementType = model.getType(elementTypeName); // search for collection members later property = minDash.find(descriptor.properties, function(p) { return !p.isVirtual && !p.isReference && !p.isAttribute && elementType.hasType(p.type); }); if (property) { return minDash.assign({}, property, { effectiveType: getModdleDescriptor(elementType).name }); } } else { // parse unknown element (maybe extension) property = minDash.find(descriptor.properties, function(p) { return !p.isReference && !p.isAttribute && p.type === 'Element'; }); if (property) { return property; } } throw error('unrecognized element <' + nameNs.name + '>'); }; ElementHandler.prototype.toString = function() { return 'ElementDescriptor[' + getModdleDescriptor(this.type).name + ']'; }; ElementHandler.prototype.valueHandler = function(propertyDesc, element) { return new ValueHandler(propertyDesc, element); }; ElementHandler.prototype.referenceHandler = function(propertyDesc) { return new ReferenceHandler(propertyDesc, this.context); }; ElementHandler.prototype.handler = function(type) { if (type === 'Element') { return new GenericElementHandler(this.model, type, this.context); } else { return new ElementHandler(this.model, type, this.context); } }; /** * Handle the child element parsing * * @param {Element} node the xml node */ ElementHandler.prototype.handleChild = function(node) { var propertyDesc, type, element, childHandler; propertyDesc = this.getPropertyForNode(node); element = this.element; type = propertyDesc.effectiveType || propertyDesc.type; if (moddle.isSimpleType(type)) { return this.valueHandler(propertyDesc, element); } if (propertyDesc.isReference) { childHandler = this.referenceHandler(propertyDesc).handleNode(node); } else { childHandler = this.handler(type).handleNode(node); } var newElement = childHandler.element; // child handles may decide to skip elements // by not returning anything if (newElement !== undefined) { if (propertyDesc.isMany) { element.get(propertyDesc.name).push(newElement); } else { element.set(propertyDesc.name, newElement); } if (propertyDesc.isReference) { minDash.assign(newElement, { element: element }); this.context.addReference(newElement); } else { // establish child -> parent relationship newElement.$parent = element; } } return childHandler; }; /** * An element handler that performs special validation * to ensure the node it gets initialized with matches * the handlers type (namespace wise). * * @param {Moddle} model * @param {String} typeName * @param {Context} context */ function RootElementHandler(model, typeName, context) { ElementHandler.call(this, model, typeName, context); } RootElementHandler.prototype = Object.create(ElementHandler.prototype); RootElementHandler.prototype.createElement = function(node) { var name = node.name, nameNs = moddle.parseNameNS(name), model = this.model, type = this.type, pkg = model.getPackage(nameNs.prefix), typeName = pkg && aliasToName(nameNs, pkg) || name; // verify the correct namespace if we parse // the first element in the handler tree // // this ensures we don't mistakenly import wrong namespace elements if (!type.hasType(typeName)) { throw error('unexpected element <' + node.originalName + '>'); } return ElementHandler.prototype.createElement.call(this, node); }; function GenericElementHandler(model, typeName, context) { this.model = model; this.context = context; } GenericElementHandler.prototype = Object.create(BaseElementHandler.prototype); GenericElementHandler.prototype.createElement = function(node) { var name = node.name, ns = moddle.parseNameNS(name), prefix = ns.prefix, uri = node.ns[prefix + '$uri'], attributes = node.attributes; return this.model.createAny(name, uri, attributes); }; GenericElementHandler.prototype.handleChild = function(node) { var handler = new GenericElementHandler(this.model, 'Element', this.context).handleNode(node), element = this.element; var newElement = handler.element, children; if (newElement !== undefined) { children = element.$children = element.$children || []; children.push(newElement); // establish child -> parent relationship newElement.$parent = element; } return handler; }; GenericElementHandler.prototype.handleEnd = function() { if (this.body) { this.element.$body = this.body; } }; /** * A reader for a meta-model * * @param {Object} options * @param {Model} options.model used to read xml files * @param {Boolean} options.lax whether to make parse errors warnings */ function Reader(options) { if (options instanceof moddle.Moddle) { options = { model: options }; } minDash.assign(this, { lax: false }, options); } /** * The fromXML result. * * @typedef {Object} ParseResult * * @property {ModdleElement} rootElement * @property {Array<Object>} references * @property {Array<Error>} warnings * @property {Object} elementsById - a mapping containing each ID -> ModdleElement */ /** * The fromXML result. * * @typedef {Error} ParseError * * @property {Array<Error>} warnings */ /** * Parse the given XML into a moddle document tree. * * @param {String} xml * @param {ElementHandler|Object} options or rootHandler * * @returns {Promise<ParseResult, ParseError>} */ Reader.prototype.fromXML = function(xml, options, done) { var rootHandler = options.rootHandler; if (options instanceof ElementHandler) { // root handler passed via (xml, { rootHandler: ElementHandler }, ...) rootHandler = options; options = {}; } else { if (typeof options === 'string') { // rootHandler passed via (xml, 'someString', ...) rootHandler = this.handler(options); options = {}; } else if (typeof rootHandler === 'string') { // rootHandler passed via (xml, { rootHandler: 'someString' }, ...) rootHandler = this.handler(rootHandler); } } var model = this.model, lax = this.lax; var context = new Context(minDash.assign({}, options, { rootHandler: rootHandler })), parser = new saxen.Parser({ proxy: true }), stack = createStack(); rootHandler.context = context; // push root handler stack.push(rootHandler); /** * Handle error. * * @param {Error} err * @param {Function} getContext * @param {boolean} lax * * @return {boolean} true if handled */ function handleError(err, getContext, lax) { var ctx = getContext(); var line = ctx.line, column = ctx.column, data = ctx.data; // we receive the full context data here, // for elements trim down the information // to the tag name, only if (data.charAt(0) === '<' && data.indexOf(' ') !== -1) { data = data.slice(0, data.indexOf(' ')) + '>'; } var message = 'unparsable content ' + (data ? data + ' ' : '') + 'detected\n\t' + 'line: ' + line + '\n\t' + 'column: ' + column + '\n\t' + 'nested error: ' + err.message; if (lax) { context.addWarning({ message: message, error: err }); return true; } else { throw error(message); } } function handleWarning(err, getContext) { // just like handling errors in <lax=true> mode return handleError(err, getContext, true); } /** * Resolve collected references on parse end. */ function resolveReferences() { var elementsById = context.elementsById; var references = context.references; var i, r; for (i = 0; (r = references[i]); i++) { var element = r.element; var reference = elementsById[r.id]; var property = getModdleDescriptor(element).propertiesByName[r.property]; if (!reference) { context.addWarning({ message: 'unresolved reference <' + r.id + '>', element: r.element, property: r.property, value: r.id }); } if (property.isMany) { var collection = element.get(property.name), idx = collection.indexOf(r); // we replace an existing place holder (idx != -1) or // append to the collection instead if (idx === -1) { idx = collection.length; } if (!reference) { // remove unresolvable reference collection.splice(idx, 1); } else { // add or update reference in collection collection[idx] = reference; } } else { element.set(property.name, reference); } } } function handleClose() { stack.pop().handleEnd(); } var PREAMBLE_START_PATTERN = /^<\?xml /i; var ENCODING_PATTERN = / encoding="([^"]+)"/i; var UTF_8_PATTERN = /^utf-8$/i; function handleQuestion(question) { if (!PREAMBLE_START_PATTERN.test(question)) { return; } var match = ENCODING_PATTERN.exec(question); var encoding = match && match[1]; if (!encoding || UTF_8_PATTERN.test(encoding)) { return; } context.addWarning({ message: 'unsupported document encoding <' + encoding + '>, ' + 'falling back to UTF-8' }); } function handleOpen(node, getContext) { var handler = stack.peek(); try { stack.push(handler.handleNode(node)); } catch (err) { if (handleError(err, getContext, lax)) { stack.push(new NoopHandler()); } } } function handleCData(text, getContext) { try { stack.peek().handleText(text); } catch (err) { handleWarning(err, getContext); } } function handleText(text, getContext) { // strip whitespace only nodes, i.e. before // <!CDATA[ ... ]> sections and in between tags if (!text.trim()) { return; } handleCData(text, getContext); } var uriMap = model.getPackages().reduce(function(uriMap, p) { uriMap[p.uri] = p.prefix; return uriMap; }, Object.entries(DEFAULT_NS_MAP).reduce(function(map, [ prefix, url ]) { map[url] = prefix; return map; }, model.config && model.config.nsMap || {})); parser .ns(uriMap) .on('openTag', function(obj, decodeStr, selfClosing, getContext) { // gracefully handle unparsable attributes (attrs=false) var attrs = obj.attrs || {}; var decodedAttrs = Object.keys(attrs).reduce(function(d, key) { var value = decodeStr(attrs[key]); d[key] = value; return d; }, {}); var node = { name: obj.name, originalName: obj.originalName, attributes: decodedAttrs, ns: obj.ns }; handleOpen(node, getContext); }) .on('question', handleQuestion) .on('closeTag', handleClose) .on('cdata', handleCData) .on('text', function(text, decodeEntities, getContext) { handleText(decodeEntities(text), getContext); }) .on('error', handleError) .on('warn', handleWarning); // async XML parsing to make sure the execution environment // (node or brower) is kept responsive and that certain optimization // strategies can kick in. return new Promise(function(resolve, reject) { var err; try { parser.parse(xml); resolveReferences(); } catch (e) { err = e; } var rootElement = rootHandler.element; if (!err && !rootElement) { err = error('failed to parse document as <' + rootHandler.type.$descriptor.name + '>'); } var warnings = context.warnings; var references = context.references; var elementsById = context.elementsById; if (err) { err.warnings = warnings; return reject(err); } else { return resolve({ rootElement: rootElement, elementsById: elementsById, references: references, warnings: warnings }); } }); }; Reader.prototype.handler = function(name) { return new RootElementHandler(this.model, name); }; // helpers ////////////////////////// function createStack() { var stack = []; Object.defineProperty(stack, 'peek', { value: function() { return this[this.length - 1]; } }); return stack; } var XML_PREAMBLE = '<?xml version="1.0" encoding="UTF-8"?>\n'; var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g; var ESCAPE_CHARS = /<|>|&/g; function Namespaces(parent) { this.prefixMap = {}; this.uriMap = {}; this.used = {}; this.wellknown = []; this.custom = []; this.parent = parent; this.defaultPrefixMap = parent && parent.defaultPrefixMap || {}; } Namespaces.prototype.mapDefaultPrefixes = function(defaultPrefixMap) { this.defaultPrefixMap = defaultPrefixMap; }; Namespaces.prototype.defaultUriByPrefix = function(prefix) { return this.defaultPrefixMap[prefix]; }; Namespaces.prototype.byUri = function(uri) { return this.uriMap[uri] || ( this.parent && this.parent.byUri(uri) ); }; Namespaces.prototype.add = function(ns, isWellknown) { this.uriMap[ns.uri] = ns; if (isWellknown) { this.wellknown.push(ns); } else { this.custom.push(ns); } this.mapPrefix(ns.prefix, ns.uri); }; Namespaces.prototype.uriByPrefix = function(prefix) { return this.prefixMap[prefix || 'xmlns'] || ( this.parent && this.parent.uriByPrefix(prefix) ); }; Namespaces.prototype.mapPrefix = function(prefix, uri) { this.prefixMap[prefix || 'xmlns'] = uri; }; Namespaces.prototype.getNSKey = function(ns) { return (ns.prefix !== undefined) ? (ns.uri + '|' + ns.prefix) : ns.uri; }; Namespaces.prototype.logUsed = function(ns) { var uri = ns.uri; var nsKey = this.getNSKey(ns); this.used[nsKey] = this.byUri(uri); // Inform parent recursively about the usage of this NS if (this.parent) { this.parent.logUsed(ns); } }; Namespaces.prototype.getUsed = function(ns) { var allNs = [].concat(this.wellknown, this.custom); return allNs.filter(ns => { var nsKey = this.getNSKey(ns); return this.used[nsKey]; }); }; 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 (minDash.isString(ns)) { return ns; } else { return (ns.prefix ? ns.prefix + ':' : '') + ns.localName; } } function getNsAttrs(namespaces) { return namespaces.getUsed().filter(function(ns) { // do not serialize built in <xml> namespace return ns.prefix !== 'xml'; }).map(function(ns) { var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : ''); return { name: name, value: ns.uri }; }); } function getElementNs(ns, descriptor) { if (descriptor.isGeneric) { return minDash.assign({ localName: descriptor.ns.localName }, ns); } else { return minDash.assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns); } } function getPropertyNs(ns, descriptor) { return minDash.assign({ localName: descriptor.ns.localName }, ns); } function getSerializableProperties(element) { var descriptor = element.$descriptor; return minDash.filter(descriptor.properties, function(p) { var name = p.name; if (p.isVirtual) { return false; } // do not serialize defaults if (!minDash.has(element, 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 = minDash.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 minDash.filter(props, function(p) { return p.isAttr; }); } function filterContained(props) { return minDash.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 + '</' + this.tagName + '>') .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('</' + this.tagName + '>') .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.parseGenericNsAttributes(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) { this.parseGenericContainments(element); } else { 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 (minDash.isString(element)) { ns = moddle.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 minDash.assign({ localName: ns.localName }, effectiveNs); } }; ElementSerializer.prototype.parseGenericNsAttributes = function(element) { return Object.entries(element).filter( ([ key, value ]) => !key.startsWith('$') && this.parseNsAttribute(element, key, value) ).map( ([ key, value ]) => ({ name: key, value: value }) ); }; ElementSerializer.prototype.parseGenericContainments = function(element) { var body = element.$body; if (body) { this.body.push(new BodySerializer().build({ type: 'String' }, body)); } var children = element.$children; if (children) { minDash.forEach(children, child => { this.body.push(new ElementSerializer(this).build(child)); }); } }; ElementSerializer.prototype.parseNsAttribute = function(element, name, value) { var model = element.$model; var nameNs = moddle.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<Object>} */ ElementSerializer.prototype.parseNsAttributes = function(element) { 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 minDash.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; minDash.forEach(attributes, function(attr) { try { self.addAttribute(self.nsAttributeName(attr.name), attr.value); } catch (e) { // eslint-disable-next-line no-undef typeof console !== 'undefined' && 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; minDash.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 (moddle.isSimpleType(p.type)) { minDash.forEach(value, function(v) { body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v)); }); } else if (isReference) { minDash.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 serialization = getSerialization(p); minDash.forEach(value, function(v) { var serializer; if (serialization) { if (serialization === SERIALIZE_PROPERTY) { serializer = new ElementSerializer(self, p); } else { serializer = new TypeSerializer(self, p, serialization); } } 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 || local) { namespaces.add(ns, wellknown); } namespaces.mapPrefix(nsPrefix, nsUri); return ns; }; ElementSerializer.prototype.logNamespaceUsed = function(ns, local) { var 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 = namespaces.defaultUriByPrefix(prefix); uri = uri || wellknownUri || namespaces.uriByPrefix(prefix); if (!uri) { throw new Error('no namespace uri given for prefix <' + prefix + '>'); } ns = namespaces.byUri(uri); // register new default prefix <xmlns> in local scope if (!ns && !prefix) { ns = this.logNamespace({ uri }, wellknownUri === uri, true); } 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; minDash.forEach(properties, function(p) { var value = element.get(p.name); if (p.isReference) { if (!p.isMany) { value = value.id; } else { var values = []; minDash.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 (minDash.isString(value)) { value = escapeAttr(value); } // de-duplicate attributes // https://github.com/bpmn-io/moddle-xml/issues/66 var idx = minDash.findIndex(attrs, function(element) { return ( element.name.localName === name.localName && element.name.uri === name.uri && element.name.prefix === name.prefix ); }); var attr = { name: name, value: value }; if (idx !== -1) { attrs.splice(idx, 1, attr); } else { attrs.push(attr); } }; ElementSerializer.prototype.serializeAttributes = function(writer) { var attrs = this.attrs, namespaces = this.namespaces; if (namespaces) { attrs = getNsAttrs(namespaces).concat(attrs); } minDash.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(); } minDash.forEach(this.body, function(b) { b.serializeTo(writer); }); if (indent) { writer .unindent() .appendIndent(); } writer.append('</' + this.tagName + '>'); } writer.appendNewLine(); }; /** * A serializer for types that handles serialization of data types */ function TypeSerializer(parent, propertyDescriptor, serialization) { ElementSerializer.call(this, parent, propertyDescriptor); this.serialization = serialization; } inherits(TypeSerializer, ElementSerializer); TypeSerializer.prototype.parseNsAttributes = function(element) { // extracted attributes with serialization attribute // <type=typeName> stripped; it may be later var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element).filter( attr => attr.name !== this.serialization ); var descriptor = element.$descriptor; // only serialize <type=typeName> 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(this.serialization), (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 */ function Writer(options) { options = minDash.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); } var serializer = new ElementSerializer(); var model = tree.$model; serializer.getNamespaces().mapDefaultPrefixes(getDefaultPrefixMappings(model)); serializer.build(tree).serializeTo(formatingWriter); if (!writer) { return internalWriter.value; } } return { toXML: toXML }; } // helpers /////////// /** * @param {Moddle} model * * @return { Record<string, string> } map from prefix to URI */ function getDefaultPrefixMappings(model) { const nsMap = model.config && model.config.nsMap || {}; const prefixMap = {}; // { prefix -> uri } for (const prefix in DEFAULT_NS_MAP) { prefixMap[prefix] = DEFAULT_NS_MAP[prefix]; } // { uri -> prefix } for (const uri in nsMap) { const prefix = nsMap[uri]; prefixMap[prefix] = uri; } for (const pkg of model.getPackages()) { prefixMap[pkg.prefix] = pkg.uri; } return prefixMap; } exports.Reader = Reader; exports.Writer = Writer; //# sourceMappingURL=index.cjs.map