├── .github └── workflows │ ├── CI.yml │ └── CODE_SCANNING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── descriptor-xml.md ├── eslint.config.js ├── lib ├── common.js ├── index.js ├── read.js └── write.js ├── package-lock.json ├── package.json ├── rollup.config.js └── test ├── expect.js ├── fixtures ├── error │ ├── binary.png │ └── no-xml.txt ├── model │ ├── attr-child-conflict.json │ ├── datatype-aliased.json │ ├── datatype-external.json │ ├── datatype.json │ ├── extension │ │ ├── base.json │ │ └── custom.json │ ├── extensions.json │ ├── fake-id.json │ ├── noalias.json │ ├── properties-extended.json │ ├── properties.json │ ├── redefine.json │ ├── replace.json │ ├── virtual.json │ └── xmi.json └── xml │ └── UML.xmi ├── helper.js ├── integration └── distro.cjs ├── matchers.js └── spec ├── reader.js ├── roundtrip.uml.js ├── rountrip.js └── writer.js /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest ] 9 | node-version: [ 20 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'npm' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build 24 | run: npm run all 25 | -------------------------------------------------------------------------------- /.github/workflows/CODE_SCANNING.yml: -------------------------------------------------------------------------------- 1 | name: CODE_SCANNING 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | paths-ignore: 9 | - '**/*.md' 10 | 11 | jobs: 12 | codeql_build: 13 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | # Initializes the CodeQL tools for scanning. 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v2 27 | with: 28 | languages: javascript 29 | config: | 30 | paths-ignore: 31 | - '**/test' 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v2 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | dist/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [moddle-xml](https://github.com/bpmn-io/moddle-xml) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 11.0.0 10 | 11 | * `FEAT`: add package `exports` ([#73](https://github.com/bpmn-io/moddle-xml/pull/73)) 12 | * `FEAT`: access `:xmlns` as a global name ([#69](https://github.com/bpmn-io/moddle-xml/pull/69), [`bdb5824`](https://github.com/bpmn-io/moddle-xml/pull/69/commits/bdb5824d8fc7727c20de40f5460f0f2b32b0d0c1)) 13 | * `FEAT`: allow to configure custom namespace map ([#69](https://github.com/bpmn-io/moddle-xml/pull/69), [`17d8cb0`](https://github.com/bpmn-io/moddle-xml/pull/69/commits/17d8cb0a737551c738b77c7e42bb7f3b56ab2fdb)) 14 | * `FEAT`: support alternative serialization methods ([#69](https://github.com/bpmn-io/moddle-xml/pull/69), [`2cb16b2`](https://github.com/bpmn-io/moddle-xml/pull/69/commits/2cb16b277c710ff1d4a53acfa78e243de898d0a5)) 15 | * `FIX`: correct export of generic element ([#69](https://github.com/bpmn-io/moddle-xml/pull/69), [`235504f`](https://github.com/bpmn-io/moddle-xml/pull/69/commits/235504f98488fced305e13a5b8a7e9f157f80232)) 16 | * `FIX`: remove broken `main` configuration ([#73](https://github.com/bpmn-io/moddle-xml/pull/73)) 17 | * `CHORE`: drop `UMD` distribution ([#73](https://github.com/bpmn-io/moddle-xml/pull/73)) 18 | * `DEPS`: update to `min-dash@4.2.1` 19 | * `DEPS`: update to `min-dash@4.2.1` 20 | * `DEPS`: update to `saxen@10` 21 | * `DEPS`: update to `moddle@7` 22 | 23 | ### Breaking Changes 24 | 25 | * UMD distribution no longer bundled. The module is now available as an ES module. 26 | 27 | ## 10.1.0 28 | 29 | * `FEAT`: generate sourcemaps 30 | 31 | ## 10.0.0 32 | 33 | * `DEPS`: update to `min-dash@4` 34 | * `DEPS`: update to `moddle@6` 35 | * `CHORE`: turn into ES module 36 | 37 | ## 9.0.6 38 | 39 | * `FIX`: correctly handle duplicated attributes ([#66](https://github.com/bpmn-io/moddle-xml/issues/66)) 40 | 41 | ## 9.0.5 42 | 43 | * `FIX`: correct serialization of `xml` namespace attributes on `Any` elements ([#60](https://github.com/bpmn-io/moddle-xml/issues/60)) 44 | * `FIX`: do not trim non-empty element text ([#58](https://github.com/bpmn-io/moddle-xml/issues/58)) 45 | 46 | ## 9.0.4 47 | 48 | * `FIX`: make hasOwnProperty check safe ([#54](https://github.com/bpmn-io/moddle-xml/pull/54)) 49 | 50 | ## 9.0.3 51 | 52 | * `FIX`: handle default `xml` namespace ([#50](https://github.com/bpmn-io/moddle-xml/issues/50)) 53 | 54 | ## 8.0.8 55 | 56 | * `FIX`: handle default `xml` namespace 57 | 58 | ## 9.0.2 59 | 60 | * `FIX`: recursively log namespace as used ([#49](https://github.com/bpmn-io/moddle-xml/pull/49)) 61 | 62 | ## 8.0.7 63 | 64 | * `FIX`: recursively log namespace as used ([#49](https://github.com/bpmn-io/moddle-xml/pull/49)) 65 | 66 | ## 9.0.1 67 | 68 | * `FIX`: correctly serialize nested local namespaced elements ([#47](https://github.com/bpmn-io/moddle-xml/pull/47)) 69 | 70 | ## 8.0.6 71 | 72 | * `FIX`: correctly serialize nested local namespaced elements ([#48](https://github.com/bpmn-io/moddle-xml/pull/48)) 73 | 74 | ## 9.0.0 75 | 76 | * `FEAT`: promisify `Reader#fromXML` ([#45](https://github.com/bpmn-io/moddle-xml/pull/45)) 77 | 78 | ### Breaking Changes 79 | 80 | * `Reader#fromXML` API now returns a Promise. Support for callbacks is dropped. Refer to the [documentation](https://github.com/bpmn-io/moddle-xml#read-xml) for updated usage information. 81 | 82 | ## 8.0.5 83 | 84 | _Republish of `v8.0.4`._ 85 | 86 | ## 8.0.4 87 | 88 | * `CHORE`: bump to `saxen@8.1.2` 89 | 90 | ## 8.0.3 91 | 92 | * `CHORE`: bump to `saxen@8.1.1` 93 | 94 | ## 8.0.2 95 | 96 | * `FIX`: read element as type if conflicting named propery is defines an attribute ([#43](https://github.com/bpmn-io/moddle-xml/issues/43)) 97 | 98 | ## 8.0.1 99 | 100 | * `DOCS`: update documentation 101 | 102 | ## 8.0.0 103 | 104 | * `FEAT`: provide pre-packaged distribution 105 | * `CHORE`: bump to `moddle@5` 106 | 107 | ## 7.5.0 108 | 109 | * `FEAT`: validate ID attributes are [QNames](http://www.w3.org/TR/REC-xml/#NT-NameChar) 110 | 111 | ## 7.4.1 112 | 113 | * `FIX`: make ES5 compliant 114 | 115 | ## 7.4.0 116 | 117 | * `CHORE`: get rid of `tiny-stack` as a dependency ([#38](https://github.com/bpmn-io/moddle-xml/pull/38)) 118 | 119 | ## 7.3.0 120 | 121 | * `FEAT`: warn on unexpected body text 122 | * `FEAT`: warn on text outside root node 123 | * `CHORE`: remove `console.log` during import ([#28](https://github.com/bpmn-io/moddle-xml/issues/28)) 124 | * `CHORE`: bump to [`saxen@8.1.0`](https://github.com/nikku/saxen/blob/master/CHANGELOG.md#810) 125 | 126 | ## 7.2.3 127 | 128 | * `FIX`: correctly serialize extension attributes along with typed elements 129 | 130 | ## 7.2.0 131 | 132 | * `FEAT`: warn on invalid attributes under well-known namespaces ([#32](https://github.com/bpmn-io/moddle-xml/issues/32)) 133 | 134 | ## 7.1.0 135 | 136 | * `CHORE`: bump dependency versions 137 | 138 | ## 7.0.0 139 | 140 | ### Breaking Changes 141 | 142 | * `FEAT`: migrate to ES modules. Use `esm` or a ES module aware transpiler to consume this library. 143 | 144 | ## 6.0.0 145 | 146 | * `FEAT`: encode entities in body properties (instead of escaping via `CDATA`) ([`5645b582`](https://github.com/bpmn-io/moddle-xml/commit/5645b5822644a461eba9f3da481362475f040984)) 147 | 148 | ## 5.0.2 149 | 150 | * `FIX`: properly handle `.` in attribute names 151 | 152 | ## 5.0.1 153 | 154 | * `FIX`: decode entities in `text` nodes 155 | 156 | ## 5.0.0 157 | 158 | * `FEAT`: replace lodash with [min-dash](https://github.com/bpmn-io/min-dash) 159 | * `FEAT`: don't bail out from attribute parsing on parse errors ([`fd0c8b40`](https://github.com/bpmn-io/moddle-xml/commit/fd0c8b4084b4d92565dd7d3099e283fbb98f1dd0)) 160 | 161 | ## ... 162 | 163 | Check `git log` for earlier history. 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present Camunda Services GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moddle-xml 2 | 3 | [![CI](https://github.com/bpmn-io/moddle-xml/workflows/CI/badge.svg)](https://github.com/bpmn-io/moddle-xml/actions?query=workflow%3ACI) 4 | 5 | Read and write XML documents described with [moddle](https://github.com/bpmn-io/moddle). 6 | 7 | 8 | ## Usage 9 | 10 | Get the libray via [npm](http://npmjs.org) 11 | 12 | ``` 13 | npm install --save moddle-xml 14 | ``` 15 | 16 | 17 | #### Bootstrap 18 | 19 | Create a [moddle instance](https://github.com/bpmn-io/moddle) 20 | 21 | ```javascript 22 | import { Moddle } from 'moddle'; 23 | import { 24 | Reader, 25 | Writer 26 | } from 'moddle-xml'; 27 | 28 | const model = new Moddle([ myPackage ]); 29 | ``` 30 | 31 | 32 | #### Read XML 33 | 34 | Use the reader to parse XML into an easily accessible object tree: 35 | 36 | ```javascript 37 | const model; // previously created 38 | 39 | const xml = 40 | '' + 41 | '' + 42 | '' + 43 | '' + 44 | ''; 45 | 46 | const reader = new Reader(model); 47 | const rootHandler = reader.handler('my:Root'); 48 | 49 | // when 50 | try { 51 | const { 52 | rootElement: cars, 53 | warnings 54 | } = await reader.fromXML(xml, rootHandler); 55 | 56 | if (warnings.length) { 57 | console.log('import warnings', warnings); 58 | } 59 | 60 | console.log(cars); 61 | 62 | // { 63 | // $type: 'my:Root', 64 | // cars: [ 65 | // { 66 | // $type: 'my:Car', 67 | // id: 'Car_1', 68 | // engine: [ 69 | // { $type: 'my:Engine', powser: 121, fuelConsumption: 10 } 70 | // ] 71 | // } 72 | // ] 73 | // } 74 | 75 | } catch (err) { 76 | console.log('import error', err, err.warnings); 77 | } 78 | ``` 79 | 80 | 81 | #### Write XML 82 | 83 | Use the writer to serialize the object tree back to XML: 84 | 85 | ```javascript 86 | var model; // previously created 87 | 88 | var cars = model.create('my:Root'); 89 | cars.get('cars').push(model.create('my:Car', { power: 10 })); 90 | 91 | var options = { format: false, preamble: false }; 92 | var writer = new Writer(options); 93 | 94 | var xml = writer.toXML(bar); 95 | 96 | console.log(xml); // ... 97 | ``` 98 | 99 | ## License 100 | 101 | MIT 102 | -------------------------------------------------------------------------------- /docs/descriptor-xml.md: -------------------------------------------------------------------------------- 1 | # moddle descriptor - XML Extensions 2 | 3 | When reading from / writing to XML, additional meta-data is necessary to correctly map the data model to a XML document. 4 | 5 | This document contains the list of supported extensions that are understood by [moddle-xml](https://github.com/bpmn-io/moddle-xml). 6 | 7 | 8 | ## Package Definition 9 | 10 | Extensions to the package definition allows you to configure namespacing and element serialization. 11 | 12 | 13 | ### Namespace URI 14 | 15 | Specify the `uri` field in a package definition to define the associated XML namespace URI. 16 | 17 | ```json 18 | { 19 | "prefix": "s", 20 | "uri": "http://sample" 21 | } 22 | ``` 23 | 24 | This results in 25 | 26 | ```xml 27 | 28 | ``` 29 | 30 | ### Element Name Serialization 31 | 32 | Specify `alias=lowerCase` to map elements to their lower case names in xml. 33 | 34 | The above output becomes 35 | 36 | ```xml 37 | 38 | ``` 39 | 40 | when this property is specified. 41 | 42 | 43 | ## Property definition 44 | 45 | XML distinguishes between child elements, body text and attributes. moddle allows you to map your data to these places via special qualifiers. 46 | 47 | 48 | ### Qualifiers 49 | 50 | Use any of the following qualifiers to configure how a property is mapped to XML. 51 | 52 | | Qualifier | Values | Description | 53 | | ------------- | ------------- | ----- | 54 | | `isAttr=false` | `Boolean` | serializes as an attribute | 55 | | `isBody=false` | `Boolean` | serializes as the body of the element | 56 | | `serialize` | `String` | adds additional notes on how to serialize. Supported value(s): `xsi:type` serializes as data type rather than element | 57 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; 2 | 3 | const files = { 4 | lib: [ 5 | 'lib/**/*.js' 6 | ], 7 | test: [ 8 | 'test/**/*.js', 9 | 'test/**/*.cjs' 10 | ], 11 | ignored: [ 12 | 'dist' 13 | ] 14 | }; 15 | 16 | export default [ 17 | { 18 | 'ignores': files.ignored 19 | }, 20 | 21 | // build 22 | ...bpmnIoPlugin.configs.node.map(config => { 23 | 24 | return { 25 | ...config, 26 | ignores: files.lib 27 | }; 28 | }), 29 | 30 | // lib + test 31 | ...bpmnIoPlugin.configs.recommended.map(config => { 32 | 33 | return { 34 | ...config, 35 | files: files.lib 36 | }; 37 | }), 38 | 39 | // test 40 | ...bpmnIoPlugin.configs.mocha.map(config => { 41 | 42 | return { 43 | ...config, 44 | files: files.test 45 | }; 46 | }) 47 | ]; -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | export function hasLowerCaseAlias(pkg) { 2 | return pkg.xml && pkg.xml.tagAlias === 'lowerCase'; 3 | } 4 | 5 | export var DEFAULT_NS_MAP = { 6 | 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', 7 | 'xml': 'http://www.w3.org/XML/1998/namespace' 8 | }; 9 | 10 | export var SERIALIZE_PROPERTY = 'property'; 11 | 12 | export function getSerialization(element) { 13 | return element.xml && element.xml.serialize; 14 | } 15 | 16 | export function getSerializationType(element) { 17 | const type = getSerialization(element); 18 | 19 | return type !== SERIALIZE_PROPERTY && (type || null); 20 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | Reader 3 | } from './read.js'; 4 | 5 | export { 6 | Writer 7 | } from './write.js'; -------------------------------------------------------------------------------- /lib/read.js: -------------------------------------------------------------------------------- 1 | import { 2 | forEach, 3 | find, 4 | assign 5 | } from 'min-dash'; 6 | 7 | import { 8 | Parser as SaxParser 9 | } from 'saxen'; 10 | 11 | import { 12 | Moddle, 13 | parseNameNS, 14 | coerceType, 15 | isSimpleType 16 | } from 'moddle'; 17 | 18 | import { 19 | DEFAULT_NS_MAP, 20 | getSerializationType, 21 | hasLowerCaseAlias 22 | } from './common.js'; 23 | 24 | function capitalize(str) { 25 | return str.charAt(0).toUpperCase() + str.slice(1); 26 | } 27 | 28 | function aliasToName(aliasNs, pkg) { 29 | 30 | if (!hasLowerCaseAlias(pkg)) { 31 | return aliasNs.name; 32 | } 33 | 34 | return aliasNs.prefix + ':' + capitalize(aliasNs.localName); 35 | } 36 | 37 | /** 38 | * Un-prefix a potentially prefixed type name. 39 | * 40 | * @param {NsName} nameNs 41 | * @param {Object} [pkg] 42 | * 43 | * @return {string} 44 | */ 45 | function prefixedToName(nameNs, pkg) { 46 | 47 | var name = nameNs.name, 48 | localName = nameNs.localName; 49 | 50 | var typePrefix = pkg && pkg.xml && pkg.xml.typePrefix; 51 | 52 | if (typePrefix && localName.indexOf(typePrefix) === 0) { 53 | return nameNs.prefix + ':' + localName.slice(typePrefix.length); 54 | } else { 55 | return name; 56 | } 57 | } 58 | 59 | function normalizeTypeName(name, nsMap, model) { 60 | 61 | // normalize against actual NS 62 | const nameNs = parseNameNS(name, nsMap.xmlns); 63 | 64 | const normalizedName = `${ nsMap[nameNs.prefix] || nameNs.prefix }:${ nameNs.localName }`; 65 | 66 | const normalizedNameNs = parseNameNS(normalizedName); 67 | 68 | // determine actual type name, based on package-defined prefix 69 | var pkg = model.getPackage(normalizedNameNs.prefix); 70 | 71 | return prefixedToName(normalizedNameNs, pkg); 72 | } 73 | 74 | function error(message) { 75 | return new Error(message); 76 | } 77 | 78 | /** 79 | * Get the moddle descriptor for a given instance or type. 80 | * 81 | * @param {ModdleElement|Function} element 82 | * 83 | * @return {Object} the moddle descriptor 84 | */ 85 | function getModdleDescriptor(element) { 86 | return element.$descriptor; 87 | } 88 | 89 | 90 | /** 91 | * A parse context. 92 | * 93 | * @class 94 | * 95 | * @param {Object} options 96 | * @param {ElementHandler} options.rootHandler the root handler for parsing a document 97 | * @param {boolean} [options.lax=false] whether or not to ignore invalid elements 98 | */ 99 | export function Context(options) { 100 | 101 | /** 102 | * @property {ElementHandler} rootHandler 103 | */ 104 | 105 | /** 106 | * @property {Boolean} lax 107 | */ 108 | 109 | assign(this, options); 110 | 111 | this.elementsById = {}; 112 | this.references = []; 113 | this.warnings = []; 114 | 115 | /** 116 | * Add an unresolved reference. 117 | * 118 | * @param {Object} reference 119 | */ 120 | this.addReference = function(reference) { 121 | this.references.push(reference); 122 | }; 123 | 124 | /** 125 | * Add a processed element. 126 | * 127 | * @param {ModdleElement} element 128 | */ 129 | this.addElement = function(element) { 130 | 131 | if (!element) { 132 | throw error('expected element'); 133 | } 134 | 135 | var elementsById = this.elementsById; 136 | 137 | var descriptor = getModdleDescriptor(element); 138 | 139 | var idProperty = descriptor.idProperty, 140 | id; 141 | 142 | if (idProperty) { 143 | id = element.get(idProperty.name); 144 | 145 | if (id) { 146 | 147 | // for QName validation as per http://www.w3.org/TR/REC-xml/#NT-NameChar 148 | if (!/^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i.test(id)) { 149 | throw new Error('illegal ID <' + id + '>'); 150 | } 151 | 152 | if (elementsById[id]) { 153 | throw error('duplicate ID <' + id + '>'); 154 | } 155 | 156 | elementsById[id] = element; 157 | } 158 | } 159 | }; 160 | 161 | /** 162 | * Add an import warning. 163 | * 164 | * @param {Object} warning 165 | * @param {String} warning.message 166 | * @param {Error} [warning.error] 167 | */ 168 | this.addWarning = function(warning) { 169 | this.warnings.push(warning); 170 | }; 171 | } 172 | 173 | function BaseHandler() {} 174 | 175 | BaseHandler.prototype.handleEnd = function() {}; 176 | BaseHandler.prototype.handleText = function() {}; 177 | BaseHandler.prototype.handleNode = function() {}; 178 | 179 | 180 | /** 181 | * A simple pass through handler that does nothing except for 182 | * ignoring all input it receives. 183 | * 184 | * This is used to ignore unknown elements and 185 | * attributes. 186 | */ 187 | function NoopHandler() { } 188 | 189 | NoopHandler.prototype = Object.create(BaseHandler.prototype); 190 | 191 | NoopHandler.prototype.handleNode = function() { 192 | return this; 193 | }; 194 | 195 | function BodyHandler() {} 196 | 197 | BodyHandler.prototype = Object.create(BaseHandler.prototype); 198 | 199 | BodyHandler.prototype.handleText = function(text) { 200 | this.body = (this.body || '') + text; 201 | }; 202 | 203 | function ReferenceHandler(property, context) { 204 | this.property = property; 205 | this.context = context; 206 | } 207 | 208 | ReferenceHandler.prototype = Object.create(BodyHandler.prototype); 209 | 210 | ReferenceHandler.prototype.handleNode = function(node) { 211 | 212 | if (this.element) { 213 | throw error('expected no sub nodes'); 214 | } else { 215 | this.element = this.createReference(node); 216 | } 217 | 218 | return this; 219 | }; 220 | 221 | ReferenceHandler.prototype.handleEnd = function() { 222 | this.element.id = this.body; 223 | }; 224 | 225 | ReferenceHandler.prototype.createReference = function(node) { 226 | return { 227 | property: this.property.ns.name, 228 | id: '' 229 | }; 230 | }; 231 | 232 | function ValueHandler(propertyDesc, element) { 233 | this.element = element; 234 | this.propertyDesc = propertyDesc; 235 | } 236 | 237 | ValueHandler.prototype = Object.create(BodyHandler.prototype); 238 | 239 | ValueHandler.prototype.handleEnd = function() { 240 | 241 | var value = this.body || '', 242 | element = this.element, 243 | propertyDesc = this.propertyDesc; 244 | 245 | value = coerceType(propertyDesc.type, value); 246 | 247 | if (propertyDesc.isMany) { 248 | element.get(propertyDesc.name).push(value); 249 | } else { 250 | element.set(propertyDesc.name, value); 251 | } 252 | }; 253 | 254 | 255 | function BaseElementHandler() {} 256 | 257 | BaseElementHandler.prototype = Object.create(BodyHandler.prototype); 258 | 259 | BaseElementHandler.prototype.handleNode = function(node) { 260 | var parser = this, 261 | element = this.element; 262 | 263 | if (!element) { 264 | element = this.element = this.createElement(node); 265 | 266 | this.context.addElement(element); 267 | } else { 268 | parser = this.handleChild(node); 269 | } 270 | 271 | return parser; 272 | }; 273 | 274 | /** 275 | * @class Reader.ElementHandler 276 | * 277 | */ 278 | export function ElementHandler(model, typeName, context) { 279 | this.model = model; 280 | this.type = model.getType(typeName); 281 | this.context = context; 282 | } 283 | 284 | ElementHandler.prototype = Object.create(BaseElementHandler.prototype); 285 | 286 | ElementHandler.prototype.addReference = function(reference) { 287 | this.context.addReference(reference); 288 | }; 289 | 290 | ElementHandler.prototype.handleText = function(text) { 291 | 292 | var element = this.element, 293 | descriptor = getModdleDescriptor(element), 294 | bodyProperty = descriptor.bodyProperty; 295 | 296 | if (!bodyProperty) { 297 | throw error('unexpected body text <' + text + '>'); 298 | } 299 | 300 | BodyHandler.prototype.handleText.call(this, text); 301 | }; 302 | 303 | ElementHandler.prototype.handleEnd = function() { 304 | 305 | var value = this.body, 306 | element = this.element, 307 | descriptor = getModdleDescriptor(element), 308 | bodyProperty = descriptor.bodyProperty; 309 | 310 | if (bodyProperty && value !== undefined) { 311 | value = coerceType(bodyProperty.type, value); 312 | element.set(bodyProperty.name, value); 313 | } 314 | }; 315 | 316 | /** 317 | * Create an instance of the model from the given node. 318 | * 319 | * @param {Element} node the xml node 320 | */ 321 | ElementHandler.prototype.createElement = function(node) { 322 | var attributes = node.attributes, 323 | Type = this.type, 324 | descriptor = getModdleDescriptor(Type), 325 | context = this.context, 326 | instance = new Type({}), 327 | model = this.model, 328 | propNameNs; 329 | 330 | forEach(attributes, function(value, name) { 331 | 332 | var prop = descriptor.propertiesByName[name], 333 | values; 334 | 335 | if (prop && prop.isReference) { 336 | 337 | if (!prop.isMany) { 338 | context.addReference({ 339 | element: instance, 340 | property: prop.ns.name, 341 | id: value 342 | }); 343 | } else { 344 | 345 | // IDREFS: parse references as whitespace-separated list 346 | values = value.split(' '); 347 | 348 | forEach(values, function(v) { 349 | context.addReference({ 350 | element: instance, 351 | property: prop.ns.name, 352 | id: v 353 | }); 354 | }); 355 | } 356 | 357 | } else { 358 | if (prop) { 359 | value = coerceType(prop.type, value); 360 | } else if (name === 'xmlns') { 361 | name = ':' + name; 362 | } else { 363 | propNameNs = parseNameNS(name, descriptor.ns.prefix); 364 | 365 | // check whether attribute is defined in a well-known namespace 366 | // if that is the case we emit a warning to indicate potential misuse 367 | if (model.getPackage(propNameNs.prefix)) { 368 | 369 | context.addWarning({ 370 | message: 'unknown attribute <' + name + '>', 371 | element: instance, 372 | property: name, 373 | value: value 374 | }); 375 | } 376 | } 377 | 378 | instance.set(name, value); 379 | } 380 | }); 381 | 382 | return instance; 383 | }; 384 | 385 | ElementHandler.prototype.getPropertyForNode = function(node) { 386 | 387 | var name = node.name; 388 | var nameNs = parseNameNS(name); 389 | 390 | var type = this.type, 391 | model = this.model, 392 | descriptor = getModdleDescriptor(type); 393 | 394 | var propertyName = nameNs.name, 395 | property = descriptor.propertiesByName[propertyName]; 396 | 397 | // search for properties by name first 398 | 399 | if (property && !property.isAttr) { 400 | 401 | const serializationType = getSerializationType(property); 402 | 403 | if (serializationType) { 404 | const elementTypeName = node.attributes[serializationType]; 405 | 406 | // type is optional, if it does not exists the 407 | // default type is assumed 408 | if (elementTypeName) { 409 | 410 | // convert the prefix used to the mapped form, but also 411 | // take possible type prefixes from XML 412 | // into account, i.e.: xsi:type="t{ActualType}", 413 | const normalizedTypeName = normalizeTypeName(elementTypeName, node.ns, model); 414 | 415 | const elementType = model.getType(normalizedTypeName); 416 | 417 | return assign({}, property, { 418 | effectiveType: getModdleDescriptor(elementType).name 419 | }); 420 | } 421 | } 422 | 423 | // search for properties by name first 424 | return property; 425 | } 426 | 427 | var pkg = model.getPackage(nameNs.prefix); 428 | 429 | if (pkg) { 430 | const elementTypeName = aliasToName(nameNs, pkg); 431 | const elementType = model.getType(elementTypeName); 432 | 433 | // search for collection members later 434 | property = find(descriptor.properties, function(p) { 435 | return !p.isVirtual && !p.isReference && !p.isAttribute && elementType.hasType(p.type); 436 | }); 437 | 438 | if (property) { 439 | return assign({}, property, { 440 | effectiveType: getModdleDescriptor(elementType).name 441 | }); 442 | } 443 | } else { 444 | 445 | // parse unknown element (maybe extension) 446 | property = find(descriptor.properties, function(p) { 447 | return !p.isReference && !p.isAttribute && p.type === 'Element'; 448 | }); 449 | 450 | if (property) { 451 | return property; 452 | } 453 | } 454 | 455 | throw error('unrecognized element <' + nameNs.name + '>'); 456 | }; 457 | 458 | ElementHandler.prototype.toString = function() { 459 | return 'ElementDescriptor[' + getModdleDescriptor(this.type).name + ']'; 460 | }; 461 | 462 | ElementHandler.prototype.valueHandler = function(propertyDesc, element) { 463 | return new ValueHandler(propertyDesc, element); 464 | }; 465 | 466 | ElementHandler.prototype.referenceHandler = function(propertyDesc) { 467 | return new ReferenceHandler(propertyDesc, this.context); 468 | }; 469 | 470 | ElementHandler.prototype.handler = function(type) { 471 | if (type === 'Element') { 472 | return new GenericElementHandler(this.model, type, this.context); 473 | } else { 474 | return new ElementHandler(this.model, type, this.context); 475 | } 476 | }; 477 | 478 | /** 479 | * Handle the child element parsing 480 | * 481 | * @param {Element} node the xml node 482 | */ 483 | ElementHandler.prototype.handleChild = function(node) { 484 | var propertyDesc, type, element, childHandler; 485 | 486 | propertyDesc = this.getPropertyForNode(node); 487 | element = this.element; 488 | 489 | type = propertyDesc.effectiveType || propertyDesc.type; 490 | 491 | if (isSimpleType(type)) { 492 | return this.valueHandler(propertyDesc, element); 493 | } 494 | 495 | if (propertyDesc.isReference) { 496 | childHandler = this.referenceHandler(propertyDesc).handleNode(node); 497 | } else { 498 | childHandler = this.handler(type).handleNode(node); 499 | } 500 | 501 | var newElement = childHandler.element; 502 | 503 | // child handles may decide to skip elements 504 | // by not returning anything 505 | if (newElement !== undefined) { 506 | 507 | if (propertyDesc.isMany) { 508 | element.get(propertyDesc.name).push(newElement); 509 | } else { 510 | element.set(propertyDesc.name, newElement); 511 | } 512 | 513 | if (propertyDesc.isReference) { 514 | assign(newElement, { 515 | element: element 516 | }); 517 | 518 | this.context.addReference(newElement); 519 | } else { 520 | 521 | // establish child -> parent relationship 522 | newElement.$parent = element; 523 | } 524 | } 525 | 526 | return childHandler; 527 | }; 528 | 529 | /** 530 | * An element handler that performs special validation 531 | * to ensure the node it gets initialized with matches 532 | * the handlers type (namespace wise). 533 | * 534 | * @param {Moddle} model 535 | * @param {String} typeName 536 | * @param {Context} context 537 | */ 538 | function RootElementHandler(model, typeName, context) { 539 | ElementHandler.call(this, model, typeName, context); 540 | } 541 | 542 | RootElementHandler.prototype = Object.create(ElementHandler.prototype); 543 | 544 | RootElementHandler.prototype.createElement = function(node) { 545 | 546 | var name = node.name, 547 | nameNs = parseNameNS(name), 548 | model = this.model, 549 | type = this.type, 550 | pkg = model.getPackage(nameNs.prefix), 551 | typeName = pkg && aliasToName(nameNs, pkg) || name; 552 | 553 | // verify the correct namespace if we parse 554 | // the first element in the handler tree 555 | // 556 | // this ensures we don't mistakenly import wrong namespace elements 557 | if (!type.hasType(typeName)) { 558 | throw error('unexpected element <' + node.originalName + '>'); 559 | } 560 | 561 | return ElementHandler.prototype.createElement.call(this, node); 562 | }; 563 | 564 | 565 | function GenericElementHandler(model, typeName, context) { 566 | this.model = model; 567 | this.context = context; 568 | } 569 | 570 | GenericElementHandler.prototype = Object.create(BaseElementHandler.prototype); 571 | 572 | GenericElementHandler.prototype.createElement = function(node) { 573 | 574 | var name = node.name, 575 | ns = parseNameNS(name), 576 | prefix = ns.prefix, 577 | uri = node.ns[prefix + '$uri'], 578 | attributes = node.attributes; 579 | 580 | return this.model.createAny(name, uri, attributes); 581 | }; 582 | 583 | GenericElementHandler.prototype.handleChild = function(node) { 584 | 585 | var handler = new GenericElementHandler(this.model, 'Element', this.context).handleNode(node), 586 | element = this.element; 587 | 588 | var newElement = handler.element, 589 | children; 590 | 591 | if (newElement !== undefined) { 592 | children = element.$children = element.$children || []; 593 | children.push(newElement); 594 | 595 | // establish child -> parent relationship 596 | newElement.$parent = element; 597 | } 598 | 599 | return handler; 600 | }; 601 | 602 | GenericElementHandler.prototype.handleEnd = function() { 603 | if (this.body) { 604 | this.element.$body = this.body; 605 | } 606 | }; 607 | 608 | /** 609 | * A reader for a meta-model 610 | * 611 | * @param {Object} options 612 | * @param {Model} options.model used to read xml files 613 | * @param {Boolean} options.lax whether to make parse errors warnings 614 | */ 615 | export function Reader(options) { 616 | 617 | if (options instanceof Moddle) { 618 | options = { 619 | model: options 620 | }; 621 | } 622 | 623 | assign(this, { lax: false }, options); 624 | } 625 | 626 | /** 627 | * The fromXML result. 628 | * 629 | * @typedef {Object} ParseResult 630 | * 631 | * @property {ModdleElement} rootElement 632 | * @property {Array} references 633 | * @property {Array} warnings 634 | * @property {Object} elementsById - a mapping containing each ID -> ModdleElement 635 | */ 636 | 637 | /** 638 | * The fromXML result. 639 | * 640 | * @typedef {Error} ParseError 641 | * 642 | * @property {Array} warnings 643 | */ 644 | 645 | /** 646 | * Parse the given XML into a moddle document tree. 647 | * 648 | * @param {String} xml 649 | * @param {ElementHandler|Object} options or rootHandler 650 | * 651 | * @returns {Promise} 652 | */ 653 | Reader.prototype.fromXML = function(xml, options, done) { 654 | 655 | var rootHandler = options.rootHandler; 656 | 657 | if (options instanceof ElementHandler) { 658 | 659 | // root handler passed via (xml, { rootHandler: ElementHandler }, ...) 660 | rootHandler = options; 661 | options = {}; 662 | } else { 663 | if (typeof options === 'string') { 664 | 665 | // rootHandler passed via (xml, 'someString', ...) 666 | rootHandler = this.handler(options); 667 | options = {}; 668 | } else if (typeof rootHandler === 'string') { 669 | 670 | // rootHandler passed via (xml, { rootHandler: 'someString' }, ...) 671 | rootHandler = this.handler(rootHandler); 672 | } 673 | } 674 | 675 | var model = this.model, 676 | lax = this.lax; 677 | 678 | var context = new Context(assign({}, options, { rootHandler: rootHandler })), 679 | parser = new SaxParser({ proxy: true }), 680 | stack = createStack(); 681 | 682 | rootHandler.context = context; 683 | 684 | // push root handler 685 | stack.push(rootHandler); 686 | 687 | 688 | /** 689 | * Handle error. 690 | * 691 | * @param {Error} err 692 | * @param {Function} getContext 693 | * @param {boolean} lax 694 | * 695 | * @return {boolean} true if handled 696 | */ 697 | function handleError(err, getContext, lax) { 698 | 699 | var ctx = getContext(); 700 | 701 | var line = ctx.line, 702 | column = ctx.column, 703 | data = ctx.data; 704 | 705 | // we receive the full context data here, 706 | // for elements trim down the information 707 | // to the tag name, only 708 | if (data.charAt(0) === '<' && data.indexOf(' ') !== -1) { 709 | data = data.slice(0, data.indexOf(' ')) + '>'; 710 | } 711 | 712 | var message = 713 | 'unparsable content ' + (data ? data + ' ' : '') + 'detected\n\t' + 714 | 'line: ' + line + '\n\t' + 715 | 'column: ' + column + '\n\t' + 716 | 'nested error: ' + err.message; 717 | 718 | if (lax) { 719 | context.addWarning({ 720 | message: message, 721 | error: err 722 | }); 723 | 724 | return true; 725 | } else { 726 | throw error(message); 727 | } 728 | } 729 | 730 | function handleWarning(err, getContext) { 731 | 732 | // just like handling errors in mode 733 | return handleError(err, getContext, true); 734 | } 735 | 736 | /** 737 | * Resolve collected references on parse end. 738 | */ 739 | function resolveReferences() { 740 | 741 | var elementsById = context.elementsById; 742 | var references = context.references; 743 | 744 | var i, r; 745 | 746 | for (i = 0; (r = references[i]); i++) { 747 | var element = r.element; 748 | var reference = elementsById[r.id]; 749 | var property = getModdleDescriptor(element).propertiesByName[r.property]; 750 | 751 | if (!reference) { 752 | context.addWarning({ 753 | message: 'unresolved reference <' + r.id + '>', 754 | element: r.element, 755 | property: r.property, 756 | value: r.id 757 | }); 758 | } 759 | 760 | if (property.isMany) { 761 | var collection = element.get(property.name), 762 | idx = collection.indexOf(r); 763 | 764 | // we replace an existing place holder (idx != -1) or 765 | // append to the collection instead 766 | if (idx === -1) { 767 | idx = collection.length; 768 | } 769 | 770 | if (!reference) { 771 | 772 | // remove unresolvable reference 773 | collection.splice(idx, 1); 774 | } else { 775 | 776 | // add or update reference in collection 777 | collection[idx] = reference; 778 | } 779 | } else { 780 | element.set(property.name, reference); 781 | } 782 | } 783 | } 784 | 785 | function handleClose() { 786 | stack.pop().handleEnd(); 787 | } 788 | 789 | var PREAMBLE_START_PATTERN = /^<\?xml /i; 790 | 791 | var ENCODING_PATTERN = / encoding="([^"]+)"/i; 792 | 793 | var UTF_8_PATTERN = /^utf-8$/i; 794 | 795 | function handleQuestion(question) { 796 | 797 | if (!PREAMBLE_START_PATTERN.test(question)) { 798 | return; 799 | } 800 | 801 | var match = ENCODING_PATTERN.exec(question); 802 | var encoding = match && match[1]; 803 | 804 | if (!encoding || UTF_8_PATTERN.test(encoding)) { 805 | return; 806 | } 807 | 808 | context.addWarning({ 809 | message: 810 | 'unsupported document encoding <' + encoding + '>, ' + 811 | 'falling back to UTF-8' 812 | }); 813 | } 814 | 815 | function handleOpen(node, getContext) { 816 | var handler = stack.peek(); 817 | 818 | try { 819 | stack.push(handler.handleNode(node)); 820 | } catch (err) { 821 | 822 | if (handleError(err, getContext, lax)) { 823 | stack.push(new NoopHandler()); 824 | } 825 | } 826 | } 827 | 828 | function handleCData(text, getContext) { 829 | 830 | try { 831 | stack.peek().handleText(text); 832 | } catch (err) { 833 | handleWarning(err, getContext); 834 | } 835 | } 836 | 837 | function handleText(text, getContext) { 838 | 839 | // strip whitespace only nodes, i.e. before 840 | // sections and in between tags 841 | 842 | if (!text.trim()) { 843 | return; 844 | } 845 | 846 | handleCData(text, getContext); 847 | } 848 | 849 | var uriMap = model.getPackages().reduce(function(uriMap, p) { 850 | uriMap[p.uri] = p.prefix; 851 | 852 | return uriMap; 853 | }, Object.entries(DEFAULT_NS_MAP).reduce(function(map, [ prefix, url ]) { 854 | map[url] = prefix; 855 | 856 | return map; 857 | }, model.config && model.config.nsMap || {})); 858 | 859 | parser 860 | .ns(uriMap) 861 | .on('openTag', function(obj, decodeStr, selfClosing, getContext) { 862 | 863 | // gracefully handle unparsable attributes (attrs=false) 864 | var attrs = obj.attrs || {}; 865 | 866 | var decodedAttrs = Object.keys(attrs).reduce(function(d, key) { 867 | var value = decodeStr(attrs[key]); 868 | 869 | d[key] = value; 870 | 871 | return d; 872 | }, {}); 873 | 874 | var node = { 875 | name: obj.name, 876 | originalName: obj.originalName, 877 | attributes: decodedAttrs, 878 | ns: obj.ns 879 | }; 880 | 881 | handleOpen(node, getContext); 882 | }) 883 | .on('question', handleQuestion) 884 | .on('closeTag', handleClose) 885 | .on('cdata', handleCData) 886 | .on('text', function(text, decodeEntities, getContext) { 887 | handleText(decodeEntities(text), getContext); 888 | }) 889 | .on('error', handleError) 890 | .on('warn', handleWarning); 891 | 892 | // async XML parsing to make sure the execution environment 893 | // (node or brower) is kept responsive and that certain optimization 894 | // strategies can kick in. 895 | return new Promise(function(resolve, reject) { 896 | 897 | var err; 898 | 899 | try { 900 | parser.parse(xml); 901 | 902 | resolveReferences(); 903 | } catch (e) { 904 | err = e; 905 | } 906 | 907 | var rootElement = rootHandler.element; 908 | 909 | if (!err && !rootElement) { 910 | err = error('failed to parse document as <' + rootHandler.type.$descriptor.name + '>'); 911 | } 912 | 913 | var warnings = context.warnings; 914 | var references = context.references; 915 | var elementsById = context.elementsById; 916 | 917 | if (err) { 918 | err.warnings = warnings; 919 | 920 | return reject(err); 921 | } else { 922 | return resolve({ 923 | rootElement: rootElement, 924 | elementsById: elementsById, 925 | references: references, 926 | warnings: warnings 927 | }); 928 | } 929 | }); 930 | }; 931 | 932 | Reader.prototype.handler = function(name) { 933 | return new RootElementHandler(this.model, name); 934 | }; 935 | 936 | 937 | // helpers ////////////////////////// 938 | 939 | function createStack() { 940 | var stack = []; 941 | 942 | Object.defineProperty(stack, 'peek', { 943 | value: function() { 944 | return this[this.length - 1]; 945 | } 946 | }); 947 | 948 | return stack; 949 | } 950 | -------------------------------------------------------------------------------- /lib/write.js: -------------------------------------------------------------------------------- 1 | import { 2 | forEach, 3 | isString, 4 | filter, 5 | assign, 6 | has, 7 | findIndex 8 | } from 'min-dash'; 9 | 10 | import { 11 | isSimpleType, 12 | parseNameNS 13 | } from 'moddle'; 14 | 15 | import { 16 | hasLowerCaseAlias, 17 | getSerialization, 18 | SERIALIZE_PROPERTY, 19 | DEFAULT_NS_MAP 20 | } from './common.js'; 21 | 22 | var XML_PREAMBLE = '\n'; 23 | 24 | var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g; 25 | var ESCAPE_CHARS = /<|>|&/g; 26 | 27 | 28 | export function Namespaces(parent) { 29 | 30 | this.prefixMap = {}; 31 | this.uriMap = {}; 32 | this.used = {}; 33 | 34 | this.wellknown = []; 35 | this.custom = []; 36 | this.parent = parent; 37 | 38 | this.defaultPrefixMap = parent && parent.defaultPrefixMap || {}; 39 | } 40 | 41 | Namespaces.prototype.mapDefaultPrefixes = function(defaultPrefixMap) { 42 | this.defaultPrefixMap = defaultPrefixMap; 43 | }; 44 | 45 | Namespaces.prototype.defaultUriByPrefix = function(prefix) { 46 | return this.defaultPrefixMap[prefix]; 47 | }; 48 | 49 | Namespaces.prototype.byUri = function(uri) { 50 | return this.uriMap[uri] || ( 51 | this.parent && this.parent.byUri(uri) 52 | ); 53 | }; 54 | 55 | Namespaces.prototype.add = function(ns, isWellknown) { 56 | 57 | this.uriMap[ns.uri] = ns; 58 | 59 | if (isWellknown) { 60 | this.wellknown.push(ns); 61 | } else { 62 | this.custom.push(ns); 63 | } 64 | 65 | this.mapPrefix(ns.prefix, ns.uri); 66 | }; 67 | 68 | Namespaces.prototype.uriByPrefix = function(prefix) { 69 | return this.prefixMap[prefix || 'xmlns'] || ( 70 | this.parent && this.parent.uriByPrefix(prefix) 71 | ); 72 | }; 73 | 74 | Namespaces.prototype.mapPrefix = function(prefix, uri) { 75 | this.prefixMap[prefix || 'xmlns'] = uri; 76 | }; 77 | 78 | Namespaces.prototype.getNSKey = function(ns) { 79 | return (ns.prefix !== undefined) ? (ns.uri + '|' + ns.prefix) : ns.uri; 80 | }; 81 | 82 | Namespaces.prototype.logUsed = function(ns) { 83 | 84 | var uri = ns.uri; 85 | var nsKey = this.getNSKey(ns); 86 | 87 | this.used[nsKey] = this.byUri(uri); 88 | 89 | // Inform parent recursively about the usage of this NS 90 | if (this.parent) { 91 | this.parent.logUsed(ns); 92 | } 93 | }; 94 | 95 | Namespaces.prototype.getUsed = function(ns) { 96 | 97 | var allNs = [].concat(this.wellknown, this.custom); 98 | 99 | return allNs.filter(ns => { 100 | var nsKey = this.getNSKey(ns); 101 | 102 | return this.used[nsKey]; 103 | }); 104 | }; 105 | 106 | 107 | function lower(string) { 108 | return string.charAt(0).toLowerCase() + string.slice(1); 109 | } 110 | 111 | function nameToAlias(name, pkg) { 112 | if (hasLowerCaseAlias(pkg)) { 113 | return lower(name); 114 | } else { 115 | return name; 116 | } 117 | } 118 | 119 | function inherits(ctor, superCtor) { 120 | ctor.super_ = superCtor; 121 | ctor.prototype = Object.create(superCtor.prototype, { 122 | constructor: { 123 | value: ctor, 124 | enumerable: false, 125 | writable: true, 126 | configurable: true 127 | } 128 | }); 129 | } 130 | 131 | function nsName(ns) { 132 | if (isString(ns)) { 133 | return ns; 134 | } else { 135 | return (ns.prefix ? ns.prefix + ':' : '') + ns.localName; 136 | } 137 | } 138 | 139 | function getNsAttrs(namespaces) { 140 | 141 | return namespaces.getUsed().filter(function(ns) { 142 | 143 | // do not serialize built in namespace 144 | return ns.prefix !== 'xml'; 145 | }).map(function(ns) { 146 | var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : ''); 147 | return { name: name, value: ns.uri }; 148 | }); 149 | 150 | } 151 | 152 | function getElementNs(ns, descriptor) { 153 | if (descriptor.isGeneric) { 154 | return assign({ localName: descriptor.ns.localName }, ns); 155 | } else { 156 | return assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns); 157 | } 158 | } 159 | 160 | function getPropertyNs(ns, descriptor) { 161 | return assign({ localName: descriptor.ns.localName }, ns); 162 | } 163 | 164 | function getSerializableProperties(element) { 165 | var descriptor = element.$descriptor; 166 | 167 | return filter(descriptor.properties, function(p) { 168 | var name = p.name; 169 | 170 | if (p.isVirtual) { 171 | return false; 172 | } 173 | 174 | // do not serialize defaults 175 | if (!has(element, name)) { 176 | return false; 177 | } 178 | 179 | var value = element[name]; 180 | 181 | // do not serialize default equals 182 | if (value === p.default) { 183 | return false; 184 | } 185 | 186 | // do not serialize null properties 187 | if (value === null) { 188 | return false; 189 | } 190 | 191 | return p.isMany ? value.length : true; 192 | }); 193 | } 194 | 195 | var ESCAPE_ATTR_MAP = { 196 | '\n': '#10', 197 | '\n\r': '#10', 198 | '"': '#34', 199 | '\'': '#39', 200 | '<': '#60', 201 | '>': '#62', 202 | '&': '#38' 203 | }; 204 | 205 | var ESCAPE_MAP = { 206 | '<': 'lt', 207 | '>': 'gt', 208 | '&': 'amp' 209 | }; 210 | 211 | function escape(str, charPattern, replaceMap) { 212 | 213 | // ensure we are handling strings here 214 | str = isString(str) ? str : '' + str; 215 | 216 | return str.replace(charPattern, function(s) { 217 | return '&' + replaceMap[s] + ';'; 218 | }); 219 | } 220 | 221 | /** 222 | * Escape a string attribute to not contain any bad values (line breaks, '"', ...) 223 | * 224 | * @param {String} str the string to escape 225 | * @return {String} the escaped string 226 | */ 227 | function escapeAttr(str) { 228 | return escape(str, ESCAPE_ATTR_CHARS, ESCAPE_ATTR_MAP); 229 | } 230 | 231 | function escapeBody(str) { 232 | return escape(str, ESCAPE_CHARS, ESCAPE_MAP); 233 | } 234 | 235 | function filterAttributes(props) { 236 | return filter(props, function(p) { return p.isAttr; }); 237 | } 238 | 239 | function filterContained(props) { 240 | return filter(props, function(p) { return !p.isAttr; }); 241 | } 242 | 243 | 244 | function ReferenceSerializer(tagName) { 245 | this.tagName = tagName; 246 | } 247 | 248 | ReferenceSerializer.prototype.build = function(element) { 249 | this.element = element; 250 | return this; 251 | }; 252 | 253 | ReferenceSerializer.prototype.serializeTo = function(writer) { 254 | writer 255 | .appendIndent() 256 | .append('<' + this.tagName + '>' + this.element.id + '') 257 | .appendNewLine(); 258 | }; 259 | 260 | function BodySerializer() {} 261 | 262 | BodySerializer.prototype.serializeValue = 263 | BodySerializer.prototype.serializeTo = function(writer) { 264 | writer.append( 265 | this.escape 266 | ? escapeBody(this.value) 267 | : this.value 268 | ); 269 | }; 270 | 271 | BodySerializer.prototype.build = function(prop, value) { 272 | this.value = value; 273 | 274 | if (prop.type === 'String' && value.search(ESCAPE_CHARS) !== -1) { 275 | this.escape = true; 276 | } 277 | 278 | return this; 279 | }; 280 | 281 | function ValueSerializer(tagName) { 282 | this.tagName = tagName; 283 | } 284 | 285 | inherits(ValueSerializer, BodySerializer); 286 | 287 | ValueSerializer.prototype.serializeTo = function(writer) { 288 | 289 | writer 290 | .appendIndent() 291 | .append('<' + this.tagName + '>'); 292 | 293 | this.serializeValue(writer); 294 | 295 | writer 296 | .append('') 297 | .appendNewLine(); 298 | }; 299 | 300 | function ElementSerializer(parent, propertyDescriptor) { 301 | this.body = []; 302 | this.attrs = []; 303 | 304 | this.parent = parent; 305 | this.propertyDescriptor = propertyDescriptor; 306 | } 307 | 308 | ElementSerializer.prototype.build = function(element) { 309 | this.element = element; 310 | 311 | var elementDescriptor = element.$descriptor, 312 | propertyDescriptor = this.propertyDescriptor; 313 | 314 | var otherAttrs, 315 | properties; 316 | 317 | var isGeneric = elementDescriptor.isGeneric; 318 | 319 | if (isGeneric) { 320 | otherAttrs = this.parseGenericNsAttributes(element); 321 | } else { 322 | otherAttrs = this.parseNsAttributes(element); 323 | } 324 | 325 | if (propertyDescriptor) { 326 | this.ns = this.nsPropertyTagName(propertyDescriptor); 327 | } else { 328 | this.ns = this.nsTagName(elementDescriptor); 329 | } 330 | 331 | // compute tag name 332 | this.tagName = this.addTagName(this.ns); 333 | 334 | if (isGeneric) { 335 | this.parseGenericContainments(element); 336 | } else { 337 | properties = getSerializableProperties(element); 338 | 339 | this.parseAttributes(filterAttributes(properties)); 340 | this.parseContainments(filterContained(properties)); 341 | } 342 | 343 | this.parseGenericAttributes(element, otherAttrs); 344 | 345 | return this; 346 | }; 347 | 348 | ElementSerializer.prototype.nsTagName = function(descriptor) { 349 | var effectiveNs = this.logNamespaceUsed(descriptor.ns); 350 | return getElementNs(effectiveNs, descriptor); 351 | }; 352 | 353 | ElementSerializer.prototype.nsPropertyTagName = function(descriptor) { 354 | var effectiveNs = this.logNamespaceUsed(descriptor.ns); 355 | return getPropertyNs(effectiveNs, descriptor); 356 | }; 357 | 358 | ElementSerializer.prototype.isLocalNs = function(ns) { 359 | return ns.uri === this.ns.uri; 360 | }; 361 | 362 | /** 363 | * Get the actual ns attribute name for the given element. 364 | * 365 | * @param {Object} element 366 | * @param {Boolean} [element.inherited=false] 367 | * 368 | * @return {Object} nsName 369 | */ 370 | ElementSerializer.prototype.nsAttributeName = function(element) { 371 | 372 | var ns; 373 | 374 | if (isString(element)) { 375 | ns = parseNameNS(element); 376 | } else { 377 | ns = element.ns; 378 | } 379 | 380 | // return just local name for inherited attributes 381 | if (element.inherited) { 382 | return { localName: ns.localName }; 383 | } 384 | 385 | // parse + log effective ns 386 | var effectiveNs = this.logNamespaceUsed(ns); 387 | 388 | // LOG ACTUAL namespace use 389 | this.getNamespaces().logUsed(effectiveNs); 390 | 391 | // strip prefix if same namespace like parent 392 | if (this.isLocalNs(effectiveNs)) { 393 | return { localName: ns.localName }; 394 | } else { 395 | return assign({ localName: ns.localName }, effectiveNs); 396 | } 397 | }; 398 | 399 | ElementSerializer.prototype.parseGenericNsAttributes = function(element) { 400 | 401 | return Object.entries(element).filter( 402 | ([ key, value ]) => !key.startsWith('$') && this.parseNsAttribute(element, key, value) 403 | ).map( 404 | ([ key, value ]) => ({ name: key, value: value }) 405 | ); 406 | }; 407 | 408 | ElementSerializer.prototype.parseGenericContainments = function(element) { 409 | var body = element.$body; 410 | 411 | if (body) { 412 | this.body.push(new BodySerializer().build({ type: 'String' }, body)); 413 | } 414 | 415 | var children = element.$children; 416 | 417 | if (children) { 418 | forEach(children, child => { 419 | this.body.push(new ElementSerializer(this).build(child)); 420 | }); 421 | } 422 | }; 423 | 424 | ElementSerializer.prototype.parseNsAttribute = function(element, name, value) { 425 | var model = element.$model; 426 | 427 | var nameNs = parseNameNS(name); 428 | 429 | var ns; 430 | 431 | // parse xmlns:foo="http://foo.bar" 432 | if (nameNs.prefix === 'xmlns') { 433 | ns = { prefix: nameNs.localName, uri: value }; 434 | } 435 | 436 | // parse xmlns="http://foo.bar" 437 | if (!nameNs.prefix && nameNs.localName === 'xmlns') { 438 | ns = { uri: value }; 439 | } 440 | 441 | if (!ns) { 442 | return { 443 | name: name, 444 | value: value 445 | }; 446 | } 447 | 448 | if (model && model.getPackage(value)) { 449 | 450 | // register well known namespace 451 | this.logNamespace(ns, true, true); 452 | } else { 453 | 454 | // log custom namespace directly as used 455 | var actualNs = this.logNamespaceUsed(ns, true); 456 | 457 | this.getNamespaces().logUsed(actualNs); 458 | } 459 | }; 460 | 461 | 462 | /** 463 | * Parse namespaces and return a list of left over generic attributes 464 | * 465 | * @param {Object} element 466 | * @return {Array} 467 | */ 468 | ElementSerializer.prototype.parseNsAttributes = function(element) { 469 | var self = this; 470 | 471 | var genericAttrs = element.$attrs; 472 | 473 | var attributes = []; 474 | 475 | // parse namespace attributes first 476 | // and log them. push non namespace attributes to a list 477 | // and process them later 478 | forEach(genericAttrs, function(value, name) { 479 | 480 | var nonNsAttr = self.parseNsAttribute(element, name, value); 481 | 482 | if (nonNsAttr) { 483 | attributes.push(nonNsAttr); 484 | } 485 | }); 486 | 487 | return attributes; 488 | }; 489 | 490 | ElementSerializer.prototype.parseGenericAttributes = function(element, attributes) { 491 | 492 | var self = this; 493 | 494 | forEach(attributes, function(attr) { 495 | 496 | try { 497 | self.addAttribute(self.nsAttributeName(attr.name), attr.value); 498 | } catch (e) { 499 | 500 | // eslint-disable-next-line no-undef 501 | typeof console !== 'undefined' && console.warn( 502 | `missing namespace information for <${ 503 | attr.name 504 | }=${ attr.value }> on`, element, e 505 | ); 506 | } 507 | }); 508 | }; 509 | 510 | ElementSerializer.prototype.parseContainments = function(properties) { 511 | 512 | var self = this, 513 | body = this.body, 514 | element = this.element; 515 | 516 | forEach(properties, function(p) { 517 | var value = element.get(p.name), 518 | isReference = p.isReference, 519 | isMany = p.isMany; 520 | 521 | if (!isMany) { 522 | value = [ value ]; 523 | } 524 | 525 | if (p.isBody) { 526 | body.push(new BodySerializer().build(p, value[0])); 527 | } else if (isSimpleType(p.type)) { 528 | forEach(value, function(v) { 529 | body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v)); 530 | }); 531 | } else if (isReference) { 532 | forEach(value, function(v) { 533 | body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v)); 534 | }); 535 | } else { 536 | 537 | // allow serialization via type 538 | // rather than element name 539 | var serialization = getSerialization(p); 540 | 541 | forEach(value, function(v) { 542 | var serializer; 543 | 544 | if (serialization) { 545 | if (serialization === SERIALIZE_PROPERTY) { 546 | serializer = new ElementSerializer(self, p); 547 | } else { 548 | serializer = new TypeSerializer(self, p, serialization); 549 | } 550 | } else { 551 | serializer = new ElementSerializer(self); 552 | } 553 | 554 | body.push(serializer.build(v)); 555 | }); 556 | } 557 | }); 558 | }; 559 | 560 | ElementSerializer.prototype.getNamespaces = function(local) { 561 | 562 | var namespaces = this.namespaces, 563 | parent = this.parent, 564 | parentNamespaces; 565 | 566 | if (!namespaces) { 567 | parentNamespaces = parent && parent.getNamespaces(); 568 | 569 | if (local || !parentNamespaces) { 570 | this.namespaces = namespaces = new Namespaces(parentNamespaces); 571 | } else { 572 | namespaces = parentNamespaces; 573 | } 574 | } 575 | 576 | return namespaces; 577 | }; 578 | 579 | ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) { 580 | var namespaces = this.getNamespaces(local); 581 | 582 | var nsUri = ns.uri, 583 | nsPrefix = ns.prefix; 584 | 585 | var existing = namespaces.byUri(nsUri); 586 | 587 | if (!existing || local) { 588 | namespaces.add(ns, wellknown); 589 | } 590 | 591 | namespaces.mapPrefix(nsPrefix, nsUri); 592 | 593 | return ns; 594 | }; 595 | 596 | ElementSerializer.prototype.logNamespaceUsed = function(ns, local) { 597 | var namespaces = this.getNamespaces(local); 598 | 599 | // ns may be 600 | // 601 | // * prefix only 602 | // * prefix:uri 603 | // * localName only 604 | 605 | var prefix = ns.prefix, 606 | uri = ns.uri, 607 | newPrefix, idx, 608 | wellknownUri; 609 | 610 | // handle anonymous namespaces (elementForm=unqualified), cf. #23 611 | if (!prefix && !uri) { 612 | return { localName: ns.localName }; 613 | } 614 | 615 | wellknownUri = namespaces.defaultUriByPrefix(prefix); 616 | 617 | uri = uri || wellknownUri || namespaces.uriByPrefix(prefix); 618 | 619 | if (!uri) { 620 | throw new Error('no namespace uri given for prefix <' + prefix + '>'); 621 | } 622 | 623 | ns = namespaces.byUri(uri); 624 | 625 | // register new default prefix in local scope 626 | if (!ns && !prefix) { 627 | ns = this.logNamespace({ uri }, wellknownUri === uri, true); 628 | } 629 | 630 | if (!ns) { 631 | newPrefix = prefix; 632 | idx = 1; 633 | 634 | // find a prefix that is not mapped yet 635 | while (namespaces.uriByPrefix(newPrefix)) { 636 | newPrefix = prefix + '_' + idx++; 637 | } 638 | 639 | ns = this.logNamespace({ prefix: newPrefix, uri: uri }, wellknownUri === uri); 640 | } 641 | 642 | if (prefix) { 643 | namespaces.mapPrefix(prefix, uri); 644 | } 645 | 646 | return ns; 647 | }; 648 | 649 | ElementSerializer.prototype.parseAttributes = function(properties) { 650 | var self = this, 651 | element = this.element; 652 | 653 | forEach(properties, function(p) { 654 | 655 | var value = element.get(p.name); 656 | 657 | if (p.isReference) { 658 | 659 | if (!p.isMany) { 660 | value = value.id; 661 | } else { 662 | var values = []; 663 | forEach(value, function(v) { 664 | values.push(v.id); 665 | }); 666 | 667 | // IDREFS is a whitespace-separated list of references. 668 | value = values.join(' '); 669 | } 670 | 671 | } 672 | 673 | self.addAttribute(self.nsAttributeName(p), value); 674 | }); 675 | }; 676 | 677 | ElementSerializer.prototype.addTagName = function(nsTagName) { 678 | var actualNs = this.logNamespaceUsed(nsTagName); 679 | 680 | this.getNamespaces().logUsed(actualNs); 681 | 682 | return nsName(nsTagName); 683 | }; 684 | 685 | ElementSerializer.prototype.addAttribute = function(name, value) { 686 | var attrs = this.attrs; 687 | 688 | if (isString(value)) { 689 | value = escapeAttr(value); 690 | } 691 | 692 | // de-duplicate attributes 693 | // https://github.com/bpmn-io/moddle-xml/issues/66 694 | var idx = findIndex(attrs, function(element) { 695 | return ( 696 | element.name.localName === name.localName && 697 | element.name.uri === name.uri && 698 | element.name.prefix === name.prefix 699 | ); 700 | }); 701 | 702 | var attr = { name: name, value: value }; 703 | 704 | if (idx !== -1) { 705 | attrs.splice(idx, 1, attr); 706 | } else { 707 | attrs.push(attr); 708 | } 709 | }; 710 | 711 | ElementSerializer.prototype.serializeAttributes = function(writer) { 712 | var attrs = this.attrs, 713 | namespaces = this.namespaces; 714 | 715 | if (namespaces) { 716 | attrs = getNsAttrs(namespaces).concat(attrs); 717 | } 718 | 719 | forEach(attrs, function(a) { 720 | writer 721 | .append(' ') 722 | .append(nsName(a.name)).append('="').append(a.value).append('"'); 723 | }); 724 | }; 725 | 726 | ElementSerializer.prototype.serializeTo = function(writer) { 727 | var firstBody = this.body[0], 728 | indent = firstBody && firstBody.constructor !== BodySerializer; 729 | 730 | writer 731 | .appendIndent() 732 | .append('<' + this.tagName); 733 | 734 | this.serializeAttributes(writer); 735 | 736 | writer.append(firstBody ? '>' : ' />'); 737 | 738 | if (firstBody) { 739 | 740 | if (indent) { 741 | writer 742 | .appendNewLine() 743 | .indent(); 744 | } 745 | 746 | forEach(this.body, function(b) { 747 | b.serializeTo(writer); 748 | }); 749 | 750 | if (indent) { 751 | writer 752 | .unindent() 753 | .appendIndent(); 754 | } 755 | 756 | writer.append(''); 757 | } 758 | 759 | writer.appendNewLine(); 760 | }; 761 | 762 | /** 763 | * A serializer for types that handles serialization of data types 764 | */ 765 | function TypeSerializer(parent, propertyDescriptor, serialization) { 766 | ElementSerializer.call(this, parent, propertyDescriptor); 767 | 768 | this.serialization = serialization; 769 | } 770 | 771 | inherits(TypeSerializer, ElementSerializer); 772 | 773 | TypeSerializer.prototype.parseNsAttributes = function(element) { 774 | 775 | // extracted attributes with serialization attribute 776 | // stripped; it may be later 777 | var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element).filter( 778 | attr => attr.name !== this.serialization 779 | ); 780 | 781 | var descriptor = element.$descriptor; 782 | 783 | // only serialize if necessary 784 | if (descriptor.name === this.propertyDescriptor.type) { 785 | return attributes; 786 | } 787 | 788 | var typeNs = this.typeNs = this.nsTagName(descriptor); 789 | this.getNamespaces().logUsed(this.typeNs); 790 | 791 | // add xsi:type attribute to represent the elements 792 | // actual type 793 | 794 | var pkg = element.$model.getPackage(typeNs.uri), 795 | typePrefix = (pkg.xml && pkg.xml.typePrefix) || ''; 796 | 797 | this.addAttribute( 798 | this.nsAttributeName(this.serialization), 799 | (typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName 800 | ); 801 | 802 | return attributes; 803 | }; 804 | 805 | TypeSerializer.prototype.isLocalNs = function(ns) { 806 | return ns.uri === (this.typeNs || this.ns).uri; 807 | }; 808 | 809 | function SavingWriter() { 810 | this.value = ''; 811 | 812 | this.write = function(str) { 813 | this.value += str; 814 | }; 815 | } 816 | 817 | function FormatingWriter(out, format) { 818 | 819 | var indent = [ '' ]; 820 | 821 | this.append = function(str) { 822 | out.write(str); 823 | 824 | return this; 825 | }; 826 | 827 | this.appendNewLine = function() { 828 | if (format) { 829 | out.write('\n'); 830 | } 831 | 832 | return this; 833 | }; 834 | 835 | this.appendIndent = function() { 836 | if (format) { 837 | out.write(indent.join(' ')); 838 | } 839 | 840 | return this; 841 | }; 842 | 843 | this.indent = function() { 844 | indent.push(''); 845 | return this; 846 | }; 847 | 848 | this.unindent = function() { 849 | indent.pop(); 850 | return this; 851 | }; 852 | } 853 | 854 | /** 855 | * A writer for meta-model backed document trees 856 | * 857 | * @param {Object} options output options to pass into the writer 858 | */ 859 | export function Writer(options) { 860 | 861 | options = assign({ format: false, preamble: true }, options || {}); 862 | 863 | function toXML(tree, writer) { 864 | var internalWriter = writer || new SavingWriter(); 865 | var formatingWriter = new FormatingWriter(internalWriter, options.format); 866 | 867 | if (options.preamble) { 868 | formatingWriter.append(XML_PREAMBLE); 869 | } 870 | 871 | var serializer = new ElementSerializer(); 872 | 873 | var model = tree.$model; 874 | 875 | serializer.getNamespaces().mapDefaultPrefixes(getDefaultPrefixMappings(model)); 876 | 877 | serializer.build(tree).serializeTo(formatingWriter); 878 | 879 | if (!writer) { 880 | return internalWriter.value; 881 | } 882 | } 883 | 884 | return { 885 | toXML: toXML 886 | }; 887 | } 888 | 889 | 890 | // helpers /////////// 891 | 892 | /** 893 | * @param {Moddle} model 894 | * 895 | * @return { Record } map from prefix to URI 896 | */ 897 | function getDefaultPrefixMappings(model) { 898 | 899 | const nsMap = model.config && model.config.nsMap || {}; 900 | 901 | const prefixMap = {}; 902 | 903 | // { prefix -> uri } 904 | for (const prefix in DEFAULT_NS_MAP) { 905 | prefixMap[prefix] = DEFAULT_NS_MAP[prefix]; 906 | } 907 | 908 | // { uri -> prefix } 909 | for (const uri in nsMap) { 910 | const prefix = nsMap[uri]; 911 | 912 | prefixMap[prefix] = uri; 913 | } 914 | 915 | for (const pkg of model.getPackages()) { 916 | prefixMap[pkg.prefix] = pkg.uri; 917 | } 918 | 919 | return prefixMap; 920 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moddle-xml", 3 | "version": "11.0.0", 4 | "description": "XML import/export for documents described with moddle", 5 | "scripts": { 6 | "all": "run-s lint test", 7 | "build": "rollup -c", 8 | "dev": "npm test -- --watch", 9 | "lint": "eslint .", 10 | "test": "mocha --reporter=spec --recursive test", 11 | "pretest": "run-s build", 12 | "prepare": "run-s build" 13 | }, 14 | "type": "module", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/bpmn-io/moddle-xml" 18 | }, 19 | "exports": { 20 | ".": { 21 | "import": "./dist/index.js", 22 | "require": "./dist/index.cjs" 23 | }, 24 | "./package.json": "./package.json" 25 | }, 26 | "keywords": [ 27 | "moddle", 28 | "model", 29 | "meta-model", 30 | "xml", 31 | "xsd", 32 | "import", 33 | "export" 34 | ], 35 | "author": { 36 | "name": "Nico Rehwaldt", 37 | "url": "https://github.com/Nikku" 38 | }, 39 | "contributors": [ 40 | { 41 | "name": "bpmn.io contributors", 42 | "url": "https://github.com/bpmn-io" 43 | } 44 | ], 45 | "engines": { 46 | "node": ">= 18" 47 | }, 48 | "files": [ 49 | "dist" 50 | ], 51 | "license": "MIT", 52 | "sideEffects": false, 53 | "devDependencies": { 54 | "chai": "^4.4.1", 55 | "eslint": "^9.12.0", 56 | "eslint-plugin-bpmn-io": "^2.0.2", 57 | "mocha": "^10.3.0", 58 | "moddle": "^7.0.0", 59 | "npm-run-all": "^4.1.5", 60 | "rollup": "^4.12.0" 61 | }, 62 | "dependencies": { 63 | "min-dash": "^4.0.0", 64 | "saxen": "^10.0.0" 65 | }, 66 | "peerDependencies": { 67 | "moddle": ">= 6.2.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | 4 | const pkg = JSON.parse(fs.readFileSync('./package.json')); 5 | 6 | const pkgExports = pkg.exports['.']; 7 | 8 | function pgl(plugins = []) { 9 | return plugins; 10 | } 11 | 12 | const srcEntry = 'lib/index.js'; 13 | 14 | export default [ 15 | { 16 | input: srcEntry, 17 | output: [ 18 | { file: pkgExports.require, format: 'cjs', sourcemap: true }, 19 | { file: pkgExports.import, format: 'es', sourcemap: true } 20 | ], 21 | external: [ 22 | 'min-dash', 23 | 'moddle', 24 | 'saxen' 25 | ], 26 | plugins: pgl() 27 | } 28 | ]; -------------------------------------------------------------------------------- /test/expect.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import Matchers from './matchers.js'; 4 | 5 | // add matchers 6 | chai.use(Matchers); 7 | 8 | // expose chai expect 9 | export { 10 | expect as default 11 | } from 'chai'; -------------------------------------------------------------------------------- /test/fixtures/error/binary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/moddle-xml/249c32bde27e44a2985e98fc07d452a72e571e95/test/fixtures/error/binary.png -------------------------------------------------------------------------------- /test/fixtures/error/no-xml.txt: -------------------------------------------------------------------------------- 1 | this is no xml -------------------------------------------------------------------------------- /test/fixtures/model/attr-child-conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "S", 3 | "uri": "http://s", 4 | "prefix": "s", 5 | "xml": { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "Foo", 11 | "properties": [ 12 | { "name": "bar", "type": "String", "isAttr": true }, 13 | { "name": "bars", "type": "Bar", "isMany": true } 14 | ] 15 | }, 16 | { 17 | "name": "Bar", 18 | "properties": [ 19 | { "name": "woop", "type": "String", "isAttr": true } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /test/fixtures/model/datatype-aliased.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataTypes Aliased", 3 | "uri": "http://datatypes-aliased", 4 | "prefix": "da", 5 | "xml": { 6 | "typePrefix": "t" 7 | }, 8 | "types": [ 9 | { 10 | "name": "Root", 11 | "superClass": [ "dt:Root" ] 12 | }, 13 | { 14 | "name": "Rect", 15 | "properties": [ 16 | { "name": "z", "type": "Integer", "isAttr": true } 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /test/fixtures/model/datatype-external.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataTypes 2", 3 | "uri": "http://datatypes2", 4 | "prefix": "do", 5 | "types": [ 6 | { 7 | "name": "Rect", 8 | "properties": [ 9 | { "name": "x", "type": "Integer", "isAttr": true } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/fixtures/model/datatype.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataTypes", 3 | "uri": "http://datatypes", 4 | "prefix": "dt", 5 | "xml" : { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "Root", 11 | "properties": [ 12 | { "name": "bounds", "type": "Rect", "xml": { "serialize" : "xsi:type" } }, 13 | { "name": "otherBounds", "type": "Rect", "xml": { "serialize" : "xsi:type" }, "isMany": true }, 14 | { "name": "xmiBounds", "type": "Rect", "xml": { "serialize" : "xmi:type" } }, 15 | { "name": "xmiManyBounds", "type": "Rect", "xml": { "serialize" : "xmi:type" }, "isMany": true } 16 | ] 17 | }, 18 | { 19 | "name": "Rect", 20 | "properties": [ 21 | { "name": "y", "type": "Integer", "isAttr": true } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /test/fixtures/model/extension/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base", 3 | "uri": "http://base", 4 | "prefix": "b", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "own", "type": "Own" }, 10 | { "name": "ownAttr", "type": "String", "isAttr": true }, 11 | { "name": "generic", "type": "Element" }, 12 | { "name": "genericCollection", "type": "Element", "isMany": true } 13 | ] 14 | }, 15 | { 16 | "name": "SubRoot", 17 | "superClass": [ "Root" ], 18 | "properties": [ 19 | { "name": "subAttr", "type": "String", "isAttr": true } 20 | ] 21 | }, 22 | { 23 | "name": "Own", 24 | "properties": [ 25 | { "name": "count", "type": "Integer", "isAttr": true } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /test/fixtures/model/extension/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom", 3 | "uri": "http://custom", 4 | "prefix": "c", 5 | "types": [ 6 | { 7 | "name": "CustomRoot", 8 | "extends": [ "b:Root" ], 9 | "properties": [ 10 | { "name": "customAttr", "type": "Integer", "isAttr": true }, 11 | { "name": "generic", "type": "CustomGeneric", "redefines": "b:Root#generic" } 12 | ] 13 | }, 14 | { 15 | "name": "CustomGeneric", 16 | "superClass": [ "Element" ], 17 | "properties": [ 18 | { "name": "count", "type": "Integer", "isAttr": true } 19 | ] 20 | }, 21 | { 22 | "name": "Property", 23 | "superClass": [ "Element" ], 24 | "properties": [ 25 | { "name": "key", "type": "String", "isAttr": true }, 26 | { "name": "value", "type": "String", "isAttr": true } 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /test/fixtures/model/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extensions", 3 | "uri": "http://extensions", 4 | "prefix": "e", 5 | "xml" : { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "Root", 11 | "properties": [ 12 | { "name": "id", "type": "String", "isId": true }, 13 | { "name": "extensions", "type": "Element", "isMany" : true } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /test/fixtures/model/fake-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FakeId", 3 | "uri": "http://fakeid", 4 | "prefix": "fi", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { 10 | "name": "children", 11 | "type": "ChildWithFakeId", 12 | "isMany": true 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "ChildWithFakeId", 18 | "properties": [ 19 | { 20 | "name": "id", 21 | "type": "String", 22 | "isAttr": true 23 | }, 24 | { 25 | "name": "ref", 26 | "type": "String", 27 | "isReference": true, 28 | "isAttr": true 29 | } 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /test/fixtures/model/noalias.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NoAlias", 3 | "uri": "http://noalias", 4 | "prefix": "na", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "id", "type": "Integer", "isAttr": true, "isId": true } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/fixtures/model/properties-extended.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extended", 3 | "uri": "http://extended", 4 | "prefix": "ext", 5 | "xml" : { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "ExtendedComplex", 11 | "superClass": [ "props:ComplexCount" ], 12 | "properties": [ 13 | { "name": "numCount", "type": "Integer", "isAttr": true, "redefines": "props:Complex#count" } 14 | ] 15 | }, 16 | { 17 | "name": "Root", 18 | "superClass": [ "props:Root" ], 19 | "properties": [ 20 | { "name": "elements", "type": "Base", "isMany": true } 21 | ] 22 | }, 23 | { 24 | "name": "Base" 25 | }, 26 | { 27 | "name": "CABSBase" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /test/fixtures/model/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Properties", 3 | "uri": "http://properties", 4 | "prefix": "props", 5 | "xml" : { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "Complex", 11 | "properties": [ 12 | { "name": "id", "type": "String", "isAttr": true, "isId": true } 13 | ] 14 | }, 15 | { 16 | "name": "ComplexAttrs", 17 | "superClass": [ "Complex" ], 18 | "properties": [ 19 | { "name": "attrs", "type": "Attributes", "xml": { "serialize" : "xsi:type" } } 20 | ] 21 | }, 22 | { 23 | "name": "ComplexAttrsCol", 24 | "superClass": [ "Complex" ], 25 | "properties": [ 26 | { "name": "attrs", "type": "Attributes", "isMany": true, "xml": { "serialize" : "xsi:type" } } 27 | ] 28 | }, 29 | { 30 | "name": "ComplexCount", 31 | "superClass": [ "Complex" ], 32 | "properties": [ 33 | { "name": "count", "type": "Integer", "isAttr": true } 34 | ] 35 | }, 36 | { 37 | "name": "ComplexNesting", 38 | "superClass": [ "Complex" ], 39 | "properties": [ 40 | { "name": "nested", "type": "Complex", "isMany": true } 41 | ] 42 | }, 43 | { 44 | "name": "Body" 45 | }, 46 | { 47 | "name": "SimpleBody", 48 | "superClass": [ "Base", "Body" ], 49 | "properties": [ 50 | { 51 | "name": "body", 52 | "type": "String", 53 | "isBody": true 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "WithBody", 59 | "superClass": [ "Base" ], 60 | "properties": [ 61 | { 62 | "name": "someBody", 63 | "type": "Body", 64 | "xml": { "serialize" : "xsi:type" } 65 | } 66 | ] 67 | }, 68 | { 69 | "name": "SimpleBodyProperties", 70 | "superClass": [ "Base" ], 71 | "properties": [ 72 | { 73 | "name": "intValue", 74 | "type": "Integer" 75 | }, 76 | { 77 | "name": "boolValue", 78 | "type": "Boolean" 79 | }, 80 | { 81 | "name": "str", 82 | "type": "String", 83 | "isMany": true 84 | } 85 | ] 86 | }, 87 | { 88 | "name": "WithProperty", 89 | "superClass": [ "Base" ], 90 | "properties": [ 91 | { "name": "propertyName", "type": "Base", "xml": { "serialize" : "property" } } 92 | ] 93 | }, 94 | { 95 | "name": "Base" 96 | }, 97 | { 98 | "name": "BaseWithId", 99 | "superClass": [ "Base" ], 100 | "properties": [ 101 | { "name": "id", "type": "String", "isAttr": true, "isId": true } 102 | ] 103 | }, 104 | { 105 | "name": "BaseWithNumericId", 106 | "superClass": [ "BaseWithId" ], 107 | "properties": [ 108 | { "name": "idNumeric", "type": "String", "isAttr": true, "redefines": "BaseWithId#id", "isId": true } 109 | ] 110 | }, 111 | { 112 | "name": "Attributes", 113 | "superClass": [ "BaseWithId" ], 114 | "properties": [ 115 | { 116 | "name": "realValue", 117 | "type": "Real", 118 | "isAttr": true 119 | }, 120 | { 121 | "name": "integerValue", 122 | "type": "Integer", 123 | "isAttr": true 124 | }, 125 | { 126 | "name": "booleanValue", 127 | "type": "Boolean", 128 | "isAttr": true 129 | }, 130 | { 131 | "name": "defaultBooleanValue", 132 | "type": "Boolean", 133 | "isAttr": true, 134 | "default": true 135 | } 136 | ] 137 | }, 138 | { 139 | "name": "SubAttributes", 140 | "superClass": [ "Attributes" ] 141 | }, 142 | { 143 | "name": "Root", 144 | "properties": [ 145 | { 146 | "name": "any", 147 | "type": "Base", 148 | "isMany": true 149 | }, 150 | { 151 | "name": "id", 152 | "type": "String", 153 | "isAttr": true, 154 | "isId": true 155 | } 156 | ] 157 | }, 158 | { 159 | "name": "Embedding", 160 | "superClass": [ "BaseWithId" ], 161 | "properties": [ 162 | { 163 | "name": "embeddedComplex", 164 | "type": "Complex" 165 | } 166 | ] 167 | }, 168 | { 169 | "name": "ReferencingSingle", 170 | "superClass": [ "BaseWithId" ], 171 | "properties": [ 172 | { 173 | "name": "referencedComplex", 174 | "type": "Complex", 175 | "isReference": true, 176 | "isAttr": true 177 | } 178 | ] 179 | }, 180 | { 181 | "name": "ReferencingCollection", 182 | "superClass": [ "BaseWithId" ], 183 | "properties": [ 184 | { 185 | "name": "references", 186 | "type": "Complex", 187 | "isReference": true, 188 | "isMany": true 189 | } 190 | ] 191 | }, 192 | { 193 | "name": "ContainedCollection", 194 | "superClass": [ "BaseWithId" ], 195 | "properties": [ 196 | { 197 | "name": "children", 198 | "type": "Complex", 199 | "isMany": true 200 | } 201 | ] 202 | }, 203 | { 204 | "name": "AttributeReferenceCollection", 205 | "superClass": [ "BaseWithId" ], 206 | "properties": [ 207 | { 208 | "name": "refs", 209 | "type": "Complex", 210 | "isReference": true, 211 | "isMany": true, 212 | "isAttr": true 213 | } 214 | ] 215 | } 216 | ] 217 | } 218 | -------------------------------------------------------------------------------- /test/fixtures/model/redefine.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Redefine", 3 | "uri": "http://redefine", 4 | "prefix": "r", 5 | "types": [ 6 | { 7 | "name": "Base", 8 | "properties": [ 9 | { "name": "id", "type": "Integer" }, 10 | { "name": "name", "type": "String" } 11 | ] 12 | }, 13 | { 14 | "name": "Extension", 15 | "superClass": [ "Base" ], 16 | "properties": [ 17 | { "name": "value", "type": "String" }, 18 | { "name": "id", "type": "Integer", "redefines": "Base#id" } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/fixtures/model/replace.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Replace", 3 | "uri": "http://replace", 4 | "prefix": "r", 5 | "types": [ 6 | { 7 | "name": "Base", 8 | "properties": [ 9 | { "name": "id", "type": "Integer" }, 10 | { "name": "name", "type": "String" } 11 | ] 12 | }, 13 | { 14 | "name": "Extension", 15 | "superClass": [ "Base" ], 16 | "properties": [ 17 | { "name": "value", "type": "String" }, 18 | { "name": "id", "type": "Integer", "replaces": "Base#id" } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/fixtures/model/virtual.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virtual", 3 | "uri": "http://virtual", 4 | "prefix": "virt", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "child", "type": "Child", "isVirtual": true } 10 | ] 11 | }, 12 | { 13 | "name": "Child" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/model/xmi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XML", 3 | "uri": "http://www.omg.org/spec/XMI/20131001", 4 | "prefix": "xmi", 5 | "types": [ 6 | { 7 | "name": "XMI", 8 | "properties": [ 9 | { "name": "elements", "type": "Element", "isMany": true } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { 4 | map 5 | } from 'min-dash'; 6 | 7 | import { Moddle } from 'moddle'; 8 | 9 | 10 | export function ensureDirExists(dir) { 11 | 12 | if (!fs.existsSync(dir)) { 13 | fs.mkdirSync(dir); 14 | } 15 | } 16 | 17 | export function readFile(filename) { 18 | return fs.readFileSync(filename, { encoding: 'UTF-8' }); 19 | } 20 | 21 | export function createModelBuilder(base) { 22 | 23 | var cache = {}; 24 | 25 | if (!base) { 26 | throw new Error('[test-util] must specify a base directory'); 27 | } 28 | 29 | function createModel(packageNames, options = {}) { 30 | 31 | var packages = map(packageNames, function(f) { 32 | var pkg = cache[f]; 33 | var file = base + f + '.json'; 34 | 35 | if (!pkg) { 36 | try { 37 | pkg = cache[f] = JSON.parse(readFile(base + f + '.json')); 38 | } catch (e) { 39 | throw new Error('[Helper] failed to parse <' + file + '> as JSON: ' + e.message); 40 | } 41 | } 42 | 43 | return pkg; 44 | }); 45 | 46 | return new Moddle(packages, { strict: true, ...options }); 47 | } 48 | 49 | return createModel; 50 | } -------------------------------------------------------------------------------- /test/integration/distro.cjs: -------------------------------------------------------------------------------- 1 | const { 2 | expect 3 | } = require('chai'); 4 | 5 | const pkg = require('../../package.json'); 6 | 7 | const pkgExports = pkg.exports['.']; 8 | 9 | 10 | describe('integration', function() { 11 | 12 | describe('distro', function() { 13 | 14 | it('should expose CJS bundle', function() { 15 | 16 | const { 17 | Reader, 18 | Writer 19 | } = require('../../' + pkgExports['require']); 20 | 21 | expect(Reader).to.exist; 22 | expect(Writer).to.exist; 23 | }); 24 | 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/matchers.js: -------------------------------------------------------------------------------- 1 | export default function(chai, utils) { 2 | 3 | utils.addMethod(chai.Assertion.prototype, 'jsonEqual', function(comparison) { 4 | 5 | var actual = JSON.stringify(this._obj); 6 | var expected = JSON.stringify(comparison); 7 | 8 | this.assert( 9 | actual == expected, 10 | 'expected #{this} to deep equal #{act}', 11 | 'expected #{this} not to deep equal #{act}', 12 | comparison, // expected 13 | this._obj, // actual 14 | true // show diff 15 | ); 16 | }); 17 | } -------------------------------------------------------------------------------- /test/spec/roundtrip.uml.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import fs from 'node:fs'; 4 | 5 | import { 6 | assign 7 | } from 'min-dash'; 8 | 9 | import { 10 | Reader, 11 | Writer 12 | } from 'moddle-xml'; 13 | 14 | import { 15 | createModelBuilder 16 | } from '../helper.js'; 17 | 18 | 19 | describe('Roundtrip - UML', function() { 20 | 21 | var createModel = createModelBuilder('test/fixtures/model/'); 22 | 23 | var createWriter = function(model, options) { 24 | return new Writer(assign({ preamble: false }, options || {})); 25 | }; 26 | 27 | 28 | it('should roundtrip UML', async function() { 29 | 30 | // given 31 | const xmiModdle = createModel([ 'xmi' ]); 32 | 33 | const reader = new Reader(xmiModdle); 34 | const writer = createWriter(xmiModdle, { 35 | format: true, 36 | preamble: true 37 | }); 38 | 39 | const rootHandler = reader.handler('xmi:XMI'); 40 | 41 | const input = fs.readFileSync('test/fixtures/xml/UML.xmi', 'utf-8') 42 | .replace(/"\/>/g, '" />') 43 | .replace(/ /g, ''); 44 | 45 | // when 46 | const { 47 | rootElement 48 | } = await reader.fromXML(input, rootHandler); 49 | 50 | const output = writer.toXML(rootElement); 51 | 52 | // then 53 | expect(output).to.eql(input); 54 | }); 55 | 56 | }); -------------------------------------------------------------------------------- /test/spec/rountrip.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | assign 5 | } from 'min-dash'; 6 | 7 | import { 8 | Reader, 9 | Writer 10 | } from 'moddle-xml'; 11 | 12 | import { 13 | createModelBuilder 14 | } from '../helper.js'; 15 | 16 | 17 | describe('Roundtrip', function() { 18 | 19 | var createModel = createModelBuilder('test/fixtures/model/'); 20 | 21 | var createWriter = function(model, options) { 22 | return new Writer(assign({ preamble: false }, options || {})); 23 | }; 24 | 25 | 26 | it('should strip unused global', async function() { 27 | 28 | // given 29 | var extendedModel = createModel([ 'properties', 'properties-extended' ]); 30 | 31 | var reader = new Reader(extendedModel); 32 | var writer = createWriter(extendedModel); 33 | 34 | var rootHandler = reader.handler('ext:Root'); 35 | 36 | var input = 37 | '' + 38 | '' + 39 | ''; 40 | 41 | // when 42 | var { 43 | rootElement 44 | } = await reader.fromXML(input, rootHandler); 45 | 46 | var output = writer.toXML(rootElement); 47 | 48 | // then 49 | expect(output).to.eql( 50 | '' + 51 | '' + 52 | '' 53 | ); 54 | }); 55 | 56 | 57 | it('should strip unused ', async function() { 58 | 59 | // given 60 | var extendedModel = createModel([ 'datatype' ]); 61 | 62 | var reader = new Reader(extendedModel); 63 | var writer = createWriter(extendedModel); 64 | 65 | var rootHandler = reader.handler('dt:Root'); 66 | 67 | var input = 68 | '' + 69 | '' + 70 | ''; 71 | 72 | // when 73 | var { 74 | rootElement 75 | } = await reader.fromXML(input, rootHandler); 76 | 77 | var output = writer.toXML(rootElement); 78 | 79 | // then 80 | expect(output).to.eql( 81 | '' + 82 | '' + 83 | '' 84 | ); 85 | }); 86 | 87 | 88 | it('should reuse global namespace', async function() { 89 | 90 | // given 91 | var extendedModel = createModel([ 'properties', 'properties-extended' ]); 92 | 93 | var reader = new Reader(extendedModel); 94 | var writer = createWriter(extendedModel); 95 | 96 | var rootHandler = reader.handler('props:ComplexNesting'); 97 | 98 | var input = 99 | '' + 100 | '' + 101 | '' + 102 | '' + 103 | ''; 104 | 105 | // when 106 | var { 107 | rootElement 108 | } = await reader.fromXML(input, rootHandler); 109 | 110 | var output = writer.toXML(rootElement); 111 | 112 | expect(output).to.eql( 113 | '' + 114 | '' + 115 | '' + 116 | '' + 117 | '' 118 | ); 119 | }); 120 | 121 | 122 | it('should keep default namespace', async function() { 123 | 124 | // given 125 | var extendedModel = createModel([ 'properties' ]); 126 | 127 | var reader = new Reader(extendedModel); 128 | var writer = createWriter(extendedModel); 129 | 130 | var rootHandler = reader.handler('props:ComplexNesting'); 131 | 132 | var input = ''; 133 | 134 | // when 135 | var { 136 | rootElement 137 | } = await reader.fromXML(input, rootHandler); 138 | 139 | var output = writer.toXML(rootElement); 140 | 141 | // then 142 | expect(output).to.eql( 143 | '' 144 | ); 145 | }); 146 | 147 | 148 | it('should de-duplicate attribute names', async function() { 149 | 150 | // given 151 | var extendedModel = createModel([ 'extension/base' ]); 152 | var reader = new Reader(extendedModel); 153 | var writer = createWriter(extendedModel); 154 | 155 | var rootHandler = reader.handler('b:Root'); 156 | 157 | var input = '' + 158 | '' + 159 | ''; 160 | 161 | // when 162 | var { 163 | rootElement 164 | } = await reader.fromXML(input, rootHandler); 165 | 166 | var output = writer.toXML(rootElement); 167 | 168 | // then 169 | expect(output).to.eql( 170 | '' + 171 | '' + 172 | ''); 173 | }); 174 | 175 | 176 | describe('generic', function() { 177 | 178 | it('should keep local ns attribute', async function() { 179 | 180 | // given 181 | var extendedModel = createModel([ 'extensions' ]); 182 | 183 | var reader = new Reader(extendedModel); 184 | var writer = createWriter(extendedModel); 185 | 186 | var rootHandler = reader.handler('e:Root'); 187 | 188 | var input = 189 | '' + 190 | '' + 191 | '' + 192 | '' + 193 | ''; 194 | 195 | // when 196 | var { 197 | rootElement 198 | } = await reader.fromXML(input, rootHandler); 199 | 200 | var output = writer.toXML(rootElement); 201 | 202 | // then 203 | expect(output).to.eql(input); 204 | }); 205 | 206 | 207 | it('should keep local ', async function() { 208 | 209 | // given 210 | var extendedModel = createModel([ 'extensions' ]); 211 | 212 | var reader = new Reader(extendedModel); 213 | var writer = createWriter(extendedModel); 214 | 215 | var rootHandler = reader.handler('e:Root'); 216 | 217 | var input = 218 | '' + 219 | '' + 220 | '' + 221 | '' + 222 | ''; 223 | 224 | // when 225 | var { 226 | rootElement 227 | } = await reader.fromXML(input, rootHandler); 228 | 229 | var output = writer.toXML(rootElement); 230 | 231 | // then 232 | expect(output).to.eql(input); 233 | }); 234 | 235 | 236 | it('should keep local (renamed)', async function() { 237 | 238 | // given 239 | var extendedModel = createModel([ 'extensions' ]); 240 | 241 | var reader = new Reader(extendedModel); 242 | var writer = createWriter(extendedModel); 243 | 244 | var rootHandler = reader.handler('e:Root'); 245 | 246 | var input = 247 | '' + 248 | '' + 249 | '' + 250 | '' + 251 | ''; 252 | 253 | // when 254 | var { 255 | rootElement 256 | } = await reader.fromXML(input, rootHandler); 257 | 258 | var output = writer.toXML(rootElement); 259 | 260 | // then 261 | expect(output).to.eql(input); 262 | }); 263 | 264 | 265 | it('should keep generic ', async function() { 266 | 267 | // given 268 | var extendedModel = createModel([ 'extensions' ]); 269 | 270 | var reader = new Reader(extendedModel); 271 | var writer = createWriter(extendedModel); 272 | 273 | var rootHandler = reader.handler('e:Root'); 274 | 275 | var input = 276 | '' + 277 | '' + 278 | '' + 279 | '' + 280 | ''; 281 | 282 | // when 283 | var { 284 | rootElement 285 | } = await reader.fromXML(input, rootHandler); 286 | 287 | var output = writer.toXML(rootElement); 288 | 289 | // then 290 | expect(output).to.eql(input); 291 | }); 292 | 293 | 294 | it('should keep generic ', async function() { 295 | 296 | // given 297 | var extendedModel = createModel([ 'extensions' ]); 298 | 299 | var reader = new Reader(extendedModel); 300 | var writer = createWriter(extendedModel); 301 | 302 | var rootHandler = reader.handler('e:Root'); 303 | 304 | var input = 305 | '' + 306 | '' + 307 | ''; 308 | 309 | // when 310 | var { 311 | rootElement 312 | } = await reader.fromXML(input, rootHandler); 313 | 314 | var output = writer.toXML(rootElement); 315 | 316 | // then 317 | expect(output).to.eql(input); 318 | }); 319 | 320 | 321 | it('should keep generic (local)', async function() { 322 | 323 | // given 324 | var extendedModel = createModel([ 'extensions' ]); 325 | 326 | var reader = new Reader(extendedModel); 327 | var writer = createWriter(extendedModel); 328 | 329 | var rootHandler = reader.handler('e:Root'); 330 | 331 | var input = 332 | '' + 333 | '' + 334 | ''; 335 | 336 | // when 337 | var { 338 | rootElement 339 | } = await reader.fromXML(input, rootHandler); 340 | 341 | var output = writer.toXML(rootElement); 342 | 343 | // then 344 | expect(output).to.eql(input); 345 | }); 346 | 347 | 348 | it('should keep generic (nested)', async function() { 349 | 350 | // given 351 | var extendedModel = createModel([ 'extensions' ]); 352 | 353 | var reader = new Reader(extendedModel); 354 | var writer = createWriter(extendedModel); 355 | 356 | var rootHandler = reader.handler('e:Root'); 357 | 358 | var input = 359 | '' + 360 | '' + 361 | '' + 362 | '' + 363 | ''; 364 | 365 | // when 366 | var { 367 | rootElement 368 | } = await reader.fromXML(input, rootHandler); 369 | 370 | var output = writer.toXML(rootElement); 371 | 372 | // then 373 | expect(output).to.eql(input); 374 | }); 375 | 376 | }); 377 | 378 | 379 | describe('custom namespace mapping', function() { 380 | 381 | it('should preserve remapped xmi:type', async function() { 382 | 383 | // given 384 | var datatypesModel = createModel([ 385 | 'datatype', 386 | 'datatype-external' 387 | ], { 388 | nsMap: { 389 | 'http://www.omg.org/spec/XMI/20131001': 'xmi' 390 | } 391 | }); 392 | 393 | var reader = new Reader(datatypesModel); 394 | var writer = createWriter(datatypesModel); 395 | 396 | var rootHandler = reader.handler('dt:Root'); 397 | 398 | var input = 399 | '' + 400 | '' + 404 | ''; 405 | 406 | // when 407 | var { 408 | rootElement 409 | } = await reader.fromXML(input, rootHandler); 410 | 411 | var output = writer.toXML(rootElement); 412 | 413 | // then 414 | expect(output).to.eql(input); 415 | }); 416 | 417 | 418 | it('should keep remapped generic prefix', async function() { 419 | 420 | // given 421 | var extensionModel = createModel([ 'extensions' ], { 422 | nsMap: { 423 | 'http://other': 'o', 424 | 'http://foo': 'f' 425 | } 426 | }); 427 | 428 | // given 429 | var reader = new Reader(extensionModel); 430 | var writer = createWriter(extensionModel); 431 | 432 | var rootHandler = reader.handler('e:Root'); 433 | 434 | var input = 435 | '' + 436 | '' + 437 | '' + 438 | '' + 439 | '' + 440 | '' + 441 | '' + 442 | ''; 443 | 444 | // when 445 | var { 446 | rootElement 447 | } = await reader.fromXML(input, rootHandler); 448 | 449 | var output = writer.toXML(rootElement); 450 | 451 | // then 452 | expect(output).to.eql(input); 453 | }); 454 | 455 | }); 456 | 457 | }); 458 | -------------------------------------------------------------------------------- /test/spec/writer.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | Writer 5 | } from 'moddle-xml'; 6 | 7 | import { 8 | createModelBuilder 9 | } from '../helper.js'; 10 | 11 | import { 12 | assign 13 | } from 'min-dash'; 14 | 15 | 16 | describe('Writer', function() { 17 | 18 | var createModel = createModelBuilder('test/fixtures/model/'); 19 | 20 | function createWriter(model, options) { 21 | return new Writer(assign({ preamble: false }, options || {})); 22 | } 23 | 24 | 25 | describe('should export', function() { 26 | 27 | describe('base', function() { 28 | 29 | var model = createModel([ 'properties' ]); 30 | 31 | it('should write xml preamble', function() { 32 | 33 | // given 34 | var writer = new Writer({ preamble: true }); 35 | var root = model.create('props:Root'); 36 | 37 | // when 38 | var xml = writer.toXML(root); 39 | 40 | // then 41 | expect(xml).to.eql( 42 | '\n' + 43 | ''); 44 | }); 45 | }); 46 | 47 | 48 | describe('datatypes', function() { 49 | 50 | var datatypesModel = createModel([ 51 | 'datatype', 52 | 'datatype-external', 53 | 'datatype-aliased' 54 | ], { 55 | nsMap: { 56 | 'http://www.omg.org/spec/XMI/20131001': 'xmi' 57 | } 58 | }); 59 | 60 | 61 | it('via xsi:type', function() { 62 | 63 | // given 64 | var writer = createWriter(datatypesModel); 65 | 66 | var root = datatypesModel.create('dt:Root'); 67 | 68 | root.set('bounds', datatypesModel.create('dt:Rect', { y: 100 })); 69 | 70 | // when 71 | var xml = writer.toXML(root); 72 | 73 | // then 74 | expect(xml).to.eql( 75 | '' + 76 | '' + 77 | ''); 78 | }); 79 | 80 | 81 | it('via xmi:type', function() { 82 | 83 | // given 84 | var writer = createWriter(datatypesModel); 85 | 86 | var root = datatypesModel.create('dt:Root'); 87 | 88 | root.set('xmiBounds', datatypesModel.create('dt:Rect', { y: 100 })); 89 | 90 | // when 91 | var xml = writer.toXML(root); 92 | 93 | // then 94 | expect(xml).to.eql( 95 | '' + 96 | '' + 97 | ''); 98 | }); 99 | 100 | 101 | it('via xsi:type / default / extension attributes', function() { 102 | 103 | // given 104 | var writer = createWriter(datatypesModel); 105 | 106 | var root = datatypesModel.create('dt:Root'); 107 | 108 | root.set('bounds', datatypesModel.create('dt:Rect', { 109 | y: 100, 110 | 'xmlns:f': 'http://foo', 111 | 'f:bar': 'BAR' 112 | })); 113 | 114 | // when 115 | var xml = writer.toXML(root); 116 | 117 | // then 118 | expect(xml).to.eql( 119 | '' + 120 | '' + 121 | ''); 122 | }); 123 | 124 | 125 | it('via xsi:type / explicit / extension attributes', function() { 126 | 127 | // given 128 | var writer = createWriter(datatypesModel); 129 | 130 | var root = datatypesModel.create('dt:Root'); 131 | 132 | root.set('bounds', datatypesModel.create('do:Rect', { 133 | x: 100, 134 | 'xmlns:f': 'http://foo', 135 | 'f:bar': 'BAR' 136 | })); 137 | 138 | // when 139 | var xml = writer.toXML(root); 140 | 141 | // then 142 | expect(xml).to.eql( 143 | '' + 144 | '' + 148 | '' 149 | ); 150 | }); 151 | 152 | 153 | it('via xsi:type / explicit / local ns declaration', function() { 154 | 155 | // given 156 | var writer = createWriter(datatypesModel); 157 | 158 | var root = datatypesModel.create('dt:Root'); 159 | 160 | root.set('bounds', datatypesModel.create('do:Rect', { 161 | x: 100, 162 | 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' 163 | })); 164 | 165 | // when 166 | var xml = writer.toXML(root); 167 | 168 | // then 169 | expect(xml).to.eql( 170 | '' + 171 | '' + 175 | '' 176 | ); 177 | }); 178 | 179 | 180 | it('via xsi:type / overriding existing attr', function() { 181 | 182 | // given 183 | var writer = createWriter(datatypesModel); 184 | 185 | var root = datatypesModel.create('dt:Root', { 186 | 'xmlns:foo': 'http://datatypes', 187 | 'xmlns:bar': 'http://datatypes2' 188 | }); 189 | 190 | root.set('bounds', datatypesModel.create('do:Rect', { 191 | x: 100, 192 | 'xsi:type': 'other:Rect' 193 | })); 194 | 195 | // when 196 | var xml = writer.toXML(root); 197 | 198 | // then 199 | expect(xml).to.eql( 200 | '' + 204 | '' + 205 | '' 206 | ); 207 | }); 208 | 209 | 210 | it('via xmi:type / implicit / extension attributes', function() { 211 | 212 | // given 213 | var writer = createWriter(datatypesModel); 214 | 215 | var root = datatypesModel.create('dt:Root'); 216 | 217 | root.set('xmiBounds', datatypesModel.create('do:Rect', { 218 | x: 100, 219 | 'xmlns:f': 'http://foo', 220 | 'f:bar': 'BAR' 221 | })); 222 | 223 | // when 224 | var xml = writer.toXML(root); 225 | 226 | // then 227 | expect(xml).to.eql( 228 | '' + 229 | '' + 233 | '' 234 | ); 235 | }); 236 | 237 | 238 | it('via xsi:type / no namespace', function() { 239 | 240 | // given 241 | var writer = createWriter(datatypesModel); 242 | 243 | var root = datatypesModel.create('dt:Root', { ':xmlns': 'http://datatypes' }); 244 | 245 | root.set('bounds', datatypesModel.create('dt:Rect', { y: 100 })); 246 | 247 | // when 248 | var xml = writer.toXML(root); 249 | 250 | // then 251 | expect(xml).to.eql( 252 | '' + 253 | '' + 254 | '' 255 | ); 256 | }); 257 | 258 | 259 | it('via xsi:type / other namespace', function() { 260 | 261 | // given 262 | var writer = createWriter(datatypesModel); 263 | 264 | var root = datatypesModel.create('dt:Root', { 'xmlns:a' : 'http://datatypes' }); 265 | 266 | root.set('bounds', datatypesModel.create('dt:Rect', { y: 100 })); 267 | 268 | // when 269 | var xml = writer.toXML(root); 270 | 271 | // then 272 | expect(xml).to.eql( 273 | '' + 274 | '' + 275 | '' 276 | ); 277 | }); 278 | 279 | 280 | it('via xsi:type / in collection / other namespace)', function() { 281 | 282 | // given 283 | var writer = createWriter(datatypesModel); 284 | 285 | var root = datatypesModel.create('dt:Root'); 286 | 287 | var otherBounds = root.get('otherBounds'); 288 | 289 | otherBounds.push(datatypesModel.create('dt:Rect', { y: 200 })); 290 | otherBounds.push(datatypesModel.create('do:Rect', { x: 100 })); 291 | 292 | // when 293 | var xml = writer.toXML(root); 294 | 295 | // then 296 | expect(xml).to.eql( 297 | '' + 300 | '' + 301 | '' + 302 | '' 303 | ); 304 | }); 305 | 306 | 307 | it('via xsi:type / in collection / type prefix', function() { 308 | 309 | // given 310 | var writer = createWriter(datatypesModel); 311 | 312 | var root = datatypesModel.create('dt:Root'); 313 | 314 | var otherBounds = root.get('otherBounds'); 315 | 316 | otherBounds.push(datatypesModel.create('da:Rect', { z: 200 })); 317 | otherBounds.push(datatypesModel.create('dt:Rect', { y: 100 })); 318 | 319 | // when 320 | var xml = writer.toXML(root); 321 | 322 | // then 323 | expect(xml).to.eql( 324 | '' + 327 | '' + 328 | '' + 329 | '' 330 | ); 331 | }); 332 | 333 | 334 | it('via xsi:type / body property', function() { 335 | 336 | var propertiesModel = createModel([ 'properties' ]); 337 | 338 | // given 339 | var writer = createWriter(propertiesModel); 340 | 341 | var body = propertiesModel.create('props:SimpleBody', { 342 | body: '${ foo < bar }' 343 | }); 344 | var root = propertiesModel.create('props:WithBody', { 345 | someBody: body 346 | }); 347 | 348 | // when 349 | var xml = writer.toXML(root); 350 | 351 | // then 352 | expect(xml).to.eql( 353 | '' + 355 | '' + 356 | '${ foo < bar }' + 357 | '' + 358 | '' 359 | ); 360 | }); 361 | 362 | 363 | it('via xsi:type / body property / formated', function() { 364 | 365 | var propertiesModel = createModel([ 'properties' ]); 366 | 367 | // given 368 | var writer = createWriter(propertiesModel, { format: true }); 369 | 370 | var body = propertiesModel.create('props:SimpleBody', { body: '${ foo < bar }' }); 371 | var root = propertiesModel.create('props:WithBody', { someBody: body }); 372 | 373 | // when 374 | var xml = writer.toXML(root); 375 | 376 | // then 377 | expect(xml).to.eql( 378 | '\n' + 380 | ' ${ foo < bar }\n' + 381 | '\n' 382 | ); 383 | }); 384 | 385 | 386 | it('keep empty tag', function() { 387 | 388 | // given 389 | var replaceModel = createModel([ 'replace' ]); 390 | 391 | var writer = createWriter(replaceModel); 392 | 393 | var simple = replaceModel.create('r:Extension', { value: '' }); 394 | 395 | // when 396 | var xml = writer.toXML(simple); 397 | 398 | var expectedXml = 399 | '' + 400 | '' + 401 | ''; 402 | 403 | // then 404 | expect(xml).to.eql(expectedXml); 405 | }); 406 | 407 | }); 408 | 409 | 410 | describe('attributes', function() { 411 | 412 | it('with line breaks', function() { 413 | 414 | // given 415 | var model = createModel([ 'properties' ]); 416 | 417 | var writer = createWriter(model); 418 | 419 | var root = model.create('props:BaseWithId', { 420 | id: 'FOO\nBAR' 421 | }); 422 | 423 | // when 424 | var xml = writer.toXML(root); 425 | 426 | // then 427 | expect(xml).to.eql(''); 428 | }); 429 | 430 | 431 | it('inherited', function() { 432 | 433 | // given 434 | var extendedModel = createModel([ 'properties', 'properties-extended' ]); 435 | 436 | var writer = createWriter(extendedModel); 437 | 438 | var root = extendedModel.create('ext:Root', { 439 | id: 'FOO' 440 | }); 441 | 442 | // when 443 | var xml = writer.toXML(root); 444 | 445 | // then 446 | expect(xml).to.eql(''); 447 | }); 448 | 449 | 450 | it('extended', function() { 451 | 452 | // given 453 | var extendedModel = createModel([ 'extension/base', 'extension/custom' ]); 454 | 455 | var writer = createWriter(extendedModel); 456 | 457 | var root = extendedModel.create('b:SubRoot', { 458 | customAttr: 1, 459 | subAttr: 'FOO', 460 | ownAttr: 'OWN' 461 | }); 462 | 463 | // when 464 | var xml = writer.toXML(root); 465 | 466 | // then 467 | expect(xml).to.eql( 468 | '' 473 | ); 474 | }); 475 | 476 | 477 | it('ignore undefined attribute values', function() { 478 | 479 | // given 480 | var model = createModel([ 'properties' ]); 481 | 482 | var writer = createWriter(model); 483 | 484 | var root = model.create('props:BaseWithId', { 485 | id: undefined 486 | }); 487 | 488 | // when 489 | var xml = writer.toXML(root); 490 | 491 | // then 492 | expect(xml).to.eql(''); 493 | }); 494 | 495 | 496 | it('ignore null attribute values', function() { 497 | 498 | // given 499 | var model = createModel([ 'properties' ]); 500 | 501 | var writer = createWriter(model); 502 | 503 | var root = model.create('props:BaseWithId', { 504 | id: null 505 | }); 506 | 507 | // when 508 | var xml = writer.toXML(root); 509 | 510 | // then 511 | expect(xml).to.eql(''); 512 | }); 513 | 514 | }); 515 | 516 | 517 | describe('simple properties', function() { 518 | 519 | var model = createModel([ 'properties' ]); 520 | 521 | it('attribute', function() { 522 | 523 | // given 524 | var writer = createWriter(model); 525 | 526 | var attributes = model.create('props:Attributes', { integerValue: 1000 }); 527 | 528 | // when 529 | var xml = writer.toXML(attributes); 530 | 531 | // then 532 | expect(xml).to.eql(''); 533 | }); 534 | 535 | 536 | it('attribute, escaping special characters', function() { 537 | 538 | // given 539 | var writer = createWriter(model); 540 | 541 | var complex = model.create('props:Complex', { id: '<>\n&' }); 542 | 543 | // when 544 | var xml = writer.toXML(complex); 545 | 546 | // then 547 | expect(xml).to.eql(''); 548 | }); 549 | 550 | 551 | it('write integer property', function() { 552 | 553 | // given 554 | var writer = createWriter(model); 555 | 556 | var root = model.create('props:SimpleBodyProperties', { 557 | intValue: 5 558 | }); 559 | 560 | // when 561 | var xml = writer.toXML(root); 562 | 563 | var expectedXml = 564 | '' + 565 | '5' + 566 | ''; 567 | 568 | // then 569 | expect(xml).to.eql(expectedXml); 570 | }); 571 | 572 | 573 | it('write boolean property', function() { 574 | 575 | // given 576 | var writer = createWriter(model); 577 | 578 | var root = model.create('props:SimpleBodyProperties', { 579 | boolValue: false 580 | }); 581 | 582 | // when 583 | var xml = writer.toXML(root); 584 | 585 | var expectedXml = 586 | '' + 587 | 'false' + 588 | ''; 589 | 590 | // then 591 | expect(xml).to.eql(expectedXml); 592 | }); 593 | 594 | 595 | it('write boolean property, formated', function() { 596 | 597 | // given 598 | var writer = createWriter(model, { format: true }); 599 | 600 | var root = model.create('props:SimpleBodyProperties', { 601 | boolValue: false 602 | }); 603 | 604 | // when 605 | var xml = writer.toXML(root); 606 | 607 | var expectedXml = 608 | '\n' + 609 | ' false\n' + 610 | '\n'; 611 | 612 | // then 613 | expect(xml).to.eql(expectedXml); 614 | }); 615 | 616 | 617 | it('write string isMany property', function() { 618 | 619 | // given 620 | var writer = createWriter(model); 621 | 622 | var root = model.create('props:SimpleBodyProperties', { 623 | str: [ 'A', 'B', 'C' ] 624 | }); 625 | 626 | // when 627 | var xml = writer.toXML(root); 628 | 629 | var expectedXml = 630 | '' + 631 | 'A' + 632 | 'B' + 633 | 'C' + 634 | ''; 635 | 636 | // then 637 | expect(xml).to.eql(expectedXml); 638 | }); 639 | 640 | 641 | it('write string isMany property, formated', function() { 642 | 643 | // given 644 | var writer = createWriter(model, { format: true }); 645 | 646 | var root = model.create('props:SimpleBodyProperties', { 647 | str: [ 'A', 'B', 'C' ] 648 | }); 649 | 650 | // when 651 | var xml = writer.toXML(root); 652 | 653 | var expectedXml = 654 | '\n' + 655 | ' A\n' + 656 | ' B\n' + 657 | ' C\n' + 658 | '\n'; 659 | 660 | // then 661 | expect(xml).to.eql(expectedXml); 662 | }); 663 | 664 | }); 665 | 666 | 667 | describe('embedded properties', function() { 668 | 669 | var model = createModel([ 'properties' ]); 670 | 671 | var extendedModel = createModel([ 'properties', 'properties-extended' ]); 672 | 673 | it('single', function() { 674 | 675 | // given 676 | var writer = createWriter(model); 677 | 678 | var complexCount = model.create('props:ComplexCount', { id: 'ComplexCount_1' }); 679 | var embedding = model.create('props:Embedding', { embeddedComplex: complexCount }); 680 | 681 | // when 682 | var xml = writer.toXML(embedding); 683 | 684 | var expectedXml = 685 | '' + 686 | '' + 687 | ''; 688 | 689 | // then 690 | expect(xml).to.eql(expectedXml); 691 | }); 692 | 693 | 694 | it('property name', function() { 695 | 696 | // given 697 | var writer = createWriter(model); 698 | 699 | var propertyValue = model.create('props:BaseWithId', { id: 'PropertyValue' }); 700 | var container = model.create('props:WithProperty', { propertyName: propertyValue }); 701 | 702 | // when 703 | var xml = writer.toXML(container); 704 | 705 | var expectedXml = 706 | '' + 707 | '' + 708 | ''; 709 | 710 | // then 711 | expect(xml).to.eql(expectedXml); 712 | }); 713 | 714 | 715 | it('collection', function() { 716 | 717 | // given 718 | var writer = createWriter(model); 719 | 720 | var root = model.create('props:Root'); 721 | 722 | var attributes = model.create('props:Attributes', { id: 'Attributes_1' }); 723 | var simpleBody = model.create('props:SimpleBody'); 724 | var containedCollection = model.create('props:ContainedCollection'); 725 | 726 | var any = root.get('any'); 727 | 728 | any.push(attributes); 729 | any.push(simpleBody); 730 | any.push(containedCollection); 731 | 732 | // when 733 | var xml = writer.toXML(root); 734 | 735 | var expectedXml = 736 | '' + 737 | '' + 738 | '' + 739 | '' + 740 | ''; 741 | 742 | // then 743 | expect(xml).to.eql(expectedXml); 744 | }); 745 | 746 | 747 | it('collection / different ns', function() { 748 | 749 | // given 750 | var writer = createWriter(extendedModel); 751 | 752 | var root = extendedModel.create('ext:Root'); 753 | 754 | var attributes1 = extendedModel.create('props:Attributes', { id: 'Attributes_1' }); 755 | var attributes2 = extendedModel.create('props:Attributes', { id: 'Attributes_2' }); 756 | var extendedComplex = extendedModel.create('ext:ExtendedComplex', { numCount: 100 }); 757 | 758 | var any = root.get('any'); 759 | 760 | any.push(attributes1); 761 | any.push(attributes2); 762 | any.push(extendedComplex); 763 | 764 | var elements = root.get('elements'); 765 | elements.push(extendedModel.create('ext:Base')); 766 | 767 | // when 768 | var xml = writer.toXML(root); 769 | 770 | var expectedXml = 771 | '' + 772 | '' + 773 | '' + 774 | '' + 775 | '' + 776 | ''; 777 | 778 | // then 779 | expect(xml).to.eql(expectedXml); 780 | }); 781 | 782 | }); 783 | 784 | 785 | describe('virtual properties', function() { 786 | 787 | var model = createModel([ 'virtual' ]); 788 | 789 | it('should not serialize virtual property', function() { 790 | 791 | // given 792 | var writer = createWriter(model); 793 | 794 | var root = model.create('virt:Root', { 795 | child: model.create('virt:Child') 796 | }); 797 | 798 | // when 799 | var xml = writer.toXML(root); 800 | 801 | // then 802 | expect(xml).to.eql( 803 | ''); 804 | }); 805 | 806 | }); 807 | 808 | 809 | describe('body text', function() { 810 | 811 | var model = createModel([ 'properties' ]); 812 | 813 | it('write body text property', function() { 814 | 815 | // given 816 | var writer = createWriter(model); 817 | 818 | var root = model.create('props:SimpleBody', { 819 | body: 'textContent' 820 | }); 821 | 822 | // when 823 | var xml = writer.toXML(root); 824 | 825 | // then 826 | expect(xml).to.eql('textContent'); 827 | }); 828 | 829 | 830 | it('write encode body property', function() { 831 | 832 | // given 833 | var writer = createWriter(model); 834 | 835 | var root = model.create('props:SimpleBody', { 836 | body: '

HTML "markup"

' 837 | }); 838 | 839 | // when 840 | var xml = writer.toXML(root); 841 | 842 | var expectedXml = 843 | '' + 844 | '<h2>HTML&nbsp;"markup"</h2>' + 845 | ''; 846 | 847 | // then 848 | expect(xml).to.eql(expectedXml); 849 | }); 850 | 851 | 852 | it('write encode body property in subsequent calls', function() { 853 | 854 | // given 855 | var writer = createWriter(model); 856 | 857 | var root1 = model.create('props:SimpleBody', { 858 | body: '<>' 859 | }); 860 | var root2 = model.create('props:SimpleBody', { 861 | body: '<>' 862 | }); 863 | 864 | // when 865 | var xml1 = writer.toXML(root1); 866 | var xml2 = writer.toXML(root2); 867 | 868 | var expectedXml = 869 | '' + 870 | '<>' + 871 | ''; 872 | 873 | // then 874 | expect(xml1).to.eql(expectedXml); 875 | expect(xml2).to.eql(expectedXml); 876 | }); 877 | 878 | 879 | it('write encode body property with special chars', function() { 880 | 881 | // given 882 | var writer = createWriter(model); 883 | 884 | var root = model.create('props:SimpleBody', { 885 | body: '&\n<>"\'' 886 | }); 887 | 888 | // when 889 | var xml = writer.toXML(root); 890 | 891 | var expectedXml = 892 | '' + 893 | '&\n<>"\'' + 894 | ''; 895 | 896 | // then 897 | expect(xml).to.eql(expectedXml); 898 | }); 899 | 900 | }); 901 | 902 | 903 | describe('alias', function() { 904 | 905 | var model = createModel([ 'properties' ]); 906 | 907 | var noAliasModel = createModel([ 'noalias' ]); 908 | 909 | it('lowerCase', function() { 910 | 911 | // given 912 | var writer = createWriter(model); 913 | 914 | var root = model.create('props:Root'); 915 | 916 | // when 917 | var xml = writer.toXML(root); 918 | 919 | // then 920 | expect(xml).to.eql(''); 921 | }); 922 | 923 | 924 | it('none', function() { 925 | 926 | // given 927 | var writer = createWriter(noAliasModel); 928 | 929 | var root = noAliasModel.create('na:Root'); 930 | 931 | // when 932 | var xml = writer.toXML(root); 933 | 934 | // then 935 | expect(xml).to.eql(''); 936 | }); 937 | }); 938 | 939 | 940 | describe('ns', function() { 941 | 942 | var model = createModel([ 'properties' ]); 943 | var extendedModel = createModel([ 'properties', 'properties-extended' ]); 944 | 945 | it('single package', function() { 946 | 947 | // given 948 | var writer = createWriter(model); 949 | 950 | var root = model.create('props:Root'); 951 | 952 | // when 953 | var xml = writer.toXML(root); 954 | 955 | // then 956 | expect(xml).to.eql(''); 957 | }); 958 | 959 | 960 | it('multiple packages', function() { 961 | 962 | // given 963 | var writer = createWriter(extendedModel); 964 | 965 | var root = extendedModel.create('props:Root'); 966 | 967 | root.get('any').push(extendedModel.create('ext:ExtendedComplex')); 968 | 969 | // when 970 | var xml = writer.toXML(root); 971 | 972 | var expectedXml = 973 | '' + 975 | '' + 976 | ''; 977 | 978 | // then 979 | expect(xml).to.eql(expectedXml); 980 | }); 981 | 982 | 983 | it('default ns', function() { 984 | 985 | // given 986 | var writer = createWriter(extendedModel); 987 | 988 | var root = extendedModel.create('props:Root', { ':xmlns': 'http://properties' }); 989 | 990 | // when 991 | var xml = writer.toXML(root); 992 | 993 | // then 994 | expect(xml).to.eql(''); 995 | }); 996 | 997 | 998 | it('default ns / attributes', function() { 999 | 1000 | // given 1001 | var writer = createWriter(extendedModel); 1002 | 1003 | var root = extendedModel.create('props:Root', { ':xmlns': 'http://properties', id: 'Root' }); 1004 | 1005 | var any = root.get('any'); 1006 | any.push(extendedModel.create('ext:ExtendedComplex')); 1007 | any.push(extendedModel.create('props:Attributes', { id: 'Attributes_2' })); 1008 | 1009 | // when 1010 | var xml = writer.toXML(root); 1011 | 1012 | // then 1013 | expect(xml) 1014 | .to.eql('' + 1015 | '' + 1016 | '' + 1017 | ''); 1018 | }); 1019 | 1020 | 1021 | it('default ns / extension attributes', function() { 1022 | 1023 | // given 1024 | var writer = createWriter(extendedModel); 1025 | 1026 | var root = extendedModel.create('props:Root', { 1027 | ':xmlns': 'http://properties', 1028 | 'xmlns:foo': 'http://fooo', 1029 | id: 'Root', 1030 | 'foo:bar': 'BAR' 1031 | }); 1032 | 1033 | // when 1034 | var xml = writer.toXML(root); 1035 | 1036 | // then 1037 | expect(xml).to.eql(''); 1038 | }); 1039 | 1040 | 1041 | it('explicit ns / attributes', function() { 1042 | 1043 | // given 1044 | var writer = createWriter(extendedModel); 1045 | 1046 | var root = extendedModel.create('props:Root', { 'xmlns:foo': 'http://properties', id: 'Root' }); 1047 | 1048 | // when 1049 | var xml = writer.toXML(root); 1050 | 1051 | // then 1052 | expect(xml).to.eql(''); 1053 | }); 1054 | 1055 | }); 1056 | 1057 | 1058 | describe('reference', function() { 1059 | 1060 | var model = createModel([ 'properties' ]); 1061 | 1062 | it('single', function() { 1063 | 1064 | // given 1065 | var writer = createWriter(model); 1066 | 1067 | var complex = model.create('props:Complex', { id: 'Complex_1' }); 1068 | var referencingSingle = model.create('props:ReferencingSingle', { referencedComplex: complex }); 1069 | 1070 | // when 1071 | var xml = writer.toXML(referencingSingle); 1072 | 1073 | // then 1074 | expect(xml).to.eql(''); 1075 | }); 1076 | 1077 | 1078 | it('collection', function() { 1079 | 1080 | // given 1081 | var writer = createWriter(model); 1082 | 1083 | var complexCount = model.create('props:ComplexCount', { id: 'ComplexCount_1' }); 1084 | var complexNesting = model.create('props:ComplexNesting', { id: 'ComplexNesting_1' }); 1085 | 1086 | var referencingCollection = model.create('props:ReferencingCollection', { 1087 | references: [ complexCount, complexNesting ] 1088 | }); 1089 | 1090 | // when 1091 | var xml = writer.toXML(referencingCollection); 1092 | 1093 | // then 1094 | expect(xml).to.eql( 1095 | '' + 1096 | 'ComplexCount_1' + 1097 | 'ComplexNesting_1' + 1098 | ''); 1099 | }); 1100 | 1101 | 1102 | it('attribute collection', function() { 1103 | 1104 | // given 1105 | var writer = createWriter(model); 1106 | 1107 | var complexCount = model.create('props:ComplexCount', { id: 'ComplexCount_1' }); 1108 | var complexNesting = model.create('props:ComplexNesting', { id: 'ComplexNesting_1' }); 1109 | 1110 | var attrReferenceCollection = model.create('props:AttributeReferenceCollection', { 1111 | refs: [ complexCount, complexNesting ] 1112 | }); 1113 | 1114 | // when 1115 | var xml = writer.toXML(attrReferenceCollection); 1116 | 1117 | // then 1118 | expect(xml).to.eql(''); 1119 | }); 1120 | 1121 | }); 1122 | 1123 | 1124 | it('redefined properties', function() { 1125 | 1126 | // given 1127 | var model = createModel([ 'redefine' ]); 1128 | 1129 | var writer = createWriter(model); 1130 | 1131 | var element = model.create('r:Extension', { 1132 | id: 1, 1133 | name: 'FOO', 1134 | value: 'BAR' 1135 | }); 1136 | 1137 | var expectedXml = '' + 1138 | '1' + 1139 | 'FOO' + 1140 | 'BAR' + 1141 | ''; 1142 | 1143 | // when 1144 | var xml = writer.toXML(element); 1145 | 1146 | // then 1147 | expect(xml).to.eql(expectedXml); 1148 | }); 1149 | 1150 | 1151 | it('replaced properties', function() { 1152 | 1153 | // given 1154 | var model = createModel([ 'replace' ]); 1155 | 1156 | var writer = createWriter(model); 1157 | 1158 | var element = model.create('r:Extension', { 1159 | id: 1, 1160 | name: 'FOO', 1161 | value: 'BAR' 1162 | }); 1163 | 1164 | var expectedXml = '' + 1165 | 'FOO' + 1166 | 'BAR' + 1167 | '1' + 1168 | ''; 1169 | 1170 | // when 1171 | var xml = writer.toXML(element); 1172 | 1173 | // then 1174 | expect(xml).to.eql(expectedXml); 1175 | }); 1176 | 1177 | }); 1178 | 1179 | 1180 | describe('extension handling', function() { 1181 | 1182 | var extensionModel = createModel([ 'extensions' ]); 1183 | 1184 | 1185 | describe('attributes', function() { 1186 | 1187 | it('should write xsi:schemaLocation', function() { 1188 | 1189 | // given 1190 | var writer = createWriter(extensionModel); 1191 | 1192 | var root = extensionModel.create('e:Root', { 1193 | 'xsi:schemaLocation': 'http://fooo ./foo.xsd' 1194 | }); 1195 | 1196 | // when 1197 | var xml = writer.toXML(root); 1198 | 1199 | var expectedXml = 1200 | ''; 1203 | 1204 | // then 1205 | expect(xml).to.eql(expectedXml); 1206 | }); 1207 | 1208 | 1209 | it('should write extension attributes', function() { 1210 | 1211 | // given 1212 | var writer = createWriter(extensionModel); 1213 | 1214 | var root = extensionModel.create('e:Root', { 1215 | 'xmlns:foo': 'http://fooo', 1216 | 'foo:bar': 'BAR' 1217 | }); 1218 | 1219 | // when 1220 | var xml = writer.toXML(root); 1221 | 1222 | // then 1223 | expect(xml).to.eql(''); 1224 | }); 1225 | 1226 | }); 1227 | 1228 | 1229 | describe('elements', function() { 1230 | 1231 | it('should write self-closing extension elements', function() { 1232 | 1233 | // given 1234 | var writer = createWriter(extensionModel); 1235 | 1236 | var meta1 = extensionModel.createAny('other:meta', 'http://other', { 1237 | key: 'FOO', 1238 | value: 'BAR' 1239 | }); 1240 | 1241 | var meta2 = extensionModel.createAny('other:meta', 'http://other', { 1242 | key: 'BAZ', 1243 | value: 'FOOBAR' 1244 | }); 1245 | 1246 | var root = extensionModel.create('e:Root', { 1247 | id: 'FOO', 1248 | extensions: [ meta1, meta2 ] 1249 | }); 1250 | 1251 | // when 1252 | var xml = writer.toXML(root); 1253 | 1254 | // then 1255 | expect(xml).to.eql( 1256 | '' + 1257 | 'FOO' + 1258 | '' + 1259 | '' + 1260 | ''); 1261 | }); 1262 | 1263 | 1264 | // #23 1265 | it('should write unqualified element', function() { 1266 | 1267 | // given 1268 | var writer = createWriter(extensionModel); 1269 | 1270 | // explicitly create element with elementForm=unqualified 1271 | var root = extensionModel.createAny('root', undefined, { 1272 | key: 'FOO', 1273 | value: 'BAR' 1274 | }); 1275 | 1276 | // when 1277 | var xml = writer.toXML(root); 1278 | 1279 | // then 1280 | expect(xml).to.eql( 1281 | '' 1282 | ); 1283 | }); 1284 | 1285 | 1286 | it('should write extension element body', function() { 1287 | 1288 | // given 1289 | var writer = createWriter(extensionModel); 1290 | 1291 | var note = extensionModel.createAny('other:note', 'http://other', { 1292 | $body: 'a note' 1293 | }); 1294 | 1295 | var root = extensionModel.create('e:Root', { 1296 | id: 'FOO', 1297 | extensions: [ note ] 1298 | }); 1299 | 1300 | // when 1301 | var xml = writer.toXML(root); 1302 | 1303 | // then 1304 | expect(xml).to.eql( 1305 | '' + 1306 | 'FOO' + 1307 | '' + 1308 | 'a note' + 1309 | '' + 1310 | ''); 1311 | }); 1312 | 1313 | 1314 | it('should write nested extension element', function() { 1315 | 1316 | // given 1317 | var writer = createWriter(extensionModel); 1318 | 1319 | var meta1 = extensionModel.createAny('other:meta', 'http://other', { 1320 | key: 'k1', 1321 | value: 'v1' 1322 | }); 1323 | 1324 | var meta2 = extensionModel.createAny('other:meta', 'http://other', { 1325 | key: 'k2', 1326 | value: 'v2' 1327 | }); 1328 | 1329 | var additionalNote = extensionModel.createAny('other:additionalNote', 'http://other', { 1330 | $body: 'this is some text' 1331 | }); 1332 | 1333 | var nestedMeta = extensionModel.createAny('other:nestedMeta', 'http://other', { 1334 | $children: [ meta1, meta2, additionalNote ] 1335 | }); 1336 | 1337 | var root = extensionModel.create('e:Root', { 1338 | id: 'FOO', 1339 | extensions: [ nestedMeta ] 1340 | }); 1341 | 1342 | // when 1343 | var xml = writer.toXML(root); 1344 | 1345 | var expectedXml = 1346 | '' + 1347 | 'FOO' + 1348 | '' + 1349 | '' + 1350 | '' + 1351 | '' + 1352 | 'this is some text' + 1353 | '' + 1354 | '' + 1355 | ''; 1356 | 1357 | // then 1358 | expect(xml).to.eql(expectedXml); 1359 | }); 1360 | }); 1361 | 1362 | }); 1363 | 1364 | 1365 | describe('qualified extensions', function() { 1366 | 1367 | var extensionModel = createModel([ 'extension/base', 'extension/custom' ]); 1368 | 1369 | 1370 | it('should write typed extension property', function() { 1371 | 1372 | // given 1373 | var writer = createWriter(extensionModel); 1374 | 1375 | var customGeneric = extensionModel.create('c:CustomGeneric', { count: 10 }); 1376 | 1377 | var root = extensionModel.create('b:Root', { 1378 | generic: customGeneric 1379 | }); 1380 | 1381 | // when 1382 | var xml = writer.toXML(root); 1383 | 1384 | var expectedXml = 1385 | '' + 1386 | '' + 1387 | ''; 1388 | 1389 | // then 1390 | expect(xml).to.eql(expectedXml); 1391 | }); 1392 | 1393 | 1394 | it('should write typed extension attribute', function() { 1395 | 1396 | // given 1397 | var writer = createWriter(extensionModel); 1398 | 1399 | var root = extensionModel.create('b:Root', { customAttr: 666 }); 1400 | 1401 | // when 1402 | var xml = writer.toXML(root); 1403 | 1404 | var expectedXml = 1405 | ''; 1406 | 1407 | // then 1408 | expect(xml).to.eql(expectedXml); 1409 | }); 1410 | 1411 | 1412 | it('should write generic collection', function() { 1413 | 1414 | // given 1415 | var writer = createWriter(extensionModel); 1416 | 1417 | var property1 = extensionModel.create('c:Property', { key: 'foo', value: 'FOO' }); 1418 | var property2 = extensionModel.create('c:Property', { key: 'bar', value: 'BAR' }); 1419 | 1420 | var any = extensionModel.createAny('other:Xyz', 'http://other', { 1421 | $body: 'content' 1422 | }); 1423 | 1424 | var root = extensionModel.create('b:Root', { 1425 | genericCollection: [ property1, property2, any ] 1426 | }); 1427 | 1428 | var xml = writer.toXML(root); 1429 | 1430 | var expectedXml = 1431 | '' + 1433 | '' + 1434 | '' + 1435 | 'content' + 1436 | ''; 1437 | 1438 | // then 1439 | expect(xml).to.eql(expectedXml); 1440 | 1441 | }); 1442 | 1443 | }); 1444 | 1445 | 1446 | describe('namespace declarations', function() { 1447 | 1448 | var extensionModel = createModel([ 'extensions' ]); 1449 | 1450 | var extendedModel = createModel([ 1451 | 'properties', 1452 | 'properties-extended' 1453 | ]); 1454 | 1455 | 1456 | describe('should deconflict namespace prefixes', function() { 1457 | 1458 | it('on nested Any', function() { 1459 | 1460 | // given 1461 | var writer = createWriter(extensionModel); 1462 | 1463 | var root = extensionModel.create('e:Root', { 1464 | extensions: [ 1465 | extensionModel.createAny('e:foo', 'http://not-extensions', { 1466 | foo: 'BAR' 1467 | }) 1468 | ] 1469 | }); 1470 | 1471 | // when 1472 | var xml = writer.toXML(root); 1473 | 1474 | var expectedXml = 1475 | '' + 1477 | '' + 1478 | ''; 1479 | 1480 | // then 1481 | expect(xml).to.eql(expectedXml); 1482 | }); 1483 | 1484 | 1485 | it('on explicitly added namespace', function() { 1486 | 1487 | // given 1488 | var writer = createWriter(extensionModel); 1489 | 1490 | var root = extensionModel.create('e:Root', { 1491 | 'xmlns:e': 'http://not-extensions' 1492 | }); 1493 | 1494 | // when 1495 | var xml = writer.toXML(root); 1496 | 1497 | var expectedXml = 1498 | ''; 1500 | 1501 | // then 1502 | expect(xml).to.eql(expectedXml); 1503 | }); 1504 | 1505 | 1506 | it('on explicitly added namespace + Any', function() { 1507 | 1508 | // given 1509 | var writer = createWriter(extensionModel); 1510 | 1511 | var root = extensionModel.create('e:Root', { 1512 | 'xmlns:e': 'http://not-extensions', 1513 | extensions: [ 1514 | extensionModel.createAny('e:foo', 'http://not-extensions', { 1515 | foo: 'BAR' 1516 | }) 1517 | ] 1518 | }); 1519 | 1520 | // when 1521 | var xml = writer.toXML(root); 1522 | 1523 | var expectedXml = 1524 | '' + 1526 | '' + 1527 | ''; 1528 | 1529 | // then 1530 | expect(xml).to.eql(expectedXml); 1531 | }); 1532 | 1533 | }); 1534 | 1535 | 1536 | it('should write manually added custom namespace', function() { 1537 | 1538 | // given 1539 | var writer = createWriter(extensionModel); 1540 | 1541 | var root = extensionModel.create('e:Root', { 1542 | 'xmlns:foo': 'http://fooo' 1543 | }); 1544 | 1545 | // when 1546 | var xml = writer.toXML(root); 1547 | 1548 | var expectedXml = 1549 | ''; 1551 | 1552 | // then 1553 | expect(xml).to.eql(expectedXml); 1554 | }); 1555 | 1556 | 1557 | it('should ignore unknown namespace prefix', function() { 1558 | 1559 | // given 1560 | var writer = createWriter(extensionModel); 1561 | 1562 | var root = extensionModel.create('e:Root', { 1563 | 'foo:bar': 'BAR' 1564 | }); 1565 | 1566 | // when 1567 | var xml = writer.toXML(root); 1568 | 1569 | // then 1570 | expect(xml).to.eql(''); 1571 | }); 1572 | 1573 | 1574 | it('should write custom', function() { 1575 | 1576 | // given 1577 | var writer = createWriter(extensionModel); 1578 | 1579 | var root = extensionModel.create('e:Root', { 1580 | 1581 | // unprefixed root namespace 1582 | ':xmlns': 'http://extensions', 1583 | extensions: [ 1584 | extensionModel.createAny('bar:bar', 'http://bar', { 1585 | 'xmlns:bar': 'http://bar', 1586 | $children: [ 1587 | extensionModel.createAny('other:child', 'http://other', { 1588 | 'xmlns:other': 'http://other', 1589 | b: 'B' 1590 | }) 1591 | ] 1592 | }), 1593 | extensionModel.createAny('ns0:foo', 'http://foo', { 1594 | 1595 | // unprefixed extension namespace 1596 | 'xmlns': 'http://foo', 1597 | $children: [ 1598 | extensionModel.createAny('ns0:child', 'http://foo', { 1599 | a: 'A' 1600 | }) 1601 | ] 1602 | }) 1603 | ] 1604 | }); 1605 | 1606 | // when 1607 | var xml = writer.toXML(root); 1608 | 1609 | var expectedXml = 1610 | '' + 1611 | '' + 1612 | '' + 1613 | '' + 1614 | '' + 1615 | '' + 1616 | '' + 1617 | ''; 1618 | 1619 | // then 1620 | expect(xml).to.eql(expectedXml); 1621 | 1622 | }); 1623 | 1624 | 1625 | it('should write nested custom', function() { 1626 | 1627 | // given 1628 | var writer = createWriter(extensionModel); 1629 | 1630 | var root = extensionModel.create('e:Root', { 1631 | 1632 | // unprefixed root namespace 1633 | ':xmlns': 'http://extensions', 1634 | extensions: [ 1635 | extensionModel.createAny('bar:bar', 'http://bar', { 1636 | 'xmlns:bar': 'http://bar', 1637 | 'bar:attr': 'ATTR' 1638 | }) 1639 | ] 1640 | }); 1641 | 1642 | // when 1643 | var xml = writer.toXML(root); 1644 | 1645 | var expectedXml = 1646 | '' + 1647 | '' + 1648 | ''; 1649 | 1650 | // then 1651 | expect(xml).to.eql(expectedXml); 1652 | }); 1653 | 1654 | 1655 | it('should strip redundant nested custom', function() { 1656 | 1657 | // given 1658 | var writer = createWriter(extensionModel); 1659 | 1660 | var root = extensionModel.create('e:Root', { 1661 | 1662 | // unprefixed root namespace 1663 | ':xmlns': 'http://extensions', 1664 | 'xmlns:bar': 'http://bar', 1665 | extensions: [ 1666 | extensionModel.createAny('bar:bar', 'http://bar', { 1667 | 'xmlns:bar': 'http://bar', 1668 | 'bar:attr': 'ATTR' 1669 | }) 1670 | ] 1671 | }); 1672 | 1673 | // when 1674 | var xml = writer.toXML(root); 1675 | 1676 | var expectedXml = 1677 | '' + 1678 | '' + 1679 | ''; 1680 | 1681 | // then 1682 | expect(xml).to.eql(expectedXml); 1683 | }); 1684 | 1685 | 1686 | it('should strip different prefix nested custom', function() { 1687 | 1688 | // given 1689 | var writer = createWriter(extensionModel); 1690 | 1691 | var root = extensionModel.create('e:Root', { 1692 | 1693 | // unprefixed root namespace 1694 | ':xmlns': 'http://extensions', 1695 | 'xmlns:otherBar': 'http://bar', 1696 | 'xmlns:otherFoo': 'http://foo', 1697 | extensions: [ 1698 | extensionModel.createAny('bar:bar', 'http://bar', { 1699 | 'xmlns:bar': 'http://bar', 1700 | 'xmlns:foo': 'http://foo', 1701 | 'bar:attr': 'ATTR', 1702 | 'foo:attr': 'FOO_ATTR' 1703 | }) 1704 | ] 1705 | }); 1706 | 1707 | // when 1708 | var xml = writer.toXML(root); 1709 | 1710 | var expectedXml = 1711 | '' + 1713 | '' + 1714 | ''; 1715 | 1716 | // then 1717 | expect(xml).to.eql(expectedXml); 1718 | }); 1719 | 1720 | 1721 | it('should write normalized custom', function() { 1722 | 1723 | // given 1724 | var writer = createWriter(extensionModel); 1725 | 1726 | var root = extensionModel.create('e:Root', { 1727 | 1728 | // unprefixed root namespace 1729 | ':xmlns': 'http://extensions', 1730 | 'xmlns:otherBar': 'http://bar', 1731 | extensions: [ 1732 | extensionModel.createAny('bar:bar', 'http://bar', { 1733 | 'bar:attr': 'ATTR' 1734 | }) 1735 | ] 1736 | }); 1737 | 1738 | // when 1739 | var xml = writer.toXML(root); 1740 | 1741 | var expectedXml = 1742 | '' + 1743 | '' + 1744 | ''; 1745 | 1746 | // then 1747 | expect(xml).to.eql(expectedXml); 1748 | }); 1749 | 1750 | 1751 | it('should write wellknown', function() { 1752 | 1753 | // given 1754 | var writer = createWriter(extendedModel); 1755 | 1756 | var root = extendedModel.create('props:Root', { 1757 | 1758 | // unprefixed top-level namespace 1759 | ':xmlns': 'http://properties', 1760 | any: [ 1761 | extendedModel.create('ext:ExtendedComplex', { 1762 | 1763 | // unprefixed nested namespace 1764 | ':xmlns': 'http://extended' 1765 | }) 1766 | ] 1767 | }); 1768 | 1769 | // when 1770 | var xml = writer.toXML(root); 1771 | 1772 | var expectedXml = 1773 | '' + 1774 | '' + 1775 | ''; 1776 | 1777 | // then 1778 | expect(xml).to.eql(expectedXml); 1779 | }); 1780 | 1781 | 1782 | it('should write only actually exposed', function() { 1783 | 1784 | // given 1785 | var writer = createWriter(extendedModel); 1786 | 1787 | var root = extendedModel.create('ext:Root', { 1788 | 1789 | // unprefixed top-level namespace 1790 | ':xmlns': 'http://extended', 1791 | id: 'ROOT', 1792 | any: [ 1793 | extendedModel.create('props:Complex', { 1794 | 1795 | // unprefixed nested namespace 1796 | ':xmlns': 'http://properties' 1797 | }) 1798 | ] 1799 | }); 1800 | 1801 | // when 1802 | var xml = writer.toXML(root); 1803 | 1804 | var expectedXml = 1805 | '' + 1806 | '' + 1807 | ''; 1808 | 1809 | // then 1810 | expect(xml).to.eql(expectedXml); 1811 | 1812 | }); 1813 | 1814 | 1815 | it('should write xsi:type namespaces', function() { 1816 | 1817 | var model = createModel([ 1818 | 'datatype', 1819 | 'datatype-external', 1820 | 'datatype-aliased' 1821 | ]); 1822 | 1823 | // given 1824 | var writer = createWriter(model); 1825 | 1826 | var root = model.create('da:Root', { 1827 | 'xmlns:a' : 'http://datatypes-aliased', 1828 | otherBounds: [ 1829 | model.create('dt:Rect', { 1830 | ':xmlns': 'http://datatypes', 1831 | y: 100 1832 | }) 1833 | ] 1834 | }); 1835 | 1836 | // when 1837 | var xml = writer.toXML(root); 1838 | 1839 | // then 1840 | expect(xml).to.eql( 1841 | '' + 1842 | '' + 1843 | ''); 1844 | 1845 | }); 1846 | 1847 | 1848 | it('should strip unused global', function() { 1849 | 1850 | // given 1851 | var writer = createWriter(extendedModel); 1852 | 1853 | var root = extendedModel.create('ext:Root', { 1854 | ':xmlns': 'http://extended', 1855 | id: 'Root', 1856 | 'xmlns:props': 'http://properties', 1857 | any: [ 1858 | extendedModel.create('props:Base', { ':xmlns': 'http://properties' }) 1859 | ] 1860 | }); 1861 | 1862 | // when 1863 | var xml = writer.toXML(root); 1864 | 1865 | // then 1866 | expect(xml).to.eql( 1867 | '' + 1868 | '' + 1869 | '' 1870 | ); 1871 | }); 1872 | 1873 | 1874 | it('should strip xml namespace', function() { 1875 | 1876 | // given 1877 | var writer = createWriter(extensionModel); 1878 | 1879 | var root = extensionModel.create('e:Root', { 1880 | 'xml:lang': 'de', 1881 | extensions: [ 1882 | extensionModel.createAny('bar:bar', 'http://bar', { 1883 | 'xml:lang': 'en' 1884 | }) 1885 | ] 1886 | }); 1887 | 1888 | // when 1889 | var xml = writer.toXML(root); 1890 | 1891 | // then 1892 | expect(xml).to.eql( 1893 | '' + 1894 | '' + 1895 | '' 1896 | ); 1897 | }); 1898 | 1899 | 1900 | it('should keep local override', function() { 1901 | 1902 | // given 1903 | var writer = createWriter(extendedModel); 1904 | 1905 | var root = extendedModel.create('props:ComplexNesting', { 1906 | 'xmlns:root': 'http://properties', 1907 | id: 'ComplexNesting', 1908 | nested: [ 1909 | extendedModel.create('props:ComplexNesting', { 1910 | ':xmlns': 'http://properties', 1911 | nested: [ 1912 | extendedModel.create('props:ComplexNesting', { 1913 | nested: [ 1914 | extendedModel.create('props:ComplexNesting', { 1915 | 'xmlns:foo': 'http://properties' 1916 | }) 1917 | ] 1918 | }) 1919 | ] 1920 | }) 1921 | ] 1922 | }); 1923 | 1924 | // when 1925 | var xml = writer.toXML(root); 1926 | 1927 | // then 1928 | expect(xml).to.eql( 1929 | '' + 1930 | '' + 1931 | '' + 1932 | '' + 1933 | '' + 1934 | '' + 1935 | '' 1936 | ); 1937 | }); 1938 | 1939 | }); 1940 | 1941 | 1942 | it('should reuse global namespace', function() { 1943 | 1944 | var model = createModel([ 1945 | 'properties', 1946 | 'properties-extended' 1947 | ]); 1948 | 1949 | // given 1950 | var writer = createWriter(model); 1951 | 1952 | // 1953 | // 1954 | // 1955 | // 1956 | // 1957 | var root = model.create('props:Root', { 1958 | 'xmlns:props': 'http://properties', 1959 | 'xmlns:ext': 'http://extended', 1960 | any: [ 1961 | model.create('props:ComplexNesting', { 1962 | ':xmlns': 'http://properties', 1963 | nested: [ 1964 | model.create('ext:ExtendedComplex', { numCount: 1 }) 1965 | ] 1966 | }) 1967 | ] 1968 | }); 1969 | 1970 | // when 1971 | var xml = writer.toXML(root); 1972 | 1973 | var expectedXML = 1974 | '' + 1975 | '' + 1976 | '' + 1977 | '' + 1978 | ''; 1979 | 1980 | // then 1981 | expect(xml).to.eql(expectedXML); 1982 | 1983 | }); 1984 | 1985 | 1986 | describe('custom namespace mapping', function() { 1987 | 1988 | var datatypesModel = createModel([ 1989 | 'datatype', 1990 | 'datatype-external' 1991 | ], { 1992 | nsMap: { 1993 | 'http://www.omg.org/spec/XMI/20131001': 'xmi' 1994 | } 1995 | }); 1996 | 1997 | 1998 | it('should write explicitly remapped xsi:type', function() { 1999 | 2000 | // given 2001 | var writer = createWriter(datatypesModel); 2002 | 2003 | var root = datatypesModel.create('dt:Root'); 2004 | 2005 | root.set('bounds', datatypesModel.create('do:Rect', { 2006 | x: 100, 2007 | 'xmlns:foo': 'http://www.w3.org/2001/XMLSchema-instance' 2008 | })); 2009 | 2010 | // when 2011 | var xml = writer.toXML(root); 2012 | 2013 | // then 2014 | expect(xml).to.eql( 2015 | '' + 2016 | '' + 2020 | '' 2021 | ); 2022 | }); 2023 | 2024 | 2025 | it('should write explicitly remapped xmi:type', function() { 2026 | 2027 | // given 2028 | var writer = createWriter(datatypesModel); 2029 | 2030 | var root = datatypesModel.create('dt:Root'); 2031 | 2032 | root.set('xmiBounds', datatypesModel.create('do:Rect', { 2033 | x: 100, 2034 | 'xmlns:foo': 'http://www.omg.org/spec/XMI/20131001' 2035 | })); 2036 | 2037 | // when 2038 | var xml = writer.toXML(root); 2039 | 2040 | // then 2041 | expect(xml).to.eql( 2042 | '' + 2043 | '' + 2047 | '' 2048 | ); 2049 | }); 2050 | 2051 | }); 2052 | 2053 | }); 2054 | --------------------------------------------------------------------------------