123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840 |
- 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 = '<?xml version="1.0" encoding="UTF-8"?>\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 + '</' + 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.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<Object>}
- */
- 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('</' + this.tagName + '>');
- }
- 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
- };
- }
|