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