xml-twig 1.0.0

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/twig.js ADDED
@@ -0,0 +1,1139 @@
1
+ const SAX = 'sax';
2
+ const EXPAT = 'expat';
3
+
4
+ let tree;
5
+ let current;
6
+
7
+ /**
8
+ * Optional settings for the Twig parser
9
+ * @typedef ParserOptions
10
+ * @param {string} method - The underlaying parser. Either `'sax'` or `'expat'`.
11
+ * @param {string} encoding - Encoding of the XML File. Applies only to `expat` parser.
12
+ * @param {boolean} xmlns - If true, then namespaces are accessible by `namespace` property.
13
+ * @param {boolean} trim - If true, then turn any whitespace into a single space. Text and comments are trimmed.
14
+ * @param {boolean} resumeAfterError - If true then parser continues reading after an error. Otherwiese it throws exception.
15
+ * @param {boolean} partial - It true then unhandled elements are purged.
16
+ * @example { encoding: 'UTF-8', xmlns: true }
17
+ * @default { method: 'sax', encoding: 'UTF-8', xmlns: false, trim: true, resumeAfterError: false, partial: false }
18
+ */
19
+
20
+
21
+ /**
22
+ * Reference to handler functions for Twig objects.<br>
23
+ * If `name` is not specified, then handler is called on every element.<br>
24
+ * Otherwise the element name must be equal to the string or Regular Expression. You can specify custom function
25
+ * @typedef TwigHandler
26
+ * @param {?string|RegExp|ElementCondition} name - Name of handled element or any element if not specified
27
+ * @param {function} HandlerFunction - Definition of handler function, either anonymous or explict function
28
+ */
29
+
30
+ /**
31
+ * Handler function for Twig objects, i.e. the way you like to use the XML element.
32
+ * @typedef HandlerFunction
33
+ * @param {Twig} elt - The current Twig element on which the function was called.
34
+ */
35
+
36
+
37
+
38
+ /**
39
+ * Optional condition to get elements<br>
40
+ * - If `undefined`, then all elements are returned.<br>
41
+ * - If `string` then the element name must be equal to the string
42
+ * - If `RegExp` then the element name must match the Regular Expression
43
+ * - If [ElementConditionFilter](#ElementConditionFilter) then the element must filter function
44
+ * - Use [Twig](#Twig) object to find a specific element (rarely used)
45
+ * @typedef {string|RegExp|ElementConditionFilter|Twig} ElementCondition
46
+ */
47
+
48
+ /**
49
+ *
50
+ * Custom filter function to get desired elements
51
+ * @typedef {function} ElementConditionFilter
52
+ * @param {string} name - Name of the element
53
+ * @param {Twig} elt - The full Twig object
54
+ */
55
+
56
+
57
+ /**
58
+ * Create a new Twig parser
59
+ * @param {TwigHandler|TwigHandler[]} handler - Function or array of function to handle elements
60
+ * @param {?ParserOptions} options - Object of optional options
61
+ * @throws {UnsupportedParser} - For an unsupported parser. Currently `expat` and `sax` (default) are supported.
62
+ */
63
+ function createParser(handler, options) {
64
+ options = Object.assign({ method: SAX, encoding: 'UTF-8', xmlns: false, trim: true, resumeAfterError: false, partial: false }, options)
65
+ let parser;
66
+ let namespaces = {};
67
+ let closeEvent;
68
+
69
+ // `parser.on("...", err => {...}` does not work, because I need access to 'this'
70
+ if (options.method === SAX) {
71
+ // Set options to have the same behaviour as in expat
72
+ parser = require("sax").createStream(true, { strictEntities: true, position: true, xmlns: options.xmlns, trim: options.trim });
73
+
74
+ closeEvent = "closetag";
75
+ parser.on("opentagstart", function (node) {
76
+ if (tree === undefined) {
77
+ tree = new Twig(node.name, current);
78
+ } else {
79
+ if (current.isRoot && current.name === undefined) {
80
+ current.setRoot(node.name);
81
+ } else {
82
+ let elt = new Twig(node.name, current);
83
+ if (options.partial) {
84
+ for (let hndl of Array.isArray(handler) ? handler : [handler]) {
85
+ if (typeof hndl.name === 'string' && node.name === hndl.name) {
86
+ elt.pin();
87
+ break;
88
+ } else if (hndl.name instanceof RegExp && hndl.name.test(node.name)) {
89
+ elt.pin();
90
+ break;
91
+ } else if (typeof hndl.name === 'function' && hndl.name(node.name, current ?? tree)) {
92
+ elt.pin();
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ if (options.xmlns) {
100
+ if (node.name.includes(':')) {
101
+ let prefix = node.name.split(':')[0];
102
+ if (namespaces[prefix] !== undefined) {
103
+ Object.defineProperty(current, 'namespace', {
104
+ value: { local: prefix, uri: namespaces[prefix] },
105
+ writable: false,
106
+ enumerable: true
107
+ });
108
+ }
109
+ }
110
+ }
111
+ })
112
+
113
+ parser.on("processinginstruction", function (pi) {
114
+ if (pi.name === 'xml') {
115
+ // SAX parser handles XML declaration as Processing Instruction
116
+ let declaration = {};
117
+ for (let item of pi.body.split(' ')) {
118
+ let [k, v] = item.split('=');
119
+ declaration[k] = v.replaceAll('"', '').replaceAll("'", '');
120
+ }
121
+ tree = new Twig(null);
122
+ Object.defineProperty(tree, 'declaration', {
123
+ value: declaration,
124
+ writable: false,
125
+ enumerable: true
126
+ });
127
+ } else {
128
+ Object.defineProperty(tree, 'PI', {
129
+ value: { target: pi.name, data: pi.body },
130
+ writable: false,
131
+ enumerable: true
132
+ });
133
+ }
134
+ })
135
+
136
+ parser.on("attribute", function (attr) {
137
+ current.attribute(attr.name, attr.value);
138
+ if (attr.uri !== undefined && attr.uri !== '') {
139
+ namespaces[attr.local] = attr.uri;
140
+ Object.defineProperty(current, 'namespace', {
141
+ value: { local: attr.local, uri: attr.uri },
142
+ writable: false,
143
+ enumerable: true
144
+ });
145
+ }
146
+ })
147
+ parser.on("cdata", function (str) {
148
+ current.text = str;
149
+ })
150
+
151
+ if (typeof handler === 'function') {
152
+ parser.on("end", function () {
153
+ if (typeof handler === 'function')
154
+ handler(tree);
155
+ })
156
+ }
157
+ } else if (options.method === EXPAT) {
158
+ parser = require("node-expat").createParser();
159
+ parser.encoding = options.encoding;
160
+ closeEvent = "endElement";
161
+
162
+ parser.on("startElement", function (name, attrs) {
163
+ if (tree === undefined) {
164
+ tree = new Twig(name, current, attrs);
165
+ } else {
166
+ if (current.isRoot && current.name === undefined) {
167
+ current.setRoot(name);
168
+ } else {
169
+ let elt = new Twig(name, current, attrs);
170
+ if (options.partial) {
171
+ for (let hndl of Array.isArray(handler) ? handler : [handler]) {
172
+ if (typeof hndl.name === 'string' && name === hndl.name) {
173
+ elt.pin();
174
+ break;
175
+ } else if (hndl.name instanceof RegExp && hndl.name.test(name)) {
176
+ elt.pin();
177
+ break;
178
+ } else if (typeof hndl.name === 'function' && hndl.name(name, current ?? tree)) {
179
+ elt.pin();
180
+ break;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ if (options.xmlns) {
187
+ for (let key of Object.keys(attrs).filter(x => x.startsWith('xmlns:')))
188
+ namespaces[key.split(':')[1]] = attrs[key];
189
+ if (name.includes(':')) {
190
+ let prefix = name.split(':')[0];
191
+ if (namespaces[prefix] !== undefined) {
192
+ Object.defineProperty(current, 'namespace', {
193
+ value: { local: prefix, uri: namespaces[prefix] },
194
+ writable: false,
195
+ enumerable: true
196
+ });
197
+ }
198
+ }
199
+ }
200
+ })
201
+
202
+ parser.on('xmlDecl', function (version, encoding, standalone) {
203
+ tree = new Twig(null);
204
+ let dec = {};
205
+ if (version !== undefined) dec.version = version;
206
+ if (encoding !== undefined) dec.encoding = encoding;
207
+ if (standalone !== undefined) dec.standalone = standalone;
208
+ Object.defineProperty(tree, 'declaration', {
209
+ value: dec,
210
+ writable: false,
211
+ enumerable: true
212
+ });
213
+ })
214
+
215
+ parser.on('processingInstruction', function (target, data) {
216
+ tree.PI = { target: target, data: data };
217
+ })
218
+ } else {
219
+ throw new UnsupportedParser(options.method);
220
+ }
221
+
222
+ parser.on(closeEvent, function (name) {
223
+ let pos;
224
+ if (options.method === SAX) {
225
+ pos = { line: this._parser.line + 1, column: this._parser.column + 1 };
226
+ } else if (options.method === EXPAT) {
227
+ pos = { line: this.parser.getCurrentLineNumber(), column: this.parser.getCurrentColumnNumber() };
228
+ }
229
+ current.close(pos);
230
+ let purge = true;
231
+
232
+ if (typeof handler === 'function' && options.method === EXPAT && current.isRoot) {
233
+ // Entire XML file was parsed at once. EXPAT parser has no event "document end", so trigger at "endElement" of root object
234
+ handler(tree);
235
+ } else {
236
+ for (let hndl of Array.isArray(handler) ? handler : [handler]) {
237
+ if (hndl.name === undefined) {
238
+ hndl.function(current ?? tree);
239
+ purge = false;
240
+ } else if (typeof hndl.name === 'string' && name === hndl.name) {
241
+ hndl.function(current ?? tree);
242
+ purge = false;
243
+ } else if (hndl.name instanceof RegExp && hndl.name.test(name)) {
244
+ hndl.function(current ?? tree);
245
+ purge = false;
246
+ } else if (typeof hndl.name === 'function' && hndl.name(name, current ?? tree)) {
247
+ hndl.function(current ?? tree);
248
+ purge = false;
249
+ }
250
+ }
251
+ }
252
+
253
+ if (options.partial && purge && !current.pinned && !current.isRoot)
254
+ current.purge();
255
+ current = current.parent();
256
+
257
+
258
+ })
259
+
260
+ // Common events
261
+ parser.on('text', function (str) {
262
+ current.text = options.trim ? str.trim() : str;
263
+ })
264
+
265
+ parser.on("comment", function (str) {
266
+ if (current.hasOwnProperty('comment')) {
267
+ if (typeof current.comment === 'string') {
268
+ current.comment = [current.comment, str.trim()]
269
+ } else {
270
+ current.comment.push(str.trim());
271
+ }
272
+ } else {
273
+ Object.defineProperty(current, 'comment', {
274
+ value: str.trim(),
275
+ writable: true,
276
+ enumerable: true,
277
+ configurable: true
278
+ });
279
+ }
280
+
281
+ })
282
+
283
+ parser.on('error', function (err) {
284
+ let p;
285
+ if (options.method === SAX) {
286
+ p = this._parser;
287
+ console.error(`error at line [${p.line + 1}], column [${p.column + 1}]`, err);
288
+ } else if (options.method === EXPAT) {
289
+ p = this.parser;
290
+ console.error(`error at line [${p.getCurrentLineNumber()}], column [${p.getCurrentColumnNumber()}]`, err);
291
+ }
292
+ if (options.resumeAfterError) {
293
+ p.error = null;
294
+ p.resume();
295
+ }
296
+ });
297
+
298
+ return parser;
299
+ }
300
+
301
+
302
+ /**
303
+ * Generic class modeling a XML Node
304
+ * @class Twig
305
+ */
306
+ class Twig {
307
+ /**
308
+ * Optional condition to get attributes<br>
309
+ * - If `undefined`, then all attributes are returned.<br>
310
+ * - If `string` then the attribute name must be equal to the string
311
+ * - If `RegExp` then the attribute name must match the Regular Expression
312
+ * - If [AttributeConditionFilter](#AttributeConditionFilter) then the attribute must filter function
313
+ * @typedef {string|RegExp|AttributeConditionFilter} AttributeCondition
314
+ */
315
+
316
+ /**
317
+ * Custom filter function to get desired attributes
318
+ * @typedef {function} AttributeConditionFilter
319
+ * @param {string} name - Name of the attribute
320
+ * @param {string|number} value - Value of the attribute
321
+ */
322
+
323
+ /**
324
+ * XML Processing Instruction object, exist only on root
325
+ * @typedef {object} #PI
326
+ */
327
+
328
+ /**
329
+ * XML Declaration object, exist only on root
330
+ * @typedef {object} #declaration
331
+ */
332
+
333
+ /**
334
+ * XML namespace of element. Exist onl when parsed with `xmlns: true`
335
+ * @typedef {object} #namespace
336
+ */
337
+
338
+ /**
339
+ * Comment or array of comments inside the XML Elements
340
+ * @typedef {string|string[]} #comment
341
+ */
342
+
343
+ /**
344
+ * @property {?object} #attributes - XML attribute `{ <attribute 1>: <value 1>, <attribute 2>: <value 2>, ... }`
345
+ * @private
346
+ */
347
+ #attributes = {};
348
+
349
+ /**
350
+ * @property {?string|number} #text - Content of XML Element
351
+ * @private
352
+ */
353
+ #text = null;
354
+
355
+ /**
356
+ * @property {string} #name - The XML tag name
357
+ * @private
358
+ */
359
+ #name;
360
+
361
+ /**
362
+ * @property {?Twig[]} #children - Child XML Elements
363
+ * @private
364
+ */
365
+ #children = [];
366
+
367
+ /**
368
+ * @property {?Twig} #parent - The parent object. Undefined on root element
369
+ * @private
370
+ */
371
+ #parent;
372
+
373
+ /**
374
+ * @property {object} #postion - The postion of the element in #children array
375
+ * @private
376
+ */
377
+ #postion = {};
378
+
379
+ /**
380
+ * @property {number} #level - Root element is level 0, children have 1 and so on
381
+ * @private
382
+ */
383
+ #level;
384
+
385
+ /**
386
+ * @property {boolean} #pinned - Determines whether twig is needed in partial load
387
+ * @private
388
+ */
389
+ #pinned = false;
390
+
391
+ /**
392
+ * Create a new Twig object
393
+ * @param {string} name - The name of the XML element
394
+ * @param {Twig} parent - The parent object
395
+ * @param {?object} attributes - Attriubte object
396
+ */
397
+ constructor(name, parent, attributes) {
398
+ current = this;
399
+ if (name === null) {
400
+ // Root element not available yet
401
+ tree = this;
402
+ this.#level = 0;
403
+ } else {
404
+ this.#name = name;
405
+ if (attributes !== undefined)
406
+ this.#attributes = attributes;
407
+ if (parent === undefined) {
408
+ // Root element
409
+ tree = this;
410
+ this.#level = 0;
411
+ } else {
412
+ this.#parent = parent;
413
+ this.#level = this.#parent.#level + 1;
414
+ if (this.#parent.#pinned)
415
+ this.#pinned = true;
416
+ parent.#children.push(this);
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Purges the current, typically used after element has been processed.<br>The root object cannot be purged.
423
+ */
424
+ purge() {
425
+ if (!this.isRoot)
426
+ this.#parent.#children = this.#parent.#children.filter(x => !Object.is(this, x));
427
+ }
428
+
429
+ /**
430
+ * Purges up to the elt element. This allows you to keep part of the tree in memory when you purge.
431
+ * @param {Twig} elt - Up to this element the tree will be purged. The `elt` object itself is not purged.<br>
432
+ * If `undefined` then the current element is purged (i.e. `purge()`)
433
+ */
434
+ purgeUpTo(elt) {
435
+ if (elt === undefined) {
436
+ this.purge();
437
+ } else {
438
+ let purgeThis = this.descendantOrSelf();
439
+ purgeThis = purgeThis[purgeThis.length - 1];
440
+ let stopAt = elt.descendantOrSelf();
441
+ stopAt = stopAt[stopAt.length - 1];
442
+ while (purgeThis !== null && !Object.is(purgeThis, stopAt)) {
443
+ let prev = purgeThis.previous();
444
+ purgeThis.purge();
445
+ purgeThis = prev;
446
+ }
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Sets the name of root element. In some cases the root is created before the XML-Root element is available<br>
452
+ * Used internally!
453
+ * @param {string} name - The element name
454
+ * @private
455
+ */
456
+ setRoot(name) {
457
+ if (this.isRoot)
458
+ this.#name = name;
459
+ }
460
+
461
+ /**
462
+ * Returns `true` if the element is empty, otherwise `false`.
463
+ * An empty element has no text nor any child elements, however empty elements can have attributes.
464
+ * @returns {boolean} true if empty element
465
+ */
466
+ get isEmpty() {
467
+ return this.#text === null && this.#children.length == 0;
468
+ }
469
+
470
+ /**
471
+ * Returns the level of the element. Root element has 0, children have 1, grand-children 2 and so on
472
+ * @returns {number} The level of the element.
473
+ */
474
+ get level() {
475
+ return this.#level;
476
+ }
477
+
478
+ /**
479
+ * Returns `true` if element is the root object
480
+ * @returns {boolean} true if root element
481
+ */
482
+ get isRoot() {
483
+ return this.#parent === undefined;
484
+ }
485
+
486
+ /**
487
+ * Returns `true` if element has child elements
488
+ * @returns {boolean} true if has child elements exists
489
+ */
490
+ get hasChildren() {
491
+ return this.#children.length > 0;
492
+ }
493
+
494
+ /**
495
+ * Returns the line where current element is closed
496
+ * @returns {number} Current line
497
+ */
498
+ get line() {
499
+ return this.#postion.line;
500
+ }
501
+
502
+ /**
503
+ * Returns the column where current element is closed
504
+ * @returns {number} Current column
505
+ */
506
+ get column() {
507
+ return this.#postion.column;
508
+ }
509
+
510
+ /**
511
+ * The position in `#children` array. For root object 0
512
+ * @returns {number} Position of element in parent
513
+ */
514
+ get index() {
515
+ return this.isRoot ? 0 : this.#parent.#children.indexOf(this);
516
+ }
517
+
518
+ /**
519
+ * Returns the name of the element.
520
+ * @returns {string} Element name
521
+ */
522
+ get name() {
523
+ return this.#name;
524
+ }
525
+ /**
526
+ * Returns the name of the element. Synonym for `twig.name`
527
+ * @returns {string} Element name
528
+ */
529
+ get tag() {
530
+ return this.#name;
531
+ }
532
+
533
+ /**
534
+ * The text of the element. No matter if given as text or CDATA entity
535
+ * @returns {string} Element text or empty string
536
+ */
537
+ get text() {
538
+ return this.#text ?? '';
539
+ }
540
+
541
+ /**
542
+ * Modifies the text of the element
543
+ * @param {string} value - New value of the attribute
544
+ * @throws {UnsupportedType} - If value is not a string or numeric type
545
+ */
546
+ set text(value) {
547
+ if (!['string', 'number', 'bigint'].includes(typeof value))
548
+ throw new UnsupportedType(value);
549
+ this.#text = this.#text ?? '' + value;
550
+ }
551
+
552
+ /**
553
+ * Pins the current element. Used for partial reading.
554
+ */
555
+ pin = function () {
556
+ this.#pinned = true;
557
+ }
558
+
559
+ /**
560
+ * Checks if element is pinned
561
+ * @returns {boolean} `true` when the element is pinned
562
+ */
563
+ get pinned() {
564
+ return this.#pinned;
565
+ }
566
+
567
+ /**
568
+ * Modifies the text of the element
569
+ * @param {string} value - New value of the attribute
570
+ * @throws {UnsupportedType} - If value is not a string or numeric type
571
+ */
572
+ set text(value) {
573
+ if (!['string', 'number', 'bigint'].includes(typeof value))
574
+ throw new UnsupportedType(value);
575
+ this.#text = this.#text ?? '' + value;
576
+ }
577
+
578
+
579
+ /**
580
+ * Closes the element
581
+ * @param {object} pos - The current possion (line and column) in the XML document
582
+ */
583
+ close = function (pos) {
584
+ this.#postion = pos;
585
+ Object.seal(this);
586
+ }
587
+
588
+ /**
589
+ * Internal recursive function used by `writer()`
590
+ * @param {XMLWriter} xw - The writer object
591
+ * @param {Twig[]} children - Array of child elements
592
+ */
593
+ #addChild = function (xw, childArray) {
594
+ for (let elt of childArray) {
595
+ xw.startElement(elt.name);
596
+ for (let key in elt.attributes)
597
+ xw.writeAttribute(key, elt.attributes[key]);
598
+ if (elt.text !== null)
599
+ xw.text(elt.text);
600
+ this.#addChild(xw, elt.children());
601
+ }
602
+ xw.endElement();
603
+ }
604
+
605
+
606
+ /**
607
+ * Creates xml-writer from current element
608
+ * @param {?boolean|string|XMLWriter} par - `true` or intention character or an already created XMLWriter
609
+ * @returns {XMLWriter}
610
+ */
611
+ writer = function (par) {
612
+ const XMLWriter = require('xml-writer');
613
+ let xw = par instanceof XMLWriter ? par : new XMLWriter(par);
614
+
615
+ xw.startElement(this.#name);
616
+ for (let key in this.#attributes)
617
+ xw.writeAttribute(key, this.#attributes[key]);
618
+ if (this.#text !== null)
619
+ xw.text(this.#text);
620
+ this.#addChild(xw, this.#children);
621
+ xw.endElement();
622
+ return xw;
623
+ }
624
+
625
+ /**
626
+ * Returns attriute value or `null` if not found.<br>
627
+ * If more than one matches the condition, then it returns object as [attribute()](#attribute)
628
+ * @param {?AttributeCondition} condition - Optional condition to select attribute
629
+ * @returns {string|number|object} - The value of the attrubute or `null` if the does not exist
630
+ */
631
+ attr = function (condition) {
632
+ let attr = this.attribute(condition);
633
+ if (attr === null)
634
+ return null;
635
+
636
+ return Object.keys(attr).length === 1 ? attr[Object.keys(attr)[0]] : attr;
637
+ }
638
+
639
+ /**
640
+ * Returns all attributes of the element
641
+ * @returns {obejct} All XML Atrributes
642
+ */
643
+ get attributes() {
644
+ return this.#attributes;
645
+ }
646
+
647
+ /**
648
+ * Check if the attribute exist or not
649
+ * @param {string} name - The name of the attribute
650
+ * @returns {boolean} - Returns `true` if the attribute exists, else `false`
651
+ */
652
+ hasAttribute = function (name) {
653
+ return this.#attributes[name] !== undefined;
654
+ }
655
+
656
+ /**
657
+ * Retrieve or update XML attribute.
658
+ * @param {?AttributeCondition} condition - Optional condition to select attributes
659
+ * @param {?string|number} text - New value of the attribute
660
+ * @returns {object} Attributes or `null` if no matching attribute found
661
+ * @example attribute((name, val) => { return name === 'age' && val > 50})
662
+ * attribute((name) => { return ['firstName', 'lastName'].includes(name) })
663
+ * attribute('firstName')
664
+ * attribute(/name/i)
665
+ */
666
+ attribute = function (condition, text) {
667
+ if (text === undefined) {
668
+ let attr;
669
+ if (condition === undefined) {
670
+ attr = this.#attributes;
671
+ } else if (typeof condition === 'function') {
672
+ attr = Object.fromEntries(Object.entries(this.#attributes).filter(([key, val]) => condition(key, val)));
673
+ } else if (typeof condition === 'string') {
674
+ attr = this.attribute(key => { return key === condition });
675
+ } else if (condition instanceof RegExp) {
676
+ attr = this.attribute(key => { return condition.test(key) });
677
+ } else if (condition instanceof Twig) {
678
+ throw new UnsupportedCondition(condition, ['string', 'RegEx', 'function']);
679
+ } else {
680
+ return this.attribute();
681
+ }
682
+ return attr === null || Object.keys(attr).length == 0 ? null : attr;
683
+ } else {
684
+ if (text === null) {
685
+ delete this.#attributes[condition];
686
+ } else {
687
+ if (!['string', 'number', 'bigint'].includes(typeof text))
688
+ throw new UnsupportedType(text);
689
+ if (typeof condition !== 'string')
690
+ throw new UnsupportedCondition(condition, ['string']);
691
+ this.#attributes[condition] = text;
692
+ }
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Returns the root object
698
+ * @returns {Twig} The root element of XML tree
699
+ */
700
+ root = function () {
701
+ if (this.isRoot) {
702
+ return this;
703
+ } else {
704
+ let ret = this.#parent;
705
+ while (!ret.isRoot) {
706
+ ret = ret.#parent;
707
+ }
708
+ return ret;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Returns the parent element or null if root element
714
+ * @returns {Twig} The parament element
715
+ */
716
+ parent = function () {
717
+ return this.isRoot ? null : this.#parent;
718
+ }
719
+
720
+ /**
721
+ * @returns {Twig} - The current element
722
+ */
723
+ self = function () {
724
+ return this;
725
+ }
726
+
727
+ /**
728
+ * Common function to filter Twig elements from array
729
+ * @param {Twig|Twig[]} elts - Array of elements you like to filter or a single element
730
+ * @param {?ElementCondition} condition - The filter condition
731
+ * @returns {Twig[]} List of matching elements or empty array
732
+ */
733
+ filterElements(elts, condition) {
734
+ if (!Array.isArray(elts))
735
+ return filterElements([elts], condition);
736
+
737
+ if (condition === undefined) {
738
+ return elts;
739
+ } else if (typeof condition === 'string') {
740
+ return elts.filter(x => x.name === condition);
741
+ //return this.filterElements(elts, x => { return x === condition });
742
+ } else if (condition instanceof RegExp) {
743
+ return elts.filter(x => x.condition.test(x.name));
744
+ } else if (condition instanceof Twig) {
745
+ return elts.filter(x => Object.is(x, condition));
746
+ } else if (typeof condition === 'function') {
747
+ return elts.filter(x => condition(x.name, x));
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Common function to filter Twig element
753
+ * @param {Twig} elt - Element you like to filter
754
+ * @param {?ElementCondition} condition - The filter condition
755
+ * @returns {boolean} `true` if the condition matches
756
+ */
757
+ testElement(elt, condition) {
758
+ if (condition === undefined) {
759
+ return true;
760
+ } else if (typeof condition === 'string') {
761
+ return elt.name === condition;
762
+ } else if (condition instanceof RegExp) {
763
+ return condition.test(elt.name);
764
+ } else if (condition instanceof Twig) {
765
+ return Object.is(elt, condition);
766
+ } else if (typeof condition === 'function') {
767
+ return condition(elt.name, elt);
768
+ }
769
+ return false;
770
+ }
771
+
772
+ /**
773
+ * All children, optionally matching `condition` of the current element or empty array
774
+ * @param {?ElementCondition} condition - Optional condition
775
+ * @returns {Twig[]}
776
+ */
777
+ children = function (condition) {
778
+ return this.filterElements(this.#children, condition);
779
+ }
780
+
781
+ /**
782
+ * Returns the next matching element.
783
+ * @param {?ElementCondition} condition - Optional condition
784
+ * @returns {Twig} - The next element
785
+ * @see https://www.w3.org/TR/xpath-datamodel-31/#document-order
786
+ */
787
+ next = function (condition) {
788
+ if (this === null)
789
+ return null;
790
+
791
+ let elt;
792
+ if (this.hasChildren) {
793
+ elt = this.#children[0];
794
+ } else {
795
+ elt = this.nextSibling();
796
+ if (elt === null) {
797
+ elt = this.#parent;
798
+ elt = elt.nextSibling();
799
+ }
800
+ }
801
+ if (elt === null)
802
+ return null;
803
+
804
+ return this.testElement(elt, condition) ? elt : elt.next(condition);
805
+ }
806
+
807
+ /**
808
+ * Returns the previous matching element.
809
+ * @param {?ElementCondition} condition - Optional condition
810
+ * @returns {Twig} - The previous element
811
+ * @see https://www.w3.org/TR/xpath-datamodel-31/#document-order
812
+ */
813
+ previous = function (condition) {
814
+ if (this === null || this.isRoot)
815
+ return null;
816
+
817
+ let elt = this.prevSibling();
818
+ if (elt === null) {
819
+ elt = this.parent();
820
+ } else {
821
+ elt = elt.descendantOrSelf();
822
+ elt = elt[elt.length - 1];
823
+ }
824
+
825
+ return this.testElement(elt, condition) ? elt : elt.previous(condition);
826
+ }
827
+
828
+ /**
829
+ * Returns the first matching element. This is usally the first child element
830
+ * @param {?ElementCondition} condition - Optional condition
831
+ * @returns {Twig} - The first element
832
+ */
833
+ first = function (condition) {
834
+ if (this === null)
835
+ return null;
836
+ return this.testElement(this.root(), condition) ? this.root() : this.root().next(condition);
837
+ }
838
+
839
+ /**
840
+ * Returns the last matching element. This is usally the root element
841
+ * @param {?ElementCondition} condition - Optional condition
842
+ * @returns {Twig} - The last element
843
+ */
844
+ last = function (condition) {
845
+ if (this === null)
846
+ return null;
847
+
848
+ let elt = this.root();
849
+ if (this.root().hasChildren) {
850
+ elt = this.root().#children[this.root().#children.length - 1];
851
+ while (elt.hasChildren)
852
+ elt = elt.children()[elt.children().length - 1];
853
+ }
854
+
855
+ return this.testElement(elt, condition) ? elt : elt.previous(condition);
856
+ }
857
+
858
+ /**
859
+ * Check if the element is the first child of the parent
860
+ * @returns {boolean} `true` if the first child else `false`
861
+ */
862
+ get isFirstChild() {
863
+ if (this.isRoot) {
864
+ return false;
865
+ } else {
866
+ return this.index === 0
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Check if the element is the last child of the parent
872
+ * @returns {boolean} `true` if the last child else `false`
873
+ */
874
+ get isLastChild() {
875
+ if (this.isRoot) {
876
+ return false;
877
+ } else {
878
+ return this.index === this.#parent.#children.length - 1;
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Returns descendants (children, grandchildren, etc.) of the current element
884
+ * @param {?ElementCondition} condition - Optional condition
885
+ * @returns {Twig[]} - Array of descendants or empty array
886
+ */
887
+ descendant = function (condition) {
888
+ let elts = [];
889
+ for (let c of this.#children) {
890
+ elts.push(c);
891
+ elts = elts.concat(c.descendant());
892
+ }
893
+ return this.filterElements(elts, condition);
894
+ }
895
+
896
+ /**
897
+ * Returns descendants (children, grandchildren, etc.) of the current element and the current element itself
898
+ * @param {?ElementCondition} condition - Optional condition
899
+ * @returns {Twig[]} - Array of descendants or empty array
900
+ */
901
+ descendantOrSelf = function (condition) {
902
+ let elts = [this];
903
+ for (let c of this.#children) {
904
+ elts.push(c);
905
+ elts = elts.concat(c.descendant());
906
+ }
907
+ return this.filterElements(elts, condition);
908
+ }
909
+
910
+ /**
911
+ * Returns ancestors (parent, grandparent, etc.) of the current element
912
+ * @param {?ElementCondition} condition - Optional condition
913
+ * @returns {Twig[]} - Array of ancestors or empty array
914
+ */
915
+ ancestor = function (condition) {
916
+ let elts = [];
917
+ if (!this.isRoot) {
918
+ let parent = this.#parent;
919
+ elts.push(parent);
920
+ while (!parent.isRoot) {
921
+ parent = parent.#parent;
922
+ elts.push(parent);
923
+ }
924
+ }
925
+ return this.filterElements(elts, condition);
926
+ }
927
+
928
+ /**
929
+ * Returns ancestors (parent, grandparent, etc.) of the current element and the current element itself
930
+ * @param {?ElementCondition} condition - Optional condition
931
+ * @returns {Twig[]} - Array of ancestors or empty array
932
+ */
933
+ ancestorOrSelf = function (condition) {
934
+ let elts = [this];
935
+ if (!this.isRoot) {
936
+ let parent = this.#parent;
937
+ elts.push(parent);
938
+ while (!parent.isRoot) {
939
+ parent = parent.#parent;
940
+ elts.push(parent);
941
+ }
942
+ }
943
+ return this.filterElements(elts, condition);
944
+ }
945
+
946
+ /**
947
+ * Returns all sibling element of the current element
948
+ * @param {?ElementCondition} condition - Optional condition
949
+ * @returns {Twig[]} - Array of sibling or empty array
950
+ */
951
+ sibling = function (condition) {
952
+ let elts = [];
953
+ if (!this.isRoot) {
954
+ elts = this.#parent.#children.filter(x => !Object.is(x, this));
955
+ }
956
+ return this.filterElements(elts, condition);
957
+ }
958
+
959
+ /**
960
+ * Returns all sibling element of the current element and the current element itself
961
+ * @param {?ElementCondition} condition - Optional condition
962
+ * @returns {Twig[]} - Array of sibling or empty array
963
+ */
964
+ siblingOrSelf = function (condition) {
965
+ let elts = [this];
966
+ if (!this.isRoot) {
967
+ elts = this.#parent.#children;
968
+ }
969
+ return this.filterElements(elts, condition);
970
+ }
971
+
972
+ /**
973
+ * Returns all following sibling element of the current element
974
+ * @param {?ElementCondition} condition - Optional condition
975
+ * @returns {Twig[]} - Array of sibling or empty array
976
+ */
977
+ followingSibling = function (condition) {
978
+ let elts = [];
979
+ if (!this.isRoot) {
980
+ elts = this.#parent.#children.slice(this.index + 1);
981
+ }
982
+ return this.filterElements(elts, condition);
983
+ }
984
+
985
+ /**
986
+ * Returns all preceding sibling element of the current element
987
+ * @param {?ElementCondition} condition - Optional condition
988
+ * @returns {Twig[]} - Array of sibling or empty array
989
+ */
990
+ precedingSibling = function (condition) {
991
+ let elts = [];
992
+ if (!this.isRoot) {
993
+ elts = this.#parent.#children.slice(0, this.index);
994
+ }
995
+ return this.filterElements(elts, condition);
996
+ }
997
+
998
+ /**
999
+ * Returns next sibling element of the current element
1000
+ * @param {?ElementCondition} condition - Optional condition
1001
+ * @returns {Twig} - The next sibling or `null`
1002
+ */
1003
+ nextSibling = function (condition) {
1004
+ let elt;
1005
+ if (!this.isRoot)
1006
+ elt = this.#parent.#children[this.index + 1];
1007
+ if (elt === undefined)
1008
+ return null;
1009
+
1010
+ return this.testElement(elt, condition) ? elt : elt.nextSibling(condition);
1011
+ }
1012
+
1013
+ /**
1014
+ * Returns previous sibling element of the current element
1015
+ * @param {?ElementCondition} condition - Optional condition
1016
+ * @returns {Twig} - The previous sibling or `null`
1017
+ */
1018
+ prevSibling = function (condition) {
1019
+ if (!this.isRoot && this.index > 0) {
1020
+ let elt = this.#parent.#children[this.index - 1];
1021
+ return this.testElement(elt, condition) ? elt : elt.prevSibling(condition);
1022
+ } else {
1023
+ return null;
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Find a specific element within current element. Same as `.descendant(condition)[0]`
1029
+ * @param {ElementCondition} condition - Find condition
1030
+ * @returns {Twig} - First matching element or `null`
1031
+ */
1032
+ find = function (condition) {
1033
+ let children = this.filterElements(this.#children, condition);
1034
+ if (children.length > 0)
1035
+ return children[0];
1036
+
1037
+ for (let child of this.#children) {
1038
+ let ret = child.find(condition);
1039
+ if (ret !== null)
1040
+ return ret;
1041
+ }
1042
+ return null;
1043
+ }
1044
+
1045
+ }
1046
+
1047
+
1048
+ /**
1049
+ * Error for unsupported data types
1050
+ * @exception UnsupportedParser
1051
+ */
1052
+ class UnsupportedParser extends TypeError {
1053
+ /**
1054
+ * @param {string} t Parser type
1055
+ */
1056
+ constructor(t) {
1057
+ super(`Parser '${t}' is not supported. Use 'expat' or 'sax' (default)`);
1058
+ }
1059
+ }
1060
+
1061
+
1062
+ /**
1063
+ * Generic error for unsupported data types
1064
+ * @exception UnsupportedType
1065
+ */
1066
+ class UnsupportedType extends TypeError {
1067
+ /**
1068
+ * @param {*} t Parameter which was used
1069
+ */
1070
+ constructor(t) {
1071
+ super(`Type ${typeof t} is not supported in XML`);
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Generic error for unsupported data types
1077
+ * @exception UnsupportedCondition
1078
+ */
1079
+ class UnsupportedCondition extends TypeError {
1080
+ /**
1081
+ * @param {*} condition The condition value
1082
+ * @param {string[]} t List of supported data types
1083
+ */
1084
+ constructor(condition, t) {
1085
+ super(`Condition '${JSON.stringify(condition)}' must be a ${t.map(x => `'${x}'`).join(' or ')}`);
1086
+ }
1087
+ }
1088
+
1089
+
1090
+
1091
+
1092
+
1093
+ module.exports = { createParser, Twig };
1094
+
1095
+ /*
1096
+ // All events emit with a single argument. To listen to an event, assign a function to on<eventname>.
1097
+ sax.EVENTS = [
1098
+ 'text',
1099
+ 'processinginstruction',
1100
+ 'sgmldeclaration',
1101
+ 'doctype',
1102
+ 'comment',
1103
+ 'opentagstart',
1104
+ 'attribute',
1105
+ 'opentag',
1106
+ 'closetag',
1107
+ 'opencdata',
1108
+ 'cdata',
1109
+ 'closecdata',
1110
+ 'error',
1111
+ 'end',
1112
+ 'ready',
1113
+ 'script',
1114
+ 'opennamespace',
1115
+ 'closenamespace'
1116
+ ]
1117
+ parser.onerror = function (e) {
1118
+ // an error happened.
1119
+ };
1120
+
1121
+
1122
+
1123
+
1124
+ node-expat.events = {
1125
+ #on('startElement' function (name, attrs) {})
1126
+ #on('endElement' function (name) {})
1127
+ #on('text' function (text) {})
1128
+ #on('processingInstruction', function (target, data) {})
1129
+ #on('comment', function (s) {})
1130
+ #on('xmlDecl', function (version, encoding, standalone) {})
1131
+ #on('startCdata', function () {})
1132
+ #on('endCdata', function () {})
1133
+ #on('entityDecl', function (entityName, isParameterEntity, value, base, systemId, publicId, notationName) {})
1134
+ #on('error', function (e) {})
1135
+ #stop() pauses
1136
+ #resume() resumes
1137
+ }
1138
+
1139
+ */