write.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. import {
  2. map,
  3. forEach,
  4. isString,
  5. filter,
  6. assign
  7. } from 'min-dash';
  8. import {
  9. isSimple as isSimpleType
  10. } from 'moddle/lib/types';
  11. import {
  12. parseName as parseNameNs
  13. } from 'moddle/lib/ns';
  14. import {
  15. hasLowerCaseAlias,
  16. serializeAsType,
  17. serializeAsProperty,
  18. DEFAULT_NS_MAP,
  19. XSI_TYPE
  20. } from './common';
  21. var XML_PREAMBLE = '<?xml version="1.0" encoding="UTF-8"?>\n';
  22. var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g;
  23. var ESCAPE_CHARS = /<|>|&/g;
  24. export function Namespaces(parent) {
  25. var prefixMap = {};
  26. var uriMap = {};
  27. var used = {};
  28. var wellknown = [];
  29. var custom = [];
  30. // API
  31. this.byUri = function(uri) {
  32. return uriMap[uri] || (
  33. parent && parent.byUri(uri)
  34. );
  35. };
  36. this.add = function(ns, isWellknown) {
  37. uriMap[ns.uri] = ns;
  38. if (isWellknown) {
  39. wellknown.push(ns);
  40. } else {
  41. custom.push(ns);
  42. }
  43. this.mapPrefix(ns.prefix, ns.uri);
  44. };
  45. this.uriByPrefix = function(prefix) {
  46. return prefixMap[prefix || 'xmlns'];
  47. };
  48. this.mapPrefix = function(prefix, uri) {
  49. prefixMap[prefix || 'xmlns'] = uri;
  50. };
  51. this.logUsed = function(ns) {
  52. var uri = ns.uri;
  53. used[uri] = this.byUri(uri);
  54. };
  55. this.getUsed = function(ns) {
  56. function isUsed(ns) {
  57. return used[ns.uri];
  58. }
  59. var allNs = [].concat(wellknown, custom);
  60. return allNs.filter(isUsed);
  61. };
  62. }
  63. function lower(string) {
  64. return string.charAt(0).toLowerCase() + string.slice(1);
  65. }
  66. function nameToAlias(name, pkg) {
  67. if (hasLowerCaseAlias(pkg)) {
  68. return lower(name);
  69. } else {
  70. return name;
  71. }
  72. }
  73. function inherits(ctor, superCtor) {
  74. ctor.super_ = superCtor;
  75. ctor.prototype = Object.create(superCtor.prototype, {
  76. constructor: {
  77. value: ctor,
  78. enumerable: false,
  79. writable: true,
  80. configurable: true
  81. }
  82. });
  83. }
  84. function nsName(ns) {
  85. if (isString(ns)) {
  86. return ns;
  87. } else {
  88. return (ns.prefix ? ns.prefix + ':' : '') + ns.localName;
  89. }
  90. }
  91. function getNsAttrs(namespaces) {
  92. return map(namespaces.getUsed(), function(ns) {
  93. var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : '');
  94. return { name: name, value: ns.uri };
  95. });
  96. }
  97. function getElementNs(ns, descriptor) {
  98. if (descriptor.isGeneric) {
  99. return assign({ localName: descriptor.ns.localName }, ns);
  100. } else {
  101. return assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns);
  102. }
  103. }
  104. function getPropertyNs(ns, descriptor) {
  105. return assign({ localName: descriptor.ns.localName }, ns);
  106. }
  107. function getSerializableProperties(element) {
  108. var descriptor = element.$descriptor;
  109. return filter(descriptor.properties, function(p) {
  110. var name = p.name;
  111. if (p.isVirtual) {
  112. return false;
  113. }
  114. // do not serialize defaults
  115. if (!element.hasOwnProperty(name)) {
  116. return false;
  117. }
  118. var value = element[name];
  119. // do not serialize default equals
  120. if (value === p.default) {
  121. return false;
  122. }
  123. // do not serialize null properties
  124. if (value === null) {
  125. return false;
  126. }
  127. return p.isMany ? value.length : true;
  128. });
  129. }
  130. var ESCAPE_ATTR_MAP = {
  131. '\n': '#10',
  132. '\n\r': '#10',
  133. '"': '#34',
  134. '\'': '#39',
  135. '<': '#60',
  136. '>': '#62',
  137. '&': '#38'
  138. };
  139. var ESCAPE_MAP = {
  140. '<': 'lt',
  141. '>': 'gt',
  142. '&': 'amp'
  143. };
  144. function escape(str, charPattern, replaceMap) {
  145. // ensure we are handling strings here
  146. str = isString(str) ? str : '' + str;
  147. return str.replace(charPattern, function(s) {
  148. return '&' + replaceMap[s] + ';';
  149. });
  150. }
  151. /**
  152. * Escape a string attribute to not contain any bad values (line breaks, '"', ...)
  153. *
  154. * @param {String} str the string to escape
  155. * @return {String} the escaped string
  156. */
  157. function escapeAttr(str) {
  158. return escape(str, ESCAPE_ATTR_CHARS, ESCAPE_ATTR_MAP);
  159. }
  160. function escapeBody(str) {
  161. return escape(str, ESCAPE_CHARS, ESCAPE_MAP);
  162. }
  163. function filterAttributes(props) {
  164. return filter(props, function(p) { return p.isAttr; });
  165. }
  166. function filterContained(props) {
  167. return filter(props, function(p) { return !p.isAttr; });
  168. }
  169. function ReferenceSerializer(tagName) {
  170. this.tagName = tagName;
  171. }
  172. ReferenceSerializer.prototype.build = function(element) {
  173. this.element = element;
  174. return this;
  175. };
  176. ReferenceSerializer.prototype.serializeTo = function(writer) {
  177. writer
  178. .appendIndent()
  179. .append('<' + this.tagName + '>' + this.element.id + '</' + this.tagName + '>')
  180. .appendNewLine();
  181. };
  182. function BodySerializer() {}
  183. BodySerializer.prototype.serializeValue =
  184. BodySerializer.prototype.serializeTo = function(writer) {
  185. writer.append(
  186. this.escape
  187. ? escapeBody(this.value)
  188. : this.value
  189. );
  190. };
  191. BodySerializer.prototype.build = function(prop, value) {
  192. this.value = value;
  193. if (prop.type === 'String' && value.search(ESCAPE_CHARS) !== -1) {
  194. this.escape = true;
  195. }
  196. return this;
  197. };
  198. function ValueSerializer(tagName) {
  199. this.tagName = tagName;
  200. }
  201. inherits(ValueSerializer, BodySerializer);
  202. ValueSerializer.prototype.serializeTo = function(writer) {
  203. writer
  204. .appendIndent()
  205. .append('<' + this.tagName + '>');
  206. this.serializeValue(writer);
  207. writer
  208. .append('</' + this.tagName + '>')
  209. .appendNewLine();
  210. };
  211. function ElementSerializer(parent, propertyDescriptor) {
  212. this.body = [];
  213. this.attrs = [];
  214. this.parent = parent;
  215. this.propertyDescriptor = propertyDescriptor;
  216. }
  217. ElementSerializer.prototype.build = function(element) {
  218. this.element = element;
  219. var elementDescriptor = element.$descriptor,
  220. propertyDescriptor = this.propertyDescriptor;
  221. var otherAttrs,
  222. properties;
  223. var isGeneric = elementDescriptor.isGeneric;
  224. if (isGeneric) {
  225. otherAttrs = this.parseGeneric(element);
  226. } else {
  227. otherAttrs = this.parseNsAttributes(element);
  228. }
  229. if (propertyDescriptor) {
  230. this.ns = this.nsPropertyTagName(propertyDescriptor);
  231. } else {
  232. this.ns = this.nsTagName(elementDescriptor);
  233. }
  234. // compute tag name
  235. this.tagName = this.addTagName(this.ns);
  236. if (!isGeneric) {
  237. properties = getSerializableProperties(element);
  238. this.parseAttributes(filterAttributes(properties));
  239. this.parseContainments(filterContained(properties));
  240. }
  241. this.parseGenericAttributes(element, otherAttrs);
  242. return this;
  243. };
  244. ElementSerializer.prototype.nsTagName = function(descriptor) {
  245. var effectiveNs = this.logNamespaceUsed(descriptor.ns);
  246. return getElementNs(effectiveNs, descriptor);
  247. };
  248. ElementSerializer.prototype.nsPropertyTagName = function(descriptor) {
  249. var effectiveNs = this.logNamespaceUsed(descriptor.ns);
  250. return getPropertyNs(effectiveNs, descriptor);
  251. };
  252. ElementSerializer.prototype.isLocalNs = function(ns) {
  253. return ns.uri === this.ns.uri;
  254. };
  255. /**
  256. * Get the actual ns attribute name for the given element.
  257. *
  258. * @param {Object} element
  259. * @param {Boolean} [element.inherited=false]
  260. *
  261. * @return {Object} nsName
  262. */
  263. ElementSerializer.prototype.nsAttributeName = function(element) {
  264. var ns;
  265. if (isString(element)) {
  266. ns = parseNameNs(element);
  267. } else {
  268. ns = element.ns;
  269. }
  270. // return just local name for inherited attributes
  271. if (element.inherited) {
  272. return { localName: ns.localName };
  273. }
  274. // parse + log effective ns
  275. var effectiveNs = this.logNamespaceUsed(ns);
  276. // LOG ACTUAL namespace use
  277. this.getNamespaces().logUsed(effectiveNs);
  278. // strip prefix if same namespace like parent
  279. if (this.isLocalNs(effectiveNs)) {
  280. return { localName: ns.localName };
  281. } else {
  282. return assign({ localName: ns.localName }, effectiveNs);
  283. }
  284. };
  285. ElementSerializer.prototype.parseGeneric = function(element) {
  286. var self = this,
  287. body = this.body;
  288. var attributes = [];
  289. forEach(element, function(val, key) {
  290. var nonNsAttr;
  291. if (key === '$body') {
  292. body.push(new BodySerializer().build({ type: 'String' }, val));
  293. } else
  294. if (key === '$children') {
  295. forEach(val, function(child) {
  296. body.push(new ElementSerializer(self).build(child));
  297. });
  298. } else
  299. if (key.indexOf('$') !== 0) {
  300. nonNsAttr = self.parseNsAttribute(element, key, val);
  301. if (nonNsAttr) {
  302. attributes.push({ name: key, value: val });
  303. }
  304. }
  305. });
  306. return attributes;
  307. };
  308. ElementSerializer.prototype.parseNsAttribute = function(element, name, value) {
  309. var model = element.$model;
  310. var nameNs = parseNameNs(name);
  311. var ns;
  312. // parse xmlns:foo="http://foo.bar"
  313. if (nameNs.prefix === 'xmlns') {
  314. ns = { prefix: nameNs.localName, uri: value };
  315. }
  316. // parse xmlns="http://foo.bar"
  317. if (!nameNs.prefix && nameNs.localName === 'xmlns') {
  318. ns = { uri: value };
  319. }
  320. if (!ns) {
  321. return {
  322. name: name,
  323. value: value
  324. };
  325. }
  326. if (model && model.getPackage(value)) {
  327. // register well known namespace
  328. this.logNamespace(ns, true, true);
  329. } else {
  330. // log custom namespace directly as used
  331. var actualNs = this.logNamespaceUsed(ns, true);
  332. this.getNamespaces().logUsed(actualNs);
  333. }
  334. };
  335. /**
  336. * Parse namespaces and return a list of left over generic attributes
  337. *
  338. * @param {Object} element
  339. * @return {Array<Object>}
  340. */
  341. ElementSerializer.prototype.parseNsAttributes = function(element, attrs) {
  342. var self = this;
  343. var genericAttrs = element.$attrs;
  344. var attributes = [];
  345. // parse namespace attributes first
  346. // and log them. push non namespace attributes to a list
  347. // and process them later
  348. forEach(genericAttrs, function(value, name) {
  349. var nonNsAttr = self.parseNsAttribute(element, name, value);
  350. if (nonNsAttr) {
  351. attributes.push(nonNsAttr);
  352. }
  353. });
  354. return attributes;
  355. };
  356. ElementSerializer.prototype.parseGenericAttributes = function(element, attributes) {
  357. var self = this;
  358. forEach(attributes, function(attr) {
  359. // do not serialize xsi:type attribute
  360. // it is set manually based on the actual implementation type
  361. if (attr.name === XSI_TYPE) {
  362. return;
  363. }
  364. try {
  365. self.addAttribute(self.nsAttributeName(attr.name), attr.value);
  366. } catch (e) {
  367. console.warn(
  368. 'missing namespace information for ',
  369. attr.name, '=', attr.value, 'on', element,
  370. e);
  371. }
  372. });
  373. };
  374. ElementSerializer.prototype.parseContainments = function(properties) {
  375. var self = this,
  376. body = this.body,
  377. element = this.element;
  378. forEach(properties, function(p) {
  379. var value = element.get(p.name),
  380. isReference = p.isReference,
  381. isMany = p.isMany;
  382. if (!isMany) {
  383. value = [ value ];
  384. }
  385. if (p.isBody) {
  386. body.push(new BodySerializer().build(p, value[0]));
  387. } else
  388. if (isSimpleType(p.type)) {
  389. forEach(value, function(v) {
  390. body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v));
  391. });
  392. } else
  393. if (isReference) {
  394. forEach(value, function(v) {
  395. body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v));
  396. });
  397. } else {
  398. // allow serialization via type
  399. // rather than element name
  400. var asType = serializeAsType(p),
  401. asProperty = serializeAsProperty(p);
  402. forEach(value, function(v) {
  403. var serializer;
  404. if (asType) {
  405. serializer = new TypeSerializer(self, p);
  406. } else
  407. if (asProperty) {
  408. serializer = new ElementSerializer(self, p);
  409. } else {
  410. serializer = new ElementSerializer(self);
  411. }
  412. body.push(serializer.build(v));
  413. });
  414. }
  415. });
  416. };
  417. ElementSerializer.prototype.getNamespaces = function(local) {
  418. var namespaces = this.namespaces,
  419. parent = this.parent,
  420. parentNamespaces;
  421. if (!namespaces) {
  422. parentNamespaces = parent && parent.getNamespaces();
  423. if (local || !parentNamespaces) {
  424. this.namespaces = namespaces = new Namespaces(parentNamespaces);
  425. } else {
  426. namespaces = parentNamespaces;
  427. }
  428. }
  429. return namespaces;
  430. };
  431. ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) {
  432. var namespaces = this.getNamespaces(local);
  433. var nsUri = ns.uri,
  434. nsPrefix = ns.prefix;
  435. var existing = namespaces.byUri(nsUri);
  436. if (!existing) {
  437. namespaces.add(ns, wellknown);
  438. }
  439. namespaces.mapPrefix(nsPrefix, nsUri);
  440. return ns;
  441. };
  442. ElementSerializer.prototype.logNamespaceUsed = function(ns, local) {
  443. var element = this.element,
  444. model = element.$model,
  445. namespaces = this.getNamespaces(local);
  446. // ns may be
  447. //
  448. // * prefix only
  449. // * prefix:uri
  450. // * localName only
  451. var prefix = ns.prefix,
  452. uri = ns.uri,
  453. newPrefix, idx,
  454. wellknownUri;
  455. // handle anonymous namespaces (elementForm=unqualified), cf. #23
  456. if (!prefix && !uri) {
  457. return { localName: ns.localName };
  458. }
  459. wellknownUri = DEFAULT_NS_MAP[prefix] || model && (model.getPackage(prefix) || {}).uri;
  460. uri = uri || wellknownUri || namespaces.uriByPrefix(prefix);
  461. if (!uri) {
  462. throw new Error('no namespace uri given for prefix <' + prefix + '>');
  463. }
  464. ns = namespaces.byUri(uri);
  465. if (!ns) {
  466. newPrefix = prefix;
  467. idx = 1;
  468. // find a prefix that is not mapped yet
  469. while (namespaces.uriByPrefix(newPrefix)) {
  470. newPrefix = prefix + '_' + idx++;
  471. }
  472. ns = this.logNamespace({ prefix: newPrefix, uri: uri }, wellknownUri === uri);
  473. }
  474. if (prefix) {
  475. namespaces.mapPrefix(prefix, uri);
  476. }
  477. return ns;
  478. };
  479. ElementSerializer.prototype.parseAttributes = function(properties) {
  480. var self = this,
  481. element = this.element;
  482. forEach(properties, function(p) {
  483. var value = element.get(p.name);
  484. if (p.isReference) {
  485. if (!p.isMany) {
  486. value = value.id;
  487. }
  488. else {
  489. var values = [];
  490. forEach(value, function(v) {
  491. values.push(v.id);
  492. });
  493. // IDREFS is a whitespace-separated list of references.
  494. value = values.join(' ');
  495. }
  496. }
  497. self.addAttribute(self.nsAttributeName(p), value);
  498. });
  499. };
  500. ElementSerializer.prototype.addTagName = function(nsTagName) {
  501. var actualNs = this.logNamespaceUsed(nsTagName);
  502. this.getNamespaces().logUsed(actualNs);
  503. return nsName(nsTagName);
  504. };
  505. ElementSerializer.prototype.addAttribute = function(name, value) {
  506. var attrs = this.attrs;
  507. if (isString(value)) {
  508. value = escapeAttr(value);
  509. }
  510. attrs.push({ name: name, value: value });
  511. };
  512. ElementSerializer.prototype.serializeAttributes = function(writer) {
  513. var attrs = this.attrs,
  514. namespaces = this.namespaces;
  515. if (namespaces) {
  516. attrs = getNsAttrs(namespaces).concat(attrs);
  517. }
  518. forEach(attrs, function(a) {
  519. writer
  520. .append(' ')
  521. .append(nsName(a.name)).append('="').append(a.value).append('"');
  522. });
  523. };
  524. ElementSerializer.prototype.serializeTo = function(writer) {
  525. var firstBody = this.body[0],
  526. indent = firstBody && firstBody.constructor !== BodySerializer;
  527. writer
  528. .appendIndent()
  529. .append('<' + this.tagName);
  530. this.serializeAttributes(writer);
  531. writer.append(firstBody ? '>' : ' />');
  532. if (firstBody) {
  533. if (indent) {
  534. writer
  535. .appendNewLine()
  536. .indent();
  537. }
  538. forEach(this.body, function(b) {
  539. b.serializeTo(writer);
  540. });
  541. if (indent) {
  542. writer
  543. .unindent()
  544. .appendIndent();
  545. }
  546. writer.append('</' + this.tagName + '>');
  547. }
  548. writer.appendNewLine();
  549. };
  550. /**
  551. * A serializer for types that handles serialization of data types
  552. */
  553. function TypeSerializer(parent, propertyDescriptor) {
  554. ElementSerializer.call(this, parent, propertyDescriptor);
  555. }
  556. inherits(TypeSerializer, ElementSerializer);
  557. TypeSerializer.prototype.parseNsAttributes = function(element) {
  558. // extracted attributes
  559. var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element);
  560. var descriptor = element.$descriptor;
  561. // only serialize xsi:type if necessary
  562. if (descriptor.name === this.propertyDescriptor.type) {
  563. return attributes;
  564. }
  565. var typeNs = this.typeNs = this.nsTagName(descriptor);
  566. this.getNamespaces().logUsed(this.typeNs);
  567. // add xsi:type attribute to represent the elements
  568. // actual type
  569. var pkg = element.$model.getPackage(typeNs.uri),
  570. typePrefix = (pkg.xml && pkg.xml.typePrefix) || '';
  571. this.addAttribute(
  572. this.nsAttributeName(XSI_TYPE),
  573. (typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName
  574. );
  575. return attributes;
  576. };
  577. TypeSerializer.prototype.isLocalNs = function(ns) {
  578. return ns.uri === (this.typeNs || this.ns).uri;
  579. };
  580. function SavingWriter() {
  581. this.value = '';
  582. this.write = function(str) {
  583. this.value += str;
  584. };
  585. }
  586. function FormatingWriter(out, format) {
  587. var indent = [''];
  588. this.append = function(str) {
  589. out.write(str);
  590. return this;
  591. };
  592. this.appendNewLine = function() {
  593. if (format) {
  594. out.write('\n');
  595. }
  596. return this;
  597. };
  598. this.appendIndent = function() {
  599. if (format) {
  600. out.write(indent.join(' '));
  601. }
  602. return this;
  603. };
  604. this.indent = function() {
  605. indent.push('');
  606. return this;
  607. };
  608. this.unindent = function() {
  609. indent.pop();
  610. return this;
  611. };
  612. }
  613. /**
  614. * A writer for meta-model backed document trees
  615. *
  616. * @param {Object} options output options to pass into the writer
  617. */
  618. export function Writer(options) {
  619. options = assign({ format: false, preamble: true }, options || {});
  620. function toXML(tree, writer) {
  621. var internalWriter = writer || new SavingWriter();
  622. var formatingWriter = new FormatingWriter(internalWriter, options.format);
  623. if (options.preamble) {
  624. formatingWriter.append(XML_PREAMBLE);
  625. }
  626. new ElementSerializer().build(tree).serializeTo(formatingWriter);
  627. if (!writer) {
  628. return internalWriter.value;
  629. }
  630. }
  631. return {
  632. toXML: toXML
  633. };
  634. }