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/LICENSE +674 -0
- package/README.md +378 -0
- package/demo/demo.js +18 -0
- package/demo/memory-test.js +71 -0
- package/demo/speed-test.js +66 -0
- package/doc/build.sh +6 -0
- package/doc/twig.md +1449 -9
- package/package.json +37 -0
- package/samples/bookstore.xml +48 -0
- package/samples/breakfast-menu.xml +43 -0
- package/samples/processingInstruction.xml +29 -0
- package/samples/xmlns.xml +19 -0
- package/twig.js +1139 -0
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
|
+
*/
|