xml-twig 1.2.94 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,22 +18,22 @@ When you need to read a XML file, then you have two principles:
18
18
  This module tries to combine both principles. The XML document can be read in chunks and within a chunk you have all the nice features and functions you know from a DOM based parser.
19
19
 
20
20
  ## Dependencies
21
- XML documents are read either with [sax](https://www.npmjs.com/package/sax) or [node-expat](https://www.npmjs.com/package/node-expat) parser. More parser may be added in future releases. By default the `sax` parser is used.
21
+ XML documents are read either with [sax](https://www.npmjs.com/package/sax), [node-expat](https://www.npmjs.com/package/node-expat) or [saxophone](https://www.npmjs.com/package/saxophone) parser. More parser may be added in future releases. By default the `sax` parser is used.
22
22
 
23
- **NOTE: The `node-expat` module is not automatically installed with this module. Install the parser by yourself, if you like to use it**
23
+ **NOTE: The `node-expat` and `saxophone` modules are not automatically installed with this module. Install the parser by yourself, if you like to use it**
24
24
 
25
25
  ## Installation
26
26
 
27
- Install module like any other node module and optionally `node-expat`:
27
+ Install module like any other node module and optionally `node-expat` and/or `saxophone`:
28
28
  ```bash
29
29
  npm install xml-twig
30
30
 
31
31
  # and optionally
32
32
  npm install node-expat
33
+ npm install saxophone
33
34
 
34
35
  ```
35
- In my tests I parsed a 750 MB big XML file, the `node-expat` is around two times faster than `sax` (node-expat: 2:20 Minutes, sax: 4:20 Minutes). However, you may run into problems when you try to install the `node-expat` parser. That's the reason why `node-expat` parser is not installed automatically.
36
-
36
+ In my tests I parsed a 900 MB big XML file, the `node-expat` is faster than `sax` (node-expat: around 2:50 Minutes, sax: around 4:10 Minutes). However, you may run into problems when you try to install the `node-expat` parser. That's the reason why `node-expat` parser is not installed automatically. `saxophone` is even a little faster (around 2:10 Minutes) than `node-expat`, however `saxophone` lacks some functions and is not fully compliant to XML standards.
37
37
 
38
38
  ## How to use it
39
39
 
@@ -392,7 +392,7 @@ This `xml-twig` module focus on reading a XML files. In principle it would be po
392
392
 
393
393
  Accessing Twig-Elements by [XML-Path](https://www.w3.org/TR/xpath/) language is not supported. One reason it, the `Twig` class models more an [Element](https://www.w3schools.com/xml/xml_elements.asp) rather than a [Node](https://www.w3schools.com/xml/dom_nodes.asp) which would be more generic.
394
394
 
395
- Despite [W3C Recommendations](https://www.w3.org/TR/xml/#charencoding) ("All XML processors MUST be able to read entities in both the UTF-8 and UTF-16 encodings"), the `sax` parser does not support UTF-16 encodings. When you have a XML-File encoded in UTF-16, then you must use the `expat` parser.
395
+ Despite [W3C Recommendations](https://www.w3.org/TR/xml/#charencoding) ("All XML processors MUST be able to read entities in both the UTF-8 and UTF-16 encodings"), the `sax` and `saxophone` parsers do not support UTF-16 encodings. When you have a XML-File encoded in UTF-16, then you must use the `expat` parser.
396
396
 
397
397
 
398
398
 
package/doc/twig.md CHANGED
@@ -34,6 +34,15 @@
34
34
  <dt><a href="#createParser">createParser(handler, [options])</a> ⇒ <code><a href="#Parser">Parser</a></code></dt>
35
35
  <dd><p>Create a new Twig parser</p>
36
36
  </dd>
37
+ <dt><a href="#onStart">onStart(binds, node, attrs)</a></dt>
38
+ <dd><p>Common Event hanlder for starting tag</p>
39
+ </dd>
40
+ <dt><a href="#onClose">onClose(handler, options, name)</a></dt>
41
+ <dd><p>Common Event hanlder for closing tag</p>
42
+ </dd>
43
+ <dt><a href="#reset">reset()</a></dt>
44
+ <dd><p>Reset global variable if one like to parse multiple files</p>
45
+ </dd>
37
46
  </dl>
38
47
 
39
48
  ## Typedefs
@@ -77,7 +86,7 @@ You can specify a <code>function</code> or a <code>event</code> name</p>
77
86
  <dt><a href="#ElementConditionFilter">ElementConditionFilter</a> ⇒ <code>boolean</code></dt>
78
87
  <dd><p>Custom filter function to select desired elements</p>
79
88
  </dd>
80
- <dt><a href="#Parser">Parser</a> ⇒ <code><a href="https://www.npmjs.com/package/sax">sax</a></code> | <code><a href="https://www.npmjs.com/package/node-expat">node-expat</a></code></dt>
89
+ <dt><a href="#Parser">Parser</a> ⇒ <code><a href="https://www.npmjs.com/package/sax">sax</a></code> | <code><a href="https://www.npmjs.com/package/node-expat">node-expat</a></code> | <code><a href="https://www.npmjs.com/package/saxophone">saxophone</a></code></dt>
81
90
  <dd></dd>
82
91
  <dt><a href="#AttributeCondition">AttributeCondition</a> : <code>string</code> | <code>RegExp</code> | <code><a href="#AttributeConditionFilter">AttributeConditionFilter</a></code></dt>
83
92
  <dd><p>Optional condition to get attributes<br> </p>
@@ -1409,6 +1418,38 @@ Create a new Twig parser
1409
1418
  | handler | [<code>TwigHandler</code>](#TwigHandler) \| [<code>Array.&lt;TwigHandler&gt;</code>](#TwigHandler) | Object or array of element specification and function to handle elements |
1410
1419
  | [options] | [<code>ParserOptions</code>](#ParserOptions) | Object of optional options |
1411
1420
 
1421
+ <a name="onStart"></a>
1422
+
1423
+ ## onStart(binds, node, attrs)
1424
+ Common Event hanlder for starting tag
1425
+
1426
+ **Kind**: global function
1427
+
1428
+ | Param | Type | Description |
1429
+ | --- | --- | --- |
1430
+ | binds | <code>object</code> | Additional parameter object |
1431
+ | node | <code>object</code> \| <code>string</code> | Node or Node name |
1432
+ | attrs | <code>object</code> | Node Attributes |
1433
+
1434
+ <a name="onClose"></a>
1435
+
1436
+ ## onClose(handler, options, name)
1437
+ Common Event hanlder for closing tag
1438
+
1439
+ **Kind**: global function
1440
+
1441
+ | Param | Type | Description |
1442
+ | --- | --- | --- |
1443
+ | handler | [<code>TwigHandler</code>](#TwigHandler) \| [<code>Array.&lt;TwigHandler&gt;</code>](#TwigHandler) | Object or array of element specification and function to handle elements |
1444
+ | options | [<code>ParserOptions</code>](#ParserOptions) | Object of optional options |
1445
+ | name | <code>string</code> | Event handler parameter |
1446
+
1447
+ <a name="reset"></a>
1448
+
1449
+ ## reset()
1450
+ Reset global variable if one like to parse multiple files
1451
+
1452
+ **Kind**: global function
1412
1453
  <a name="ParserOptions"></a>
1413
1454
 
1414
1455
  ## ParserOptions
@@ -1420,7 +1461,7 @@ Optional settings for the Twig parser
1420
1461
 
1421
1462
  | Name | Type | Description |
1422
1463
  | --- | --- | --- |
1423
- | [method] | <code>&#x27;sax&#x27;</code> \| <code>&#x27;expat&#x27;</code> | The underlying parser. Either `'sax'` or `'expat'`. |
1464
+ | [method] | <code>&#x27;sax&#x27;</code> \| <code>&#x27;expat&#x27;</code> \| <code>&#x27;saxophone&#x27;</code> | The underlying parser. Either `'sax'`, `'expat'` or `'saxophone'`. |
1424
1465
  | [xmlns] | <code>boolean</code> | If `true`, then namespaces are accessible by `namespace` property. |
1425
1466
  | [trim] | <code>boolean</code> | If `true`, then turn any whitespace into a single space. Text and comments are trimmed. |
1426
1467
  | [resumeAfterError] | <code>boolean</code> | If `true` then parser continues reading after an error. Otherwise it throws exception. |
@@ -1494,15 +1535,15 @@ Custom filter function to select desired elements
1494
1535
 
1495
1536
  <a name="Parser"></a>
1496
1537
 
1497
- ## Parser ⇒ [<code>sax</code>](https://www.npmjs.com/package/sax) \| [<code>node-expat</code>](https://www.npmjs.com/package/node-expat)
1538
+ ## Parser ⇒ [<code>sax</code>](https://www.npmjs.com/package/sax) \| [<code>node-expat</code>](https://www.npmjs.com/package/node-expat) \| [<code>saxophone</code>](https://www.npmjs.com/package/saxophone)
1498
1539
  **Kind**: global typedef
1499
- **Returns**: [<code>sax</code>](https://www.npmjs.com/package/sax) \| [<code>node-expat</code>](https://www.npmjs.com/package/node-expat) - The parser Object
1540
+ **Returns**: [<code>sax</code>](https://www.npmjs.com/package/sax) \| [<code>node-expat</code>](https://www.npmjs.com/package/node-expat) \| [<code>saxophone</code>](https://www.npmjs.com/package/saxophone) - The parser Object
1500
1541
  **Properties**
1501
1542
 
1502
1543
  | Name | Type | Description |
1503
1544
  | --- | --- | --- |
1504
- | [currentLine] | <code>number</code> | The currently processed line in the XML-File |
1505
- | [currentColumn] | <code>number</code> | The currently processed column in the XML-File |
1545
+ | [currentLine] | <code>number</code> | The currently processed line in the XML-File.<br/>Not available on `saxophone` parser. |
1546
+ | [currentColumn] | <code>number</code> | The currently processed column in the XML-File.<br/>Not available on `saxophone` parser. |
1506
1547
 
1507
1548
  <a name="AttributeCondition"></a>
1508
1549
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "name": "xml-twig",
7
7
  "description": "Node module for processing huge XML documents in tree mode",
8
- "version": "1.2.94",
8
+ "version": "1.3.0",
9
9
  "main": "twig.js",
10
10
  "directories": {
11
11
  "doc": "doc"
@@ -13,7 +13,8 @@
13
13
  "devDependencies": {
14
14
  "jsdoc-to-markdown": "^8.0.0",
15
15
  "luxon": "^2.1.1",
16
- "node-expat": "^2.4.0"
16
+ "node-expat": "^2.4.0",
17
+ "saxophone": "^0.8.0"
17
18
  },
18
19
  "scripts": {
19
20
  "test": "node demo.js"
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <bookstore>
2
+ <bookstore id="a">
3
3
  <book category="cooking">
4
4
  <title lang="en">Everyday Italian</title>
5
5
  <author>Giada De Laurentiis</author>
@@ -19,8 +19,9 @@
19
19
  <food>
20
20
  <name>Berry-Berry Belgian Waffles</name>
21
21
  <price>$8.95</price>
22
- <description>
23
- Belgian waffles covered with assorted fresh berries and whipped cream
22
+ <description><![CDATA[
23
+ Belgian waffles covered with assorted <bold>fresh</bold> berries and whipped cream
24
+ ]]>
24
25
  </description>
25
26
  <calories>900</calories>
26
27
  </food>
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <person>Jérôme Picard</person>
@@ -26,4 +26,3 @@
26
26
  </html>
27
27
  </xsl:template>
28
28
  </xsl:stylesheet>
29
- <?xml-stylesheet type="text/xsl" href="style.xsl"?>
@@ -0,0 +1,17 @@
1
+ node speed-test.js expat
2
+ node speed-test.js expat
3
+ node speed-test.js expat
4
+ node speed-test.js expat
5
+ node speed-test.js expat
6
+
7
+ node speed-test.js sax
8
+ node speed-test.js sax
9
+ node speed-test.js sax
10
+ node speed-test.js sax
11
+ node speed-test.js sax
12
+
13
+ node speed-test.js saxophone
14
+ node speed-test.js saxophone
15
+ node speed-test.js saxophone
16
+ node speed-test.js saxophone
17
+ node speed-test.js saxophone
@@ -1,26 +1,27 @@
1
1
  const { DateTime } = require('luxon');
2
2
  const fs = require('fs');
3
- const twig = require('xml-twig');
3
+ const twig = require('../twig.js');
4
4
 
5
5
  let NE = 0;
6
6
  const startTime = DateTime.now();
7
- console.log('Starting...')
8
- let parser = twig.createParser([{ tag: 'subsession', function: anyHandler }], { method: 'expat' })
9
- let reader = fs.createReadStream(`${__dirname}/20231019015552.1-MSRAN.xml`);
7
+
8
+ let parser = twig.createParser([{ tag: 'subsession', function: anyHandler }], { method: process.argv[2] })
9
+ let reader = fs.createReadStream(`${__dirname}/20240304015234.9-MSRAN.xml`);
10
+ console.log(`Starting with ${parser.method}...`)
10
11
  reader.pipe(parser);
11
12
 
12
13
  function anyHandler(elt) {
13
14
  NE++;
14
15
  if (NE % 25 === 0) {
15
16
  let d = DateTime.now().diff(startTime);
16
- console.log(`${NE} NE in ${d.toFormat('mm:ss.S')}`);
17
+ console.log(`\t${NE} NE in ${d.toFormat('mm:ss.S')}`);
17
18
  }
18
19
  elt.purge();
19
20
  }
20
21
 
21
22
  reader.on('end', () => {
22
23
  let d = DateTime.now().diff(startTime);
23
- console.log(`All done in ${d.toFormat('mm:ss.S')}`);
24
+ console.log(`All done with ${parser.method} in ${d.toFormat('mm:ss.S')}`);
24
25
  });
25
26
 
26
27
 
@@ -30,38 +31,25 @@ reader.on('end', () => {
30
31
  * Results
31
32
  **********************
32
33
 
33
- Node.js with expat:
34
- 25 NE in 00:16.321
35
- 50 NE in 00:27.867
36
- 75 NE in 00:52.757
37
- 100 NE in 01:11.297
38
- 125 NE in 01:29.996
39
- 150 NE in 01:46.358
40
- 175 NE in 02:02.486
41
- 200 NE in 02:21.25
42
- All done in 02:21.31
34
+ All done with expat in 02:52.676
35
+ All done with expat in 02:53.644
36
+ All done with expat in 02:54.894
37
+ All done with expat in 03:02.545
38
+ All done with expat in 03:23.34
43
39
 
44
- Node.js with sax:
45
- 25 NE in 00:32.988
46
- 50 NE in 00:53.964
47
- 75 NE in 01:38.18
48
- 100 NE in 02:12.977
49
- 125 NE in 02:47.781
50
- 150 NE in 03:17.601
51
- 175 NE in 03:48.676
52
- 200 NE in 04:22.523
53
- All done in 04:22.528
40
+ All done with sax in 04:28.915
41
+ All done with sax in 04:05.760
42
+ All done with sax in 04:06.718
43
+ All done with sax in 04:03.962
44
+ All done with sax in 03:54.540
54
45
 
46
+ All done with saxophone in 02:12.565
47
+ All done with saxophone in 02:10.86
48
+ All done with saxophone in 02:13.447
49
+ All done with saxophone in 02:12.14
50
+ All done with saxophone in 02:08.658
55
51
 
56
52
  Good old Perl XML::Twig
57
- 25 NE in 1:14
58
- 50 NE in 2:03
59
- 75 NE in 3:43
60
- 100 NE in 5:02
61
- 125 NE in 6:12
62
- 150 NE in 7:16
63
- 175 NE in 8:17
64
- 200 NE in 9:24
65
53
  All done in 9:24
66
54
 
67
55
  */
@@ -0,0 +1,164 @@
1
+ C:\Developing\Source\xml-twig\samples>node speed-test.js expat
2
+ Starting with expat...
3
+ 25 NE in 00:15.36
4
+ 50 NE in 00:33.382
5
+ 75 NE in 00:50.253
6
+ 100 NE in 01:13.944
7
+ 125 NE in 01:42.205
8
+ 150 NE in 02:13.746
9
+ 175 NE in 02:35.650
10
+ All done with expat in 02:52.676
11
+
12
+ C:\Developing\Source\xml-twig\samples>node speed-test.js expat
13
+ Starting with expat...
14
+ 25 NE in 00:18.216
15
+ 50 NE in 00:37.92
16
+ 75 NE in 00:54.888
17
+ 100 NE in 01:17.224
18
+ 125 NE in 01:44.272
19
+ 150 NE in 02:15.283
20
+ 175 NE in 02:36.463
21
+ All done with expat in 02:53.644
22
+
23
+ C:\Developing\Source\xml-twig\samples>node speed-test.js expat
24
+ Starting with expat...
25
+ 25 NE in 00:18.639
26
+ 50 NE in 00:39.182
27
+ 75 NE in 00:59.446
28
+ 100 NE in 01:19.566
29
+ 125 NE in 01:46.400
30
+ 150 NE in 02:16.608
31
+ 175 NE in 02:37.242
32
+ All done with expat in 02:54.894
33
+
34
+ C:\Developing\Source\xml-twig\samples>node speed-test.js expat
35
+ Starting with expat...
36
+ 25 NE in 00:17.702
37
+ 50 NE in 00:36.765
38
+ 75 NE in 00:55.178
39
+ 100 NE in 01:16.259
40
+ 125 NE in 01:47.37
41
+ 150 NE in 02:18.862
42
+ 175 NE in 02:42.671
43
+ All done with expat in 03:02.545
44
+
45
+ C:\Developing\Source\xml-twig\samples>node speed-test.js expat
46
+ Starting with expat...
47
+ 25 NE in 00:18.746
48
+ 50 NE in 00:38.461
49
+ 75 NE in 00:56.901
50
+ 100 NE in 01:17.314
51
+ 125 NE in 01:44.938
52
+ 150 NE in 02:26.995
53
+ 175 NE in 02:58.149
54
+ All done with expat in 03:23.34
55
+
56
+ C:\Developing\Source\xml-twig\samples>node speed-test.js sax
57
+ Starting with sax...
58
+ 25 NE in 00:33.781
59
+ 50 NE in 01:08.679
60
+ 75 NE in 01:39.504
61
+ 100 NE in 02:11.68
62
+ 125 NE in 02:51.186
63
+ 150 NE in 03:36.142
64
+ 175 NE in 04:04.731
65
+ All done with sax in 04:28.915
66
+
67
+ C:\Developing\Source\xml-twig\samples>node speed-test.js sax
68
+ Starting with sax...
69
+ 25 NE in 00:25.21
70
+ 50 NE in 00:52.164
71
+ 75 NE in 01:17.550
72
+ 100 NE in 01:51.854
73
+ 125 NE in 02:30.192
74
+ 150 NE in 03:11.461
75
+ 175 NE in 03:39.338
76
+ All done with sax in 04:05.760
77
+
78
+ C:\Developing\Source\xml-twig\samples>node speed-test.js sax
79
+ Starting with sax...
80
+ 25 NE in 00:25.778
81
+ 50 NE in 00:52.603
82
+ 75 NE in 01:19.686
83
+ 100 NE in 01:52.268
84
+ 125 NE in 02:30.279
85
+ 150 NE in 03:13.159
86
+ 175 NE in 03:42.640
87
+ All done with sax in 04:06.718
88
+
89
+ C:\Developing\Source\xml-twig\samples>node speed-test.js sax
90
+ Starting with sax...
91
+ 25 NE in 00:24.549
92
+ 50 NE in 00:50.774
93
+ 75 NE in 01:14.500
94
+ 100 NE in 01:44.116
95
+ 125 NE in 02:22.861
96
+ 150 NE in 03:03.357
97
+ 175 NE in 03:39.237
98
+ All done with sax in 04:03.962
99
+
100
+ C:\Developing\Source\xml-twig\samples>node speed-test.js sax
101
+ Starting with sax...
102
+ 25 NE in 00:25.36
103
+ 50 NE in 00:51.544
104
+ 75 NE in 01:15.580
105
+ 100 NE in 01:45.389
106
+ 125 NE in 02:22.834
107
+ 150 NE in 03:03.487
108
+ 175 NE in 03:31.416
109
+ All done with sax in 03:54.540
110
+
111
+ C:\Developing\Source\xml-twig\samples>node speed-test.js saxophone
112
+ Starting with saxophone...
113
+ 25 NE in 00:14.96
114
+ 50 NE in 00:31.316
115
+ 75 NE in 00:44.478
116
+ 100 NE in 01:00.580
117
+ 125 NE in 01:21.74
118
+ 150 NE in 01:43.881
119
+ 175 NE in 01:59.119
120
+ All done with saxophone in 02:12.565
121
+
122
+ C:\Developing\Source\xml-twig\samples>node speed-test.js saxophone
123
+ Starting with saxophone...
124
+ 25 NE in 00:14.565
125
+ 50 NE in 00:28.947
126
+ 75 NE in 00:42.211
127
+ 100 NE in 00:58.533
128
+ 125 NE in 01:19.194
129
+ 150 NE in 01:41.595
130
+ 175 NE in 01:57.24
131
+ All done with saxophone in 02:10.86
132
+
133
+ C:\Developing\Source\xml-twig\samples>node speed-test.js saxophone
134
+ Starting with saxophone...
135
+ 25 NE in 00:13.307
136
+ 50 NE in 00:27.849
137
+ 75 NE in 00:41.466
138
+ 100 NE in 00:59.190
139
+ 125 NE in 01:22.230
140
+ 150 NE in 01:44.605
141
+ 175 NE in 02:00.299
142
+ All done with saxophone in 02:13.447
143
+
144
+ C:\Developing\Source\xml-twig\samples>node speed-test.js saxophone
145
+ Starting with saxophone...
146
+ 25 NE in 00:13.884
147
+ 50 NE in 00:28.340
148
+ 75 NE in 00:41.720
149
+ 100 NE in 00:57.897
150
+ 125 NE in 01:19.260
151
+ 150 NE in 01:42.974
152
+ 175 NE in 01:58.791
153
+ All done with saxophone in 02:12.14
154
+
155
+ C:\Developing\Source\xml-twig\samples>node speed-test.js saxophone
156
+ Starting with saxophone...
157
+ 25 NE in 00:13.443
158
+ 50 NE in 00:28.99
159
+ 75 NE in 00:40.821
160
+ 100 NE in 00:56.989
161
+ 125 NE in 01:16.787
162
+ 150 NE in 01:40.205
163
+ 175 NE in 01:55.451
164
+ All done with saxophone in 02:08.658
package/samples/xmlns.xml CHANGED
@@ -10,9 +10,9 @@
10
10
  <f:width>80</f:width>
11
11
  <f:length>120</f:length>
12
12
  </f:table>
13
- <book category="web">
13
+ <book xmlns:a="http://www.w3.org/" category="web">
14
14
  <title lang="en">Learning XML</title>
15
- <author>Erik T. Ray</author>
15
+ <a:author>Erik T. Ray</a:author>
16
16
  <year>2003</year>
17
17
  <price>39.95</price>
18
18
  </book>
package/test.js ADDED
@@ -0,0 +1,43 @@
1
+ const fs = require('fs');
2
+ const twig = require('./twig.js');
3
+
4
+
5
+ //const parser = twig.createParser({ tag: twig.Any, function: anyHandler }, { method: "sax" });
6
+ //const parser = twig.createParser({ tag: twig.Any, function: anyHandler }, { method: "expat" });
7
+ //const parser = twig.createParser({ tag: twig.Any, function: anyHandler }, { method: "saxophone" });
8
+ //fs.createReadStream(`${__dirname}/samples/bookstore.xml`).pipe(parser);
9
+ //fs.createReadStream(`${__dirname}/samples/breakfast-menu.xml`).pipe(parser);
10
+
11
+ //const parser = twig.createParser({ tag: twig.Any, function: nsHandler }, { method: "sax", xmlns: true });
12
+ //const parser = twig.createParser({ tag: twig.Any, function: nsHandler }, { method: "expat", xmlns: true });
13
+ //const parser = twig.createParser({ tag: twig.Any, function: nsHandler }, { method: "saxophone", xmlns: true });
14
+ //fs.createReadStream(`${__dirname}/samples/xmlns.xml`).pipe(parser);
15
+
16
+ //const parser = twig.createParser({ tag: twig.Root, function: piHandler }, { method: "sax" });
17
+ //const parser = twig.createParser({ tag: twig.Root, function: piHandler }, { method: "expat" });
18
+ //const parser = twig.createParser({ tag: twig.Root, function: piHandler }, { method: "saxophone" });
19
+ //fs.createReadStream(`${__dirname}/samples/processingInstruction.xml`).pipe(parser);
20
+
21
+
22
+ //fs.createReadStream(`${__dirname}/samples/encoding-UTF-8.xml`).pipe(parser);
23
+ //fs.createReadStream(`${__dirname}/samples/encoding-UTF-16LE.xml`).pipe(parser);
24
+ //fs.createReadStream(`${__dirname}/samples/encoding-UTF-16BE.xml`).pipe(parser);
25
+
26
+
27
+ function anyHandler(elt) {
28
+ console.log(`<${elt.name}> => ${elt.text} -> ${JSON.stringify(elt.attributes)}`);
29
+ }
30
+
31
+
32
+ function nsHandler(elt) {
33
+ console.log(`${elt.name} => ${JSON.stringify(elt.namespace)} -> ${JSON.stringify(elt.attributes)}`);
34
+ //console.log(`${elt.name} => isRoot = ${elt.isRoot}`);
35
+ //console.log(`${elt.name} -> ${JSON.stringify(elt.PI)}`);
36
+ //console.log(`${elt.name}`);
37
+ }
38
+
39
+ function piHandler(elt) {
40
+ console.log(`${elt.name} -> ${JSON.stringify(elt.PI)}`);
41
+ }
42
+
43
+
package/twig.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const SAX = 'sax';
2
2
  const EXPAT = 'expat';
3
+ const SAXOPHONE = 'saxophone';
3
4
 
4
5
  let tree;
5
6
  let current;
@@ -19,6 +20,25 @@ let current;
19
20
  * @see {@link https://www.npmjs.com/package/node-expat|node-expat}
20
21
  */
21
22
 
23
+ /**
24
+ * @external saxophone
25
+ * @see {@link https://www.npmjs.com/package/saxophone|saxophone}
26
+ * @see {@link https://www.npmjs.com/package/@alexbosworth/saxophone|@alexbosworth/saxophone}
27
+ * @see {@link https://www.npmjs.com/package/@pirxpilot/saxophone|@pirxpilot/saxophone}
28
+ */
29
+
30
+ /**
31
+ * @external libxmljs
32
+ * @todo Not yet implemented, maybe later. Not sure if `WritableStream` is supported
33
+ * @see {@link https://www.npmjs.com/package/libxmljs|libxmljs}
34
+ */
35
+
36
+ /**
37
+ * @external sax-wasm
38
+ * @todo Not yet implemented, maybe later. Not sure if `WritableStream` is supported
39
+ * @see {@link https://www.npmjs.com/package/sax-wasm|sax-wasm}
40
+ */
41
+
22
42
 
23
43
  class RootHandler { }
24
44
  class AnyHandler { }
@@ -40,7 +60,7 @@ const Any = new AnyHandler();
40
60
  /**
41
61
  * Optional settings for the Twig parser
42
62
  * @typedef ParserOptions
43
- * @property {'sax' | 'expat'} [method] - The underlying parser. Either `'sax'` or `'expat'`.
63
+ * @property {'sax' | 'expat' | 'saxophone'} [method] - The underlying parser. Either `'sax'`, `'expat'` or `'saxophone'`.
44
64
  * @property {boolean} [xmlns] - If `true`, then namespaces are accessible by `namespace` property.
45
65
  * @property {boolean} [trim] - If `true`, then turn any whitespace into a single space. Text and comments are trimmed.
46
66
  * @property {boolean} [resumeAfterError] - If `true` then parser continues reading after an error. Otherwise it throws exception.
@@ -103,11 +123,12 @@ const Any = new AnyHandler();
103
123
 
104
124
  /**
105
125
  * @typedef Parser
106
- * @property {number} [currentLine] - The currently processed line in the XML-File
107
- * @property {number} [currentColumn] - The currently processed column in the XML-File
108
- * @returns {external:sax|external:node-expat} The parser Object
126
+ * @property {number} [currentLine] - The currently processed line in the XML-File.<br/>Not available on `saxophone` parser.
127
+ * @property {number} [currentColumn] - The currently processed column in the XML-File.<br/>Not available on `saxophone` parser.
128
+ * @returns {external:sax|external:node-expat|external:saxophone} The parser Object
109
129
  */
110
130
 
131
+
111
132
  /**
112
133
  * Create a new Twig parser
113
134
  * @param {TwigHandler|TwigHandler[]} handler - Object or array of element specification and function to handle elements
@@ -119,7 +140,6 @@ function createParser(handler, options = {}) {
119
140
  options = Object.assign({ method: SAX, xmlns: false, trim: true, resumeAfterError: false, partial: false }, options);
120
141
  let parser;
121
142
  let namespaces = {};
122
- let closeEvent;
123
143
 
124
144
  if (options.partial) {
125
145
  const handle1 = Array.isArray(handler) ? handler : [handler];
@@ -142,44 +162,12 @@ function createParser(handler, options = {}) {
142
162
  get() { return parser._parser.column + 1; }
143
163
  });
144
164
 
145
- closeEvent = "closetag";
146
- parser.on("opentagstart", function (node) {
147
- if (tree === undefined) {
148
- tree = new Twig(node.name, current);
149
- } else {
150
- if (current.isRoot && current.name === undefined) {
151
- current.setRoot(node.name);
152
- } else {
153
- let elt = new Twig(node.name, current);
154
- if (options.partial) {
155
- for (let hndl of Array.isArray(handler) ? handler : [handler]) {
156
- if (typeof hndl.tag === 'string' && node.name === hndl.tag) {
157
- elt.pin();
158
- break;
159
- } else if (hndl.tag instanceof RegExp && hndl.tag.test(node.name)) {
160
- elt.pin();
161
- break;
162
- } else if (typeof hndl.tag === 'function' && hndl.tag(node.name, current ?? tree)) {
163
- elt.pin();
164
- break;
165
- }
166
- }
167
- }
168
- }
169
- }
170
- if (options.xmlns) {
171
- if (node.name.includes(':')) {
172
- let prefix = node.name.split(':')[0];
173
- if (namespaces[prefix] !== undefined) {
174
- Object.defineProperty(current, 'namespace', {
175
- value: { local: prefix, uri: namespaces[prefix] },
176
- writable: false,
177
- enumerable: true
178
- });
179
- }
180
- }
181
- }
182
- });
165
+ parser.on("closetag", onClose.bind(null, handler, options));
166
+ parser.on("opentagstart", onStart.bind(null, {
167
+ handler: Array.isArray(handler) ? handler : [handler],
168
+ options: options,
169
+ namespaces: namespaces
170
+ }));
183
171
 
184
172
  parser.on("processinginstruction", function (pi) {
185
173
  if (pi.name === 'xml') {
@@ -195,7 +183,7 @@ function createParser(handler, options = {}) {
195
183
  writable: false,
196
184
  enumerable: true
197
185
  });
198
- } else {
186
+ } else if (tree.PI === undefined) {
199
187
  Object.defineProperty(tree, 'PI', {
200
188
  value: { target: pi.name, data: pi.body },
201
189
  writable: false,
@@ -207,17 +195,21 @@ function createParser(handler, options = {}) {
207
195
  parser.on("attribute", function (attr) {
208
196
  if (options.xmlns && (attr.uri ?? '') !== '' && attr.local !== undefined) {
209
197
  namespaces[attr.local] = attr.uri;
210
- Object.defineProperty(current, 'namespace', {
211
- value: { local: attr.local, uri: attr.uri },
212
- writable: false,
213
- enumerable: true
214
- });
198
+ if (current.name.includes(':')) {
199
+ Object.defineProperty(current, 'namespace', {
200
+ value: { local: attr.local, uri: attr.uri },
201
+ writable: false,
202
+ enumerable: true
203
+ });
204
+ } else {
205
+ current.attribute(attr.name, attr.value);
206
+ }
215
207
  } else {
216
208
  current.attribute(attr.name, attr.value);
217
209
  }
218
210
  });
219
211
  parser.on("cdata", function (str) {
220
- current.text = current.text ?? '' + str;
212
+ current.text = options.trim ? str.trim() : str;
221
213
  });
222
214
 
223
215
  const hndl = Array.isArray(handler) ? handler : [handler];
@@ -237,52 +229,13 @@ function createParser(handler, options = {}) {
237
229
  enumerable: true,
238
230
  get() { return parser.parser.getCurrentColumnNumber(); }
239
231
  });
240
- closeEvent = "endElement";
241
232
 
242
- parser.on("startElement", function (name, attrs) {
243
- let attr = {};
244
- if (options.xmlns) {
245
- for (let key of Object.keys(attrs).filter(x => !x.startsWith('xmlns:')))
246
- attr[key] = attrs[key];
247
- }
248
- if (tree === undefined) {
249
- tree = new Twig(name, current, options.xmlns ? attr : attrs);
250
- } else {
251
- if (current.isRoot && current.name === undefined) {
252
- current.setRoot(name);
253
- } else {
254
- let elt = new Twig(name, current, options.xmlns ? attr : attrs);
255
- if (options.partial) {
256
- for (let hndl of Array.isArray(handler) ? handler : [handler]) {
257
- if (typeof hndl.tag === 'string' && name === hndl.tag) {
258
- elt.pin();
259
- break;
260
- } else if (hndl.tag instanceof RegExp && hndl.tag.test(name)) {
261
- elt.pin();
262
- break;
263
- } else if (typeof hndl.tag === 'function' && hndl.tag(name, current ?? tree)) {
264
- elt.pin();
265
- break;
266
- }
267
- }
268
- }
269
- }
270
- }
271
- if (options.xmlns) {
272
- for (let key of Object.keys(attrs).filter(x => x.startsWith('xmlns:')))
273
- namespaces[key.split(':')[1]] = attrs[key];
274
- if (name.includes(':')) {
275
- let prefix = name.split(':')[0];
276
- if (namespaces[prefix] !== undefined) {
277
- Object.defineProperty(current, 'namespace', {
278
- value: { local: prefix, uri: namespaces[prefix] },
279
- writable: false,
280
- enumerable: true
281
- });
282
- }
283
- }
284
- }
285
- });
233
+ parser.on("endElement", onClose.bind(null, handler, options));
234
+ parser.on("startElement", onStart.bind(null, {
235
+ handler: Array.isArray(handler) ? handler : [handler],
236
+ options: options,
237
+ namespaces: namespaces
238
+ }));
286
239
 
287
240
  parser.on('xmlDecl', function (version, encoding, standalone) {
288
241
  tree = new Twig(null);
@@ -300,50 +253,82 @@ function createParser(handler, options = {}) {
300
253
  parser.on('processingInstruction', function (target, data) {
301
254
  tree.PI = { target: target, data: data };
302
255
  });
303
- } else {
304
- throw new UnsupportedParser(options.method);
305
- }
306
256
 
307
- parser.on(closeEvent, function (name) {
308
- current.close();
309
- let purge = true;
310
-
311
- for (let hndl of Array.isArray(handler) ? handler : [handler]) {
312
- if (hndl.tag instanceof AnyHandler) {
313
- if (typeof hndl.function === 'function') hndl.function(current ?? tree);
314
- if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
315
- purge = false;
316
- } else if (hndl.tag instanceof RootHandler && options.method === EXPAT && current.isRoot) {
317
- if (typeof hndl.function === 'function') hndl.function(tree);
318
- if (typeof hndl.event === 'string') parser.emit(hndl.event, tree);
319
- purge = false;
320
- } else if (typeof hndl.tag === 'string' && name === hndl.tag) {
321
- if (typeof hndl.function === 'function') hndl.function(current ?? tree);
322
- if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
323
- purge = false;
324
- } else if (hndl.tag instanceof RegExp && hndl.tag.test(name)) {
325
- if (typeof hndl.function === 'function') hndl.function(current ?? tree);
326
- if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
327
- purge = false;
328
- } else if (typeof hndl.tag === 'function' && hndl.tag(name, current ?? tree)) {
329
- if (typeof hndl.function === 'function') hndl.function(current ?? tree);
330
- if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
331
- purge = false;
257
+ } else if (options.method === SAXOPHONE) {
258
+ const Saxophone = require('saxophone');
259
+ //const Saxophone = require('@alexbosworth/saxophone');
260
+ //const Saxophone = require('@pirxpilot/saxophone');
261
+ parser = new Saxophone();
262
+
263
+ parser.on("tagclose", onClose.bind(null, handler, options));
264
+ parser.on("tagopen", onStart.bind(null, {
265
+ handler: Array.isArray(handler) ? handler : [handler],
266
+ options: options,
267
+ namespaces: namespaces,
268
+ parser: parser,
269
+ Saxophone: Saxophone
270
+ }));
271
+
272
+ parser.on("cdata", function (str) {
273
+ current.text = options.trim ? str.contents.trim() : str.contents;
274
+ });
275
+
276
+ parser.on('processinginstruction', function (pi) {
277
+ if (pi.contents.startsWith('xml ')) {
278
+ let declaration = {};
279
+ for (let item of pi.contents.split(' ')) {
280
+ let [k, v] = item.split('=');
281
+ if (k === 'xml') continue;
282
+ declaration[k] = v.replaceAll('"', '').replaceAll("'", '');
283
+ }
284
+ tree = new Twig(null);
285
+ Object.defineProperty(tree, 'declaration', {
286
+ value: declaration,
287
+ writable: false,
288
+ enumerable: true
289
+ });
290
+ } else if (tree.PI === undefined) {
291
+ let instruction = { body: {} };
292
+ for (let item of pi.contents.split(' ')) {
293
+ let [k, v] = item.split('=');
294
+ if (v === undefined) {
295
+ instruction.name = k;
296
+ } else {
297
+ instruction.body[k] = v.replaceAll('"', '').replaceAll("'", '');
298
+ }
299
+ }
300
+ Object.defineProperty(tree, 'PI', {
301
+ value: { target: instruction.name, data: instruction.body },
302
+ writable: false,
303
+ enumerable: true
304
+ });
332
305
  }
333
- }
334
306
 
335
- if (options.partial && purge && !current.pinned && !current.isRoot)
336
- current.purge();
337
- current = current.parent();
307
+ });
308
+
309
+ } else {
310
+ throw new UnsupportedParser(options.method);
311
+ }
338
312
 
313
+ Object.defineProperty(parser, 'method', {
314
+ value: options.method,
315
+ writable: false,
316
+ enumerable: true
339
317
  });
340
318
 
341
319
  // Common events
342
320
  parser.on('text', function (str) {
343
- current.text = current.text ?? '' + options.trim ? str.trim() : str;
321
+ if (current === undefined || current === null) return;
322
+ if (options.method === SAXOPHONE) {
323
+ current.text = options.trim ? str.contents.trim() : str.contents;
324
+ } else {
325
+ current.text = options.trim ? str.trim() : str;
326
+ }
344
327
  });
345
328
 
346
329
  parser.on("comment", function (str) {
330
+ if (options.method === SAXOPHONE)
331
+ str = str.contents;
347
332
  if (current.hasOwnProperty('comment')) {
348
333
  if (typeof current.comment === 'string') {
349
334
  current.comment = [current.comment, str.trim()];
@@ -358,20 +343,147 @@ function createParser(handler, options = {}) {
358
343
  configurable: true
359
344
  });
360
345
  }
361
-
362
346
  });
363
347
 
364
348
  parser.on('error', function (err) {
365
- console.error(`error at line [${parser.currentLine}], column [${parser.currentColumn}]`, err);
366
- if (options.resumeAfterError) {
367
- parser.underlyingParser.error = null;
368
- parser.underlyingParser.resume();
349
+ if (options.method === SAXOPHONE) {
350
+ console.error(err);
351
+ } else {
352
+ console.error(`error at line [${parser.currentLine}], column [${parser.currentColumn}]`, err);
353
+ if (options.resumeAfterError) {
354
+ parser.underlyingParser.error = null;
355
+ parser.underlyingParser.resume();
356
+ }
369
357
  }
370
358
  });
371
359
 
372
360
  return parser;
373
361
  }
374
362
 
363
+
364
+ /**
365
+ * Common Event hanlder for starting tag
366
+ * @param {object} binds - Additional parameter object
367
+ * @param {object|string} node - Node or Node name
368
+ * @param {object} attrs - Node Attributes
369
+ */
370
+ function onStart(binds, node, attrs) {
371
+
372
+ const name = typeof node === 'string' ? node : node.name;
373
+ const handler = binds.handler;
374
+ const options = binds.options;
375
+ let namespaces = binds.namespaces;
376
+
377
+ if (attrs === undefined && options.method === SAXOPHONE)
378
+ attrs = binds.Saxophone.parseAttrs(node.attrs);
379
+
380
+ let attrNS = {};
381
+ if (options.xmlns && attrs !== undefined) {
382
+ for (let key of Object.keys(attrs).filter(x => !(x.startsWith('xmlns:') && name.includes(':'))))
383
+ attrNS[key] = attrs[key];
384
+ }
385
+
386
+ if (tree === undefined) {
387
+ tree = new Twig(name, current, options.xmlns ? attrNS : attrs);
388
+ } else {
389
+ if (current.isRoot && current.name === undefined) {
390
+ current.setRoot(name);
391
+ if (attrs !== undefined) {
392
+ const att = options.xmlns ? attrNS : attrs;
393
+ for (let key of Object.keys(att))
394
+ current.attribute(key, att[key]);
395
+ }
396
+ } else {
397
+ let elt = new Twig(name, current, options.xmlns ? attrNS : attrs);
398
+ if (options.partial) {
399
+ for (let hndl of handler) {
400
+ if (typeof hndl.tag === 'string' && name === hndl.tag) {
401
+ elt.pin();
402
+ break;
403
+ } else if (hndl.tag instanceof RegExp && hndl.tag.test(name)) {
404
+ elt.pin();
405
+ break;
406
+ } else if (typeof hndl.tag === 'function' && hndl.tag(name, current ?? tree)) {
407
+ elt.pin();
408
+ break;
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ if (options.xmlns) {
416
+ if ([EXPAT, SAXOPHONE].includes(options.method)) {
417
+ for (let key of Object.keys(attrs).filter(x => x.startsWith('xmlns:')))
418
+ namespaces[key.split(':')[1]] = attrs[key];
419
+ }
420
+ if (name.includes(':')) {
421
+ let prefix = name.split(':')[0];
422
+ if (namespaces[prefix] !== undefined) {
423
+ Object.defineProperty(current, 'namespace', {
424
+ value: { local: prefix, uri: namespaces[prefix] },
425
+ writable: false,
426
+ enumerable: true
427
+ });
428
+ }
429
+ }
430
+ }
431
+ if (options.method === SAXOPHONE && node.isSelfClosing)
432
+ binds.parser.emit("tagclose", node);
433
+ }
434
+
435
+ /**
436
+ * Common Event hanlder for closing tag
437
+ * @param {TwigHandler|TwigHandler[]} handler - Object or array of element specification and function to handle elements
438
+ * @param {ParserOptions} options - Object of optional options
439
+ * @param {string} name - Event handler parameter
440
+ */
441
+ function onClose(handler, options, name) {
442
+ current.close();
443
+ let purge = true;
444
+
445
+ if (options.method === SAXOPHONE)
446
+ name = name.name;
447
+ for (let hndl of Array.isArray(handler) ? handler : [handler]) {
448
+ if (hndl.tag instanceof AnyHandler) {
449
+ if (typeof hndl.function === 'function') hndl.function(current ?? tree);
450
+ if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
451
+ purge = false;
452
+ } else if (hndl.tag instanceof RootHandler && [EXPAT, SAXOPHONE].includes(options.method) && current.isRoot) {
453
+ if (typeof hndl.function === 'function') hndl.function(tree);
454
+ if (typeof hndl.event === 'string') parser.emit(hndl.event, tree);
455
+ purge = false;
456
+ } else if (typeof hndl.tag === 'string' && name === hndl.tag) {
457
+ if (typeof hndl.function === 'function') hndl.function(current ?? tree);
458
+ if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
459
+ purge = false;
460
+ } else if (hndl.tag instanceof RegExp && hndl.tag.test(name)) {
461
+ if (typeof hndl.function === 'function') hndl.function(current ?? tree);
462
+ if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
463
+ purge = false;
464
+ } else if (typeof hndl.tag === 'function' && hndl.tag(name, current ?? tree)) {
465
+ if (typeof hndl.function === 'function') hndl.function(current ?? tree);
466
+ if (typeof hndl.event === 'string') parser.emit(hndl.event, current ?? tree);
467
+ purge = false;
468
+ }
469
+ }
470
+
471
+ if (options.partial && purge && !current.pinned && !current.isRoot)
472
+ current.purge();
473
+ current = current.parent();
474
+
475
+ }
476
+
477
+
478
+ /**
479
+ * Reset global variable if one like to parse multiple files
480
+ */
481
+ function reset() {
482
+ tree = undefined;
483
+ current = undefined;
484
+ }
485
+
486
+
375
487
  /**
376
488
  * Generic class modeling a XML Node
377
489
  * @class Twig
@@ -617,10 +729,11 @@ class Twig {
617
729
  * @throws {UnsupportedType} - If value is not a string, boolean or numeric type
618
730
  */
619
731
  set text(value) {
732
+ if (this.#text === null) this.#text = '';
620
733
  if (typeof value === 'string')
621
- this.#text = value;
734
+ this.#text += value;
622
735
  else if (['number', 'bigint', 'boolean'].includes(typeof value))
623
- this.#text = value.toString();
736
+ this.#text += value.toString();
624
737
  else
625
738
  throw new UnsupportedType(value);
626
739
  }
@@ -1157,7 +1270,7 @@ class UnsupportedParser extends TypeError {
1157
1270
  * @param {string} t Parser type
1158
1271
  */
1159
1272
  constructor(t) {
1160
- super(`Parser '${t}' is not supported. Use 'expat' or 'sax' (default)`);
1273
+ super(`Parser '${t}' is not supported. Use 'expat', 'sax' (default) or 'saxophone'`);
1161
1274
  }
1162
1275
  }
1163
1276
 
@@ -1189,4 +1302,4 @@ class UnsupportedCondition extends TypeError {
1189
1302
  }
1190
1303
 
1191
1304
 
1192
- module.exports = { createParser, Twig, Any, Root };
1305
+ module.exports = { createParser, Twig, Any, Root, reset };