xml-toolkit 0.0.7 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,77 @@
1
1
  # node-xml-toolkit
2
2
  Collection of classes for dealing with XML
3
3
 
4
- This is a project template. The actual code is yet to be written.
4
+ # Installation
5
+
6
+ ```
7
+ npm i xml-toolkit
8
+ ```
9
+
10
+ # Using
11
+ * [Reading a Record List](https://github.com/do-/node-xml-toolkit/wiki/Use-Case:-Reading-a-Record-List)
12
+
13
+ ```js
14
+ const {XMLReader, XMLNode} = require ('xml-toolkit')
15
+
16
+ const records = new XMLReader ({
17
+ filterElements : 'Record',
18
+ map : XMLNode.toObject ({})
19
+ }).process (xmlSource)
20
+
21
+ // ...then:
22
+ // await someLoader.load (records)
23
+
24
+ // ...or
25
+ // for await (const record of records) { // pull parser mode
26
+
27
+ // ...or
28
+ // records.on ('error', e => console.log (e))
29
+ // records.pipe (nextStream)
30
+
31
+ // ...or
32
+ // records.on ('error', e => console.log (e))
33
+ // records.on ('data', record => doSomethingWith (record))
34
+ ```
35
+
36
+ * [Getting a Single Element](https://github.com/do-/node-xml-toolkit/wiki/Use-Case:-Getting-a-Single-Element)
37
+
38
+ ```js
39
+ const {XMLReader, XMLNode} = require ('xml-toolkit')
40
+
41
+ const data = await new XMLReader ({
42
+ filterElements : 'MyElementName',
43
+ map : XMLNode.toObject ({})
44
+ }).process (xmlSource).findFirst ()
45
+ ```
46
+
47
+ * [Patching XML](https://github.com/do-/node-xml-toolkit/wiki/Use-Case:-Patching-XML)
48
+
49
+ ```js
50
+ const {XMLReader} = require ('xml-toolkit')
51
+
52
+ let xmlResult = ''; for await (const node of new XMLReader ().process (xmlSource)) xmlResult +=
53
+ node.isCharacters && node.parent.localName === 'ThePlaceHolder' ? id :
54
+ node.xml
55
+ ```
56
+
57
+ * [Serializing an Object According to an XML Schema](https://github.com/do-/node-xml-toolkit/wiki/Use-Case:-Serializing-an-Object-According-to-an-XML-Schema)
58
+
59
+ ```js
60
+ const data = {ExportDebtRequestsResponse: {
61
+ "request-data": {
62
+ // ...
63
+ }
64
+ }
65
+
66
+ const xs = await XMLSchemata.fromFile ('xs.xsd')
67
+
68
+ const xml = xs.stringify (data)
69
+
70
+ /* result:
71
+ <ns0:ExportDebtRequestsResponse xmlns:ns0="urn:...">
72
+ <ns0:request-data>
73
+ <!-- ... and so on ... -->
74
+ */
75
+ ```
76
+
77
+ More information available in [wiki docs](https://github.com/do-/node-xml-toolkit/wiki).
package/index.js CHANGED
@@ -1,7 +1,11 @@
1
- const XMLLexer = require ('./lib/XMLLexer')
2
- const XMLReader = require ('./lib/XMLReader')
3
- const SAXEvent = require ('./lib/SAXEvent')
4
- const AttributesMap = require ('./lib/AttributesMap')
5
- const MoxyLikeJsonEncoder = require ('./lib/MoxyLikeJsonEncoder')
1
+ for (const name of [
6
2
 
7
- module.exports = {XMLLexer, XMLReader, SAXEvent, AttributesMap, MoxyLikeJsonEncoder}
3
+ 'XMLLexer',
4
+ 'XMLReader',
5
+ 'SAXEvent',
6
+ 'AttributesMap',
7
+ 'MoxyLikeJsonEncoder',
8
+ 'XMLNode',
9
+ 'XMLSchemata',
10
+
11
+ ]) module.exports [name] = require ('./lib/' + name)
@@ -65,7 +65,7 @@ const AttributesMap = class extends Map {
65
65
 
66
66
  }
67
67
 
68
- return super.set (k, this.fixText (v))
68
+ return super.set (k, v === '' ? null : this.fixText (v))
69
69
 
70
70
  }
71
71
 
@@ -1,39 +1,63 @@
1
- const SAXEvent = require ('./SAXEvent.js')
1
+ const SAXEvent = require ('./SAXEvent.js'), {CHARACTERS, END_ELEMENT} = SAXEvent.TYPES
2
2
 
3
- const set = (o, k, nv) => {
3
+ const GET_LOCAL_NAME = (localName, namespaceURI) => localName
4
4
 
5
- if (!(k in o)) return o [k] = nv
5
+ const MoxyLikeJsonEncoder = function (options = {}) {
6
6
 
7
- const ov = o [k]; if (!Array.isArray (ov)) o [k] = [ov]
8
-
9
- o [k].push (nv)
7
+ let {getName} = options; if (!getName) getName = GET_LOCAL_NAME
10
8
 
11
- }
9
+ const xform = ({children, attributes}) => {
12
10
 
13
- const xform = ({children, attributes}) => {
11
+ let o = null
14
12
 
15
- let o = attributes == null ? {} : Object.fromEntries (attributes.entries ())
13
+ const set = (nv, localName, namespaceURI) => {
14
+
15
+ const k = getName (localName, namespaceURI)
16
16
 
17
- if (children != null) for (const child of children) switch (child.type) {
17
+ if (o === null) return o = {[k]: nv}
18
18
 
19
- case SAXEvent.TYPES.CHARACTERS: return child.text
19
+ if (!(k in o)) return o [k] = nv
20
20
 
21
- case SAXEvent.TYPES.END_ELEMENT: set (o, child.localName, xform (child))
21
+ const ov = o [k]; if (!Array.isArray (ov)) return o [k] = [ov, nv]
22
22
 
23
- }
24
-
25
- return o
23
+ ov.push (nv)
26
24
 
27
- }
25
+ }
28
26
 
29
- const MoxyLikeJsonEncoder = function (options = {}) {
27
+ if (attributes != null)
28
+
29
+ for (const [name, value] of attributes.entries ())
30
+
31
+ set (value, attributes.getLocalName (name), attributes.getNamespaceURI (name))
32
+
33
+ if (children != null) for (const child of children) switch (child.type) {
34
+
35
+ case CHARACTERS:
36
+
37
+ return child.text
38
+
39
+ case END_ELEMENT:
40
+
41
+ set (xform (child), child.localName, child.namespaceURI)
42
+
43
+ }
44
+
45
+ return o
46
+
47
+ }
30
48
 
31
49
  return function (node) {
32
50
 
33
51
  let result = xform (node)
34
-
35
- return options.wrap ? {[node.localName]: result} : result
36
-
52
+
53
+ const {wrap, map} = options
54
+
55
+ if (wrap) result = {[getName (node.localName, node.namespaceURI)]: result}
56
+
57
+ if (map) result = map (result)
58
+
59
+ return result
60
+
37
61
  }
38
62
 
39
63
  }
@@ -0,0 +1,57 @@
1
+ const assert = require ('assert')
2
+
3
+ const NamespacePrefixesMap = class extends Map {
4
+
5
+ constructor (schemata, o) {
6
+
7
+ assert.strictEqual (typeof o, 'object')
8
+
9
+ super ()
10
+
11
+ this.schemata = schemata
12
+
13
+ this.add (o)
14
+
15
+ }
16
+
17
+ add (o) {
18
+
19
+ if (this.isAddedAsNamespaceReference (o)) return
20
+
21
+ for (let v of Object.values (o)) if (typeof v === 'object') this.add (v)
22
+
23
+ }
24
+
25
+ isAddedAsNamespaceReference (a) {
26
+
27
+ if (!Array.isArray (a)) return false
28
+
29
+ if (a.length !== 2) return false
30
+
31
+ const uri = a [1]
32
+
33
+ if (!this.schemata.has (uri)) return false
34
+
35
+ if (this.has (uri)) return true
36
+
37
+ this.set (uri, 'ns' + this.size)
38
+
39
+ }
40
+
41
+ QName (localName, namespaceURI) {
42
+
43
+ if (namespaceURI == null || !this.has (namespaceURI)) return localName
44
+
45
+ return this.get (namespaceURI) + ':' + localName
46
+
47
+ }
48
+
49
+ appendTo (buf) {
50
+
51
+ for (let [k, v] of this.entries ()) buf [0] += ' xmlns:' + v + '="' + k + '"'
52
+
53
+ }
54
+
55
+ }
56
+
57
+ module.exports = NamespacePrefixesMap
@@ -0,0 +1,361 @@
1
+ const assert = require ('assert')
2
+ const stringEscape = require ('string-escape-map')
3
+
4
+ let esc = [
5
+ ['<', '&lt;'],
6
+ ['>', '&gt;'],
7
+ ['&', '&amp;'],
8
+ ['"', '&quot;']
9
+ ]
10
+
11
+ //const XML_BODY = new stringEscape (esc)
12
+
13
+ //esc.push (['"', '&quot;'])
14
+
15
+ const XML_ATTR = new stringEscape (esc)
16
+
17
+ const BOOL = new Map ([
18
+ ['0', 'false'],
19
+ ['false', 'false'],
20
+ ['N', 'false'],
21
+ ['1', 'true'],
22
+ ['true', 'true'],
23
+ ['Y', 'true'],
24
+ ])
25
+
26
+ const XMLMarshaller = class {
27
+
28
+ constructor (xs, localName, namespaceURI) {
29
+
30
+ this.xs = xs
31
+
32
+ this.schema = xs.get (namespaceURI); assert (this.schema, 'No schema found for namespaceURI = ' + namespaceURI)
33
+
34
+ this.schemaElement = this.schema.get (localName); assert (this.schemaElement, 'No schema element found for namespaceURI = ' + namespaceURI + ', localName = ' + localName)
35
+
36
+ this.ns = this.xs.getNamespacePrefixesMap (this.schemaElement)
37
+
38
+ }
39
+
40
+ stringify (content) {
41
+
42
+ const qName = this.ns.QName (this.schemaElement.name, this.schema.targetNamespace)
43
+
44
+ const {complexType} = this.schemaElement
45
+
46
+ let buf = ['<' + qName]
47
+
48
+ this.ns.appendTo (buf)
49
+
50
+ if (complexType) this._aComplexType (buf, complexType, content)
51
+
52
+ buf [0] += '>'
53
+
54
+ if (complexType) this._bComplexType (buf, complexType, content)
55
+
56
+ return buf [0] + '</' + qName + '>'
57
+
58
+ }
59
+
60
+ /// _a: attributes
61
+
62
+ _aComplexType (buf, complexType, content) {
63
+
64
+ const {complexContent, attribute} = complexType
65
+
66
+ if (attribute)
67
+ for (const e of Array.isArray (attribute) ? attribute : [attribute])
68
+ this._aAttribute (buf, e, content [e.name])
69
+
70
+ if (complexContent) this._aComplexContent (buf, complexContent, content)
71
+
72
+ }
73
+
74
+ _aComplexContent (buf, complexContent, content) {
75
+
76
+ const {extension} = complexContent
77
+
78
+ if (extension) this._aExtension (buf, extension, content)
79
+
80
+ }
81
+
82
+ _aExtension (buf, extension, content) {
83
+
84
+ const {base, attribute} = extension
85
+
86
+ if (base) {
87
+
88
+ const [localName, namespaceURI] = base
89
+
90
+ const schema = this.xs.get (namespaceURI)
91
+
92
+ const type = schema.get (localName)
93
+
94
+ if (type._type === 'complexType') this._aComplexType (buf, type, content)
95
+
96
+ }
97
+
98
+ if (attribute)
99
+ for (const a of Array.isArray (attribute) ? attribute : [attribute])
100
+ this._aAttribute (buf, a, content [a.name])
101
+
102
+ }
103
+
104
+ _aAttribute (buf, attribute, content) {
105
+
106
+ if (content == null) return
107
+
108
+ let {name, targetNamespace, simpleType, type} = attribute
109
+
110
+ if (type) {
111
+
112
+ type = this.xs.getByReference (type)
113
+
114
+ switch (type._type) {
115
+
116
+ case 'simpleType':
117
+ simpleType = type
118
+ break
119
+
120
+ }
121
+
122
+ }
123
+
124
+ const qName = this.ns.QName (name, targetNamespace)
125
+
126
+ buf [0] += ' ' + qName + '="'
127
+
128
+ if (simpleType) this._bSimpleType (buf, simpleType, content)
129
+
130
+ buf [0] += '"'
131
+
132
+ }
133
+
134
+ /// _b: body
135
+
136
+ _bComplexType (buf, complexType, content) {
137
+
138
+ const {complexContent, sequence, choice, all} = complexType, group = sequence || choice || all
139
+
140
+ if (group) this._bSequence (buf, group, content)
141
+ if (complexContent) this._bComplexContent (buf, complexContent, content)
142
+
143
+ }
144
+
145
+ _bSimpleType (buf, simpleType, content, _restriction = {}) {
146
+
147
+ let {restriction} = simpleType; if (restriction) {
148
+
149
+ for (const [k, v] of Object.entries (_restriction)) switch (k) {
150
+ case 'base': case 'targetNamespace': break
151
+ default: restriction [k] = v
152
+ }
153
+
154
+ this._bSimpleType (buf, this.xs.getByReference (restriction.base), content, restriction)
155
+
156
+ }
157
+ else {
158
+
159
+ buf [0] += this.to_string (content, simpleType.name, _restriction)
160
+
161
+ }
162
+
163
+ }
164
+
165
+ to_string (v, type, restriction = {}) {
166
+
167
+ const carp = () => {throw new Error (`Invalid value for ${type} type: ${v} (${typeof v})`)}
168
+
169
+ switch (type) {
170
+
171
+ case 'boolean':
172
+ if (BOOL.has (v)) return BOOL.get (v)
173
+ return !v ? 'false' : 'true'
174
+
175
+ case 'date':
176
+ switch (typeof v) {
177
+ case 'string':
178
+ if (v.length === 10) return v
179
+ case 'number':
180
+ case 'bigint':
181
+ v = new Date (v)
182
+ }
183
+ if (v instanceof Date) {
184
+ return v.toJSON ().slice (0, 10)
185
+ }
186
+ else {
187
+ carp ()
188
+ }
189
+
190
+ case 'dateTime':
191
+ switch (typeof v) {
192
+ case 'string':
193
+ return v
194
+ case 'number':
195
+ case 'bigint':
196
+ v = new Date (v)
197
+ }
198
+ if (v instanceof Date) {
199
+ return v.toJSON ()
200
+ }
201
+ else {
202
+ carp ()
203
+ }
204
+
205
+ case 'integer':
206
+ case 'nonNegativeInteger':
207
+ case 'positiveInteger':
208
+ case 'nonPositiveInteger':
209
+ case 'negativeInteger':
210
+ case 'long':
211
+ if (typeof v === 'string') v = BigInt (v)
212
+ switch (typeof v) {
213
+ case 'number':
214
+ case 'bigint':
215
+ return '' + v
216
+ }
217
+ carp ()
218
+
219
+ case 'int':
220
+ case 'short':
221
+ case 'byte':
222
+ case 'unsignedLong':
223
+ case 'unsignedInt':
224
+ case 'unsignedShort':
225
+ case 'unsignedByte':
226
+ if (typeof v === 'string') v = parseInt (v)
227
+ switch (typeof v) {
228
+ case 'number':
229
+ if (!Number.isInteger (v)) carp ()
230
+ case 'bigint':
231
+ return '' + v
232
+ }
233
+ carp ()
234
+
235
+ case 'float':
236
+ case 'double':
237
+ if (typeof v === 'string') v = parseFloat (v)
238
+ if (typeof v === 'number') switch (v) {
239
+ case Number.POSITIVE_INFINITY: return 'INF'
240
+ case Number.NEGATIVE_INFINITY: return '-INF'
241
+ default: return '' + v
242
+ }
243
+ carp ()
244
+
245
+ case 'decimal':
246
+ const {fractionDigits} = restriction
247
+ if (typeof v === 'string') v = parseFloat (v)
248
+ if (typeof v === 'number') return fractionDigits ? v.toFixed (fractionDigits.value) : '' + v
249
+ carp ()
250
+
251
+ default:
252
+ return XML_ATTR.escape ('' + v)
253
+
254
+ }
255
+
256
+ }
257
+
258
+ _bComplexContent (buf, complexContent, content) {
259
+
260
+ const {extension} = complexContent
261
+
262
+ if (extension) this._bExtension (buf, extension, content)
263
+
264
+ }
265
+
266
+ _bExtension (buf, _extension, content) {
267
+
268
+ const {base, sequence, choice, all} = _extension, group = sequence || choice || all
269
+
270
+ if (base) {
271
+
272
+ const [localName, namespaceURI] = base
273
+
274
+ const schema = this.xs.get (namespaceURI)
275
+
276
+ const type = schema.get (localName)
277
+
278
+ if (type._type === 'complexType') this._bComplexType (buf, type, content)
279
+
280
+ }
281
+
282
+ if (group) this._bSequence (buf, group, content)
283
+
284
+ }
285
+
286
+ _bSequence (buf, _sequence, content) {
287
+
288
+ const {element, sequence, choice, all} = _sequence, group = sequence || choice || all
289
+
290
+ if (group) this._bSequence (buf, group, content)
291
+
292
+ if (element) {
293
+
294
+ for (const e of Array.isArray (element) ? element : [element])
295
+
296
+ this._bElement (buf, e, content [e.name])
297
+
298
+ }
299
+
300
+ }
301
+
302
+ _bElement_null (buf, element) {
303
+
304
+ if (BOOL.get (element.nillable) !== 'true') return
305
+
306
+ const {name, targetNamespace} = element, qName = this.ns.QName (name, targetNamespace)
307
+
308
+ buf [0] += `<${qName} xsi:nil="true" />`
309
+
310
+ }
311
+
312
+ _bElement (buf, element, content) {
313
+
314
+ if (content == null) return this._bElement_null (buf, element)
315
+
316
+ if (!Array.isArray (content)) content = [content]
317
+
318
+ if (content.length === 0) return
319
+
320
+ let {name, targetNamespace, complexType, simpleType, type} = element
321
+
322
+ if (type) {
323
+
324
+ type = this.xs.getByReference (type)
325
+
326
+ switch (type._type) {
327
+
328
+ case 'complexType':
329
+ complexType = type
330
+ break
331
+
332
+ case 'simpleType':
333
+ simpleType = type
334
+ break
335
+
336
+ }
337
+
338
+ }
339
+
340
+ const qName = this.ns.QName (name, targetNamespace)
341
+
342
+ for (const i of content) {
343
+
344
+ buf [0] += '<' + qName
345
+
346
+ if (complexType) this._aComplexType (buf, complexType, content)
347
+
348
+ buf [0] += '>'
349
+
350
+ if (complexType) this._bComplexType (buf, complexType, i)
351
+ if (simpleType) this._bSimpleType (buf, simpleType, i)
352
+
353
+ buf [0] += '</' + qName + '>'
354
+
355
+ }
356
+
357
+ }
358
+
359
+ }
360
+
361
+ module.exports = XMLMarshaller
package/lib/XMLNode.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const SAXEvent = require ('./SAXEvent.js')
2
2
  const AttributesMap = require ('./AttributesMap')
3
3
  const NamespacesMap = require ('./NamespacesMap')
4
+ const MoxyLikeJsonEncoder = require ('./MoxyLikeJsonEncoder')
4
5
 
5
6
  const XML_READER = Symbol ('_xmlReader')
6
7
  const ATTRIBUTES = Symbol ('_attributes')
@@ -31,8 +32,9 @@ const XMLNode = class extends SAXEvent {
31
32
 
32
33
  e [PARENT] = this [PARENT]
33
34
  e [LEVEL] = this [LEVEL]
34
- e [ATTRIBUTES] = this [ATTRIBUTES]
35
- e [NS_MAP] = this [NS_MAP]
35
+
36
+ if (ATTRIBUTES in this) e [ATTRIBUTES] = this [ATTRIBUTES]
37
+ if (NS_MAP in this) e [NS_MAP] = this [NS_MAP]
36
38
 
37
39
  return e
38
40
 
@@ -96,7 +98,9 @@ const XMLNode = class extends SAXEvent {
96
98
 
97
99
  get namespaceURI () {
98
100
 
99
- return this.namespacesMap.getNamespaceURI (this.name, true)
101
+ const {namespacesMap} = this; if (namespacesMap == null) return null
102
+
103
+ return namespacesMap.getNamespaceURI (this.name, true)
100
104
 
101
105
  }
102
106
 
@@ -138,4 +142,6 @@ XMLNode.getLocalName = name => {
138
142
 
139
143
  }
140
144
 
145
+ XMLNode.toObject = MoxyLikeJsonEncoder
146
+
141
147
  module.exports = XMLNode
package/lib/XMLReader.js CHANGED
@@ -14,7 +14,7 @@ const XMLReader = class extends Transform {
14
14
  options.decodeStrings = false
15
15
  options.objectMode = true
16
16
 
17
- if (!('stripSpace' in options)) options.stripSpace = false
17
+ if (!('stripSpace' in options)) options.stripSpace = ('filterElements' in options)
18
18
  assert (options.stripSpace === true || options.stripSpace === false, 'options.stripSpace must be boolean, not ' + typeof options.stripSpace)
19
19
 
20
20
  if (!('useEntities' in options)) options.useEntities = true