xml-event-parser 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2026 Xihua Duan and contributors
4
+
5
+ Includes work contributed to osm2geojson-lite by Harel Mazor.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Temporary Holding Version
1
+ xml-event-parser
2
+ ================
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ A tiny XML parser with a document tree and path-based events, produced in a
5
+ single pass.
6
+
7
+ - **Zero dependencies**, no DOM, no `node:*` imports, no `eval`.
8
+ - **One pass, two views**: every element is available as a tree node and as an
9
+ `open`/`close` event keyed by its dotted path, so a consumer can convert while
10
+ parsing without a second traversal.
11
+ - **Streaming**: feed `write(chunk)` from a file or socket and call `end()`.
12
+ Chunks may split anywhere - mid-tag, mid-attribute, inside CDATA or a comment -
13
+ and `retain` decides whether the parser keeps the tree, one root, or nothing,
14
+ so a 60 MiB document can be parsed with ~0 MiB of retained heap.
15
+ - **Small**: 6.1 KiB minified, 2.1 KiB gzipped (ESM), streaming included.
16
+ - **Fastest on every shape we benchmarked**: ~1.9x `txml`'s throughput on
17
+ OSM-style documents at ~30% less retained heap, and ahead of `txml` on
18
+ text/entity-heavy input too; ~1.6x `htmlparser2`'s on element callbacks. See
19
+ [Benchmarks](#benchmarks).
20
+
21
+ ```js
22
+ import { XmlParser } from 'xml-event-parser';
23
+
24
+ const parser = new XmlParser({ progressive: true });
25
+
26
+ parser.on('</osm.way>', (way) => {
27
+ // way.tagName === 'way', way.id, way.version, way.children ...
28
+ console.log(way.id, way.children?.length ?? 0);
29
+ });
30
+
31
+ const roots = parser.parse(xml);
32
+ ```
33
+
34
+ ## Node shape
35
+
36
+ ```ts
37
+ interface XmlNode {
38
+ tagName: string; // element name, e.g. 'way'
39
+ children?: XmlNode[]; // present only when the element has child elements
40
+ text?: string; // present only for text-only elements
41
+ [attribute: string]: any; // attributes, copied flat onto the node
42
+ }
43
+ ```
44
+
45
+ Attributes are flat rather than nested in an `attributes` object on purpose: one
46
+ object per element keeps a parsed document small (see the heap column in
47
+ [`bench/RESULTS.md`](bench/RESULTS.md)). Names that collide with `tagName`,
48
+ `children` or `text` are shadowed by the element's own values; if that matters
49
+ for your vocabulary, read attributes off the source XML instead.
50
+
51
+ An element with nested tags is emitted on close **after** its children are
52
+ attached, so `node.children` is complete inside a close listener.
53
+
54
+ ## Streaming
55
+
56
+ ```js
57
+ import { createReadStream } from 'node:fs';
58
+ import { XmlParser } from 'xml-event-parser';
59
+
60
+ const parser = new XmlParser({ progressive: true, retain: 'none' });
61
+ parser.on('</osm.way>', (way) => {
62
+ // one complete element; `way` is released after this returns
63
+ });
64
+
65
+ for await (const chunk of createReadStream('planet.osm.xml', 'utf8')) {
66
+ parser.write(chunk);
67
+ }
68
+ parser.end();
69
+ ```
70
+
71
+ `write()` consumes everything complete in the buffer and keeps the rest for the
72
+ next call, so a chunk boundary can fall inside a tag, an attribute value, a text
73
+ run, a comment or a `]]>` terminator without changing the result: parsing a
74
+ document whole or in 1-byte chunks produces the same tree (the suite checks every
75
+ split point).
76
+
77
+ `retain` is what makes streaming bounded:
78
+
79
+ | `retain` | keeps | `end()` returns | memory |
80
+ | --- | --- | --- | --- |
81
+ | `'all'` (default) | the document | the roots | grows with the document |
82
+ | `'root'` | the root being built | `[]`; a finished root is emitted and dropped | largest root |
83
+ | `'none'` | nothing | `[]`; every element is emitted and dropped | nesting depth |
84
+
85
+ Measured on a 60 MiB document (ten concatenated extracts, 327k elements):
86
+ `retain: 'all'` holds 302 MiB, `retain: 'none'` holds ~0 MiB, both emit the same
87
+ 327k elements. Without listeners, `retain: 'none'` still parses - it just throws
88
+ the result away.
89
+
90
+ ## Events
91
+
92
+ | form | fires |
93
+ | --- | --- |
94
+ | `'<osm.way>'` | when a `<way>` inside `<osm>` opens |
95
+ | `'</osm.way>'` | when it closes, with `children` populated |
96
+
97
+ The path is the dotted chain of ancestors. Event names are only built for paths
98
+ that have a listener, so subscribing to one path costs nothing for the elements
99
+ you ignore. Filter inside the listener:
100
+
101
+ ```js
102
+ parser.on('</osm.way>', (way) => {
103
+ if (!way.tags) return; // or any other predicate
104
+ });
105
+ ```
106
+
107
+ ## Options
108
+
109
+ | option | default | meaning |
110
+ | --- | --- | --- |
111
+ | `progressive` | `false` | emit events. Without it, `parse()` only builds the tree |
112
+ | `queryParent` | `false` | track parents so `parser.getParent(node)` works (costs a `WeakMap`) |
113
+ | `retain` | `'all'` | what survives an element: `'all'`, `'root'` or `'none'` (see [Streaming](#streaming)) |
114
+
115
+ `parse(xml, parent?, dir?)` also accepts an existing node to attach the parsed
116
+ roots to, and a dotted path prefix used when building event names.
117
+
118
+ The parser is non-validating and never throws on malformed input: unterminated
119
+ tags, comments, processing instructions and doctypes are skipped, and elements
120
+ still open at end of input are closed so a truncated download yields the
121
+ document parsed so far (`'<r><a>text'` returns `r` with an `a` child whose
122
+ `text` is `'text'`).
123
+
124
+ CDATA sections are content, not markup: their text is kept verbatim and is not
125
+ entity-decoded, while text outside them is decoded (the five predefined
126
+ entities and numeric character references).
127
+
128
+ ## Benchmarks
129
+
130
+ Six generated shapes, each isolating one cost (per-element, per-child, text,
131
+ nesting). Methodology, per-shape tables and caveats live in
132
+ [`bench/RESULTS.md`](bench/RESULTS.md); rerun with `npm run bench:data && npm run bench`.
133
+
134
+ Realistic Overpass-style document, 6.0 MiB, Apple M4 Pro / Node 26:
135
+
136
+ | workload | xml-event-parser | txml | fast-xml-parser | sax | htmlparser2 |
137
+ | --- | --- | --- | --- | --- | --- |
138
+ | tree (parse + walk) | **26.0 ms, 230 MB/s** | 50.6 ms, 118 MB/s | 167.8 ms, 36 MB/s | — | — |
139
+ | tree, retained heap | **30.0 MiB** | 42.5 MiB | 35.2 MiB | — | — |
140
+ | element callbacks | **27.8 ms, 215 MB/s** | — | — | 100.0 ms, 60 MB/s | 44.9 ms, 133 MB/s |
141
+ | streamed, 64 KiB chunks | **27.3 ms, 219 MB/s** | 77.4 ms, 77 MB/s | — | 103.3 ms, 58 MB/s | 50.8 ms, 118 MB/s |
142
+
143
+ Notes on reading the table:
144
+
145
+ - The callback and streamed rows for `xml-event-parser` use `retain: 'none'`, so
146
+ they do not build a document either; the tree row is the only one that holds
147
+ memory. `txml`'s streaming API is a Node stream (`transformStream`), measured
148
+ separately and asynchronously.
149
+ - Text-heavy documents with many entities and CDATA used to be the weak shape;
150
+ replacing the regex decoder with a manual scan took it from 325 to 442 MB/s,
151
+ ahead of `txml` (382 MB/s).
152
+
153
+ ## Status
154
+
155
+ Extracted from [osm2geojson-lite](https://github.com/tibetty/osm2geojson-lite),
156
+ where this parser served until 2.2.0 and was then replaced by a purpose-built
157
+ OSM scanner. The code and its original unit tests are unchanged apart from the
158
+ public naming (`children`/`tagName`/`text` instead of `$innerNodes`/`$tag`/
159
+ `$innerText`) and the removal of the `new Function`-based conditioned-path
160
+ events.
161
+
162
+ Not published yet. The release workflow publishes only when the repository
163
+ variable `NPM_TRUSTED_PUBLISHER` is `true`; before that, a release is published
164
+ locally because npm cannot configure a trusted publisher for a package that does
165
+ not exist yet.
166
+
167
+ ## Development
168
+
169
+ ```sh
170
+ npm ci
171
+ npm test # unit tests
172
+ npm run typecheck
173
+ npm run build # dist/index.mjs, dist/index.cjs, dist/index.d.ts
174
+ npm run bench:data # generate ~27 MiB benchmark corpus (gitignored)
175
+ npm run bench # compare against txml / sax / htmlparser2 / fast-xml-parser
176
+ ```
177
+
178
+ ## Releasing
179
+
180
+ `release.yml` publishes with npm trusted publishing (OIDC) and is gated on the
181
+ repository variable `NPM_TRUSTED_PUBLISHER`, which stays unset until a trusted
182
+ publisher exists.
183
+
184
+ One-time bootstrap, because npm cannot attach a trusted publisher to a package
185
+ that does not exist yet:
186
+
187
+ 1. Publish the first version locally:
188
+
189
+ ```sh
190
+ npm login
191
+ npm publish --access public
192
+ ```
193
+
194
+ 2. npmjs.com -> Packages -> `xml-event-parser` -> Settings -> **Trusted
195
+ publishing** -> GitHub Actions:
196
+
197
+ | field | value |
198
+ | --- | --- |
199
+ | Organization or user | `tibetty` |
200
+ | Repository | `xml-event-parser` |
201
+ | Workflow filename | `release.yml` (filename only, case-sensitive) |
202
+ | Environment name | leave empty |
203
+ | Allowed actions | tick **npm publish** |
204
+
205
+ Configurations created after September 2026 default to `npm stage publish`
206
+ only, so `npm publish` has to be ticked explicitly. The first publish through
207
+ that workflow must complete within **two days**, or the configuration expires.
208
+
209
+ 3. GitHub -> Settings -> Secrets and variables -> Actions -> Variables: add
210
+ `NPM_TRUSTED_PUBLISHER` = `true`.
211
+
212
+ 4. Optional hardening, once a workflow publish has succeeded: npmjs.com ->
213
+ package Settings -> Publishing access -> *Require two-factor authentication
214
+ and disallow tokens*.
215
+
216
+ After that, changing `version` in `package.json` on `main` publishes through
217
+ `release.yml`, tags `v<version>` and creates the GitHub release. npm generates
218
+ provenance automatically only for a public repository; from a private one the
219
+ package is published without an attestation.
220
+
221
+ ## License
222
+
223
+ MIT. Includes work contributed to `osm2geojson-lite` by Harel Mazor.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){switch(e){case`amp`:return`&`;case`lt`:return`<`;case`gt`:return`>`;case`quot`:return`"`;case`apos`:return`'`}if(e.charCodeAt(0)===35){let t=e.charCodeAt(1)===120||e.charCodeAt(1)===88?parseInt(e.slice(2),16):parseInt(e.slice(1),10);if(Number.isFinite(t)&&t>=0&&t<=1114111)return String.fromCodePoint(t)}return null}function t(t){let n=t.indexOf(`&`);if(n<0)return t;let r=t.slice(0,n);for(;n<t.length;){let i=t.indexOf(`;`,n+1);if(i<0){r+=t.slice(n);break}let a=e(t.slice(n+1,i));if(r+=a===null?t.slice(n,i+1):a,n=t.indexOf(`&`,i+1),n<0){r+=t.slice(i+1);break}r+=t.slice(i+1,n)}return r}function n(e){if(e.indexOf(`<![CDATA[`)<0)return e.indexOf(`&`)<0?e:t(e);let n=``,r=0;for(;r<e.length;){let i=e.indexOf(`<![CDATA[`,r);if(i<0){n+=t(e.slice(r));break}i>r&&(n+=t(e.slice(r,i)));let a=e.indexOf(`]]>`,i+9);if(a<0){n+=e.slice(i+9);break}n+=e.slice(i+9,a),r=a+3}return n}function r(e){return e===32||e===9||e===10||e===13}var i=class{constructor(e){this.queryParent=!1,this.progressive=!1,this.retain=`all`,this.parentMap=/* @__PURE__ */ new WeakMap,this.evtListeners={},this.openListenerCount=0,this.watchedPaths=/* @__PURE__ */ new Set,this.watchedChildren=/* @__PURE__ */ new Map,this.watchedTopLevel=/* @__PURE__ */ new Set,this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.basePath=``,this.active=!1,e&&(this.queryParent=e.queryParent===!0,this.progressive=e.progressive===!0,e.retain&&(this.retain=e.retain))}write(e){return this.active||this.begin(void 0,void 0),e.length===0||(this.buffer.length===0?this.buffer=e:this.buffer+=e,this.consume(!1)),this}end(){if(!this.active)return[];this.consume(!0),this.finish();let e=this.roots;return this.reset(),e}parse(e,t,n){return this.begin(t,n),this.write(e),this.end()}begin(e,t){this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.basePath=t||``,this.top=e?{node:e,fullTag:this.basePath,textStart:0}:void 0,this.rootOwner=this.top,this.parentMap=/* @__PURE__ */ new WeakMap,this.active=!0}reset(){this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.top=void 0,this.rootOwner=void 0,this.basePath=``,this.active=!1}attach(e,t){if(this.retain===`none`)return;if(t===void 0){this.retain===`all`&&this.roots.push(e);return}let n=t.children;n?n.push(e):t.children=[e]}consume(e){let t=this.buffer,r=t.length,i=this.progressive,a=i&&this.openListenerCount>0,o=this.cursor,s=-1;for(;o<r;){let c=t.indexOf(`<`,o);if(c<0||(s=c,o=c+1,o>=r))break;let l=t.charCodeAt(o);if(l===47){let a=t.indexOf(`>`,o);if(a<0){if(!e)break;o=r;break}let l=this.stack.pop();if(o=a+1,s=-1,!l)continue;let u=l.node,d=this.stack.length?this.stack[this.stack.length-1]:this.rootOwner,f=d?d.node:void 0;!l.hasChild&&c>l.textStart&&(u.text=n(t.slice(l.textStart,c))),i&&this.watchedPaths.has(l.fullTag)&&this.emit(`</`+l.fullTag+`>`,u,f),this.attach(u,f),d&&(d.hasChild=!0),this.top=d;continue}if(l===63){let n=t.indexOf(`>`,o);if(n<0){if(!e)break;o=r;break}o=n+1,s=-1;continue}if(l===33){let n;if(t.charCodeAt(o+1)===45&&t.charCodeAt(o+2)===45?(n=t.indexOf(`-->`,o+3),n=n<0?-1:n+3):t.startsWith(`![CDATA[`,o)?(n=t.indexOf(`]]>`,o+9),n=n<0?-1:n+3):(n=t.indexOf(`>`,o),n=n<0?-1:n+1),n<0){if(!e)break;o=r;break}o=n,s=-1;continue}let u=this.parseOpenTag(t,o,r,e);if(u===null)break;o=u.end,s=-1;let d=u.node,f=u.tag,p=this.top?this.top.node:void 0;if(this.queryParent&&p&&this.parentMap.set(d,p),u.selfClose){if(i){let e=this.top?this.top.fullTag:this.basePath,t=e?this.watchedChildren.get(e):this.watchedTopLevel;if(t&&t.has(f)){let t=e?e+`.`+f:f;a&&this.emit(`<`+t+`>`,d,p),this.emit(`</`+t+`>`,d,p)}}this.attach(d,p),this.top&&(this.top.hasChild=!0);continue}let m=this.top?this.top.fullTag?this.top.fullTag+`.`+f:f:this.basePath?this.basePath+`.`+f:f;a&&this.watchedPaths.has(m)&&this.emit(`<`+m+`>`,d,p);let h={node:d,fullTag:m,textStart:o};this.stack.push(h),this.top=h}this.cursor=e?r:s>=0?s:o,e||this.compact()}parseOpenTag(e,n,i,a){let o=n;for(;o<i;){let t=e.charCodeAt(o);if(r(t)||t===47||t===62)break;o++}if(o>=i&&!a)return null;let s=e.slice(n,o),c={tagName:s},l=!1;for(;o<i;){let n=e.charCodeAt(o);for(;o<i&&r(n);)n=e.charCodeAt(++o);if(o>=i)break;if(n===62)return o++,{node:c,tag:s,selfClose:l,end:o};if(n===47){l=!0,o++;continue}let u=o;for(;o<i&&(n=e.charCodeAt(o),!(n===61||r(n)||n===62||n===47));)o++;if(o>=i&&!a)return null;let d=e.slice(u,o);for(;o<i&&r(e.charCodeAt(o));)o++;if(o>=i&&!a)return null;if(e.charCodeAt(o)===61){for(o++;o<i&&r(e.charCodeAt(o));)o++;if(o>=i&&!a)return null;let n=e.charCodeAt(o);if(n===34||n===39){o++;let r=e.indexOf(n===34?`"`:`'`,o);if(r<0){if(!a)return null;r=i}let s=e.slice(o,r);c[d]=s.indexOf(`&`)<0?s:t(s),o=r+1}}else d&&(c[d]=``)}return a?{node:c,tag:s,selfClose:l,end:o}:null}finish(){let e=this.buffer,t=e.length;for(;this.stack.length>0;){let r=this.stack.pop(),i=r.node;!r.hasChild&&t>r.textStart&&(i.text=n(e.slice(r.textStart,t)),t=r.textStart);let a=this.stack.length?this.stack[this.stack.length-1]:this.rootOwner,o=a?a.node:void 0;this.attach(i,o)}}compact(){let e=this.cursor;for(let t=this.stack.length-1;t>=0;t--){let n=this.stack[t];if(!n.hasChild){e=n.textStart;break}}if(!(e<=0)){this.buffer=this.buffer.slice(e),this.cursor-=e;for(let t=0;t<this.stack.length;t++){let n=this.stack[t];n.textStart=n.textStart>e?n.textStart-e:0}this.rootOwner&&(this.rootOwner.textStart=0)}}getParent(e){return this.queryParent?this.parentMap.get(e):null}addListener(e,t){this.$addListener(e,t)}removeListener(e,t){this.$removeListener(e,t)}on(e,t){this.addListener(e,t)}off(e,t){this.removeListener(e,t)}$addListener(e,t){let n=this.evtListeners[e];if(n?n.push(t):this.evtListeners[e]=[t],e.charCodeAt(1)!==47&&this.openListenerCount++,e.charCodeAt(0)===60){let t=e.slice(e.charCodeAt(1)===47?2:1,e.length-1);this.watchedPaths.add(t);let n=t.lastIndexOf(`.`);if(n<0)this.watchedTopLevel.add(t);else{let e=t.slice(0,n),r=this.watchedChildren.get(e);r||(r=/* @__PURE__ */ new Set,this.watchedChildren.set(e,r)),r.add(t.slice(n+1))}}}$removeListener(e,t){let n=this.evtListeners[e],r=-1;n&&(r=n.indexOf(t)),r>=0&&(n.splice(r,1),e.charCodeAt(1)!==47&&this.openListenerCount--)}emit(e,t,n){let r=this.evtListeners[e];if(r)for(let e=0;e<r.length;e++)r[e](t,n)}};exports.XmlParser=i,exports.decodeEntities=t;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * One parsed element.
3
+ *
4
+ * Attributes are copied onto the node as own properties (flat, so a document
5
+ * costs one object per element), which is why the type carries an index
6
+ * signature. `text` is set only for elements without child elements.
7
+ */
8
+ interface XmlNode {
9
+ tagName: string;
10
+ children?: XmlNode[];
11
+ text?: string;
12
+ [attribute: string]: any;
13
+ }
14
+ /** Listener signature for the path-based events. */
15
+ type XmlListener = (node: XmlNode, parent?: XmlNode) => void;
16
+ /**
17
+ * Decode XML entities.
18
+ *
19
+ * A manual scan beats `String.replace` with a callback by ~2.5x on OSM-sized
20
+ * attribute values: no regex, no match object and no per-entity closure. The
21
+ * `indexOf` guard keeps entity-free values allocation-free, and unknown
22
+ * entities are left exactly as written.
23
+ */
24
+ export declare function decodeEntities(value: string): string;
25
+ type Retain = 'all' | 'root' | 'none';
26
+ interface XmlParserOptions {
27
+ /** Emit `<path>` / `</path>` events as elements open and close. Default `false`. */
28
+ progressive?: boolean;
29
+ /** Track parents so `getParent(node)` works. Default `false`. */
30
+ queryParent?: boolean;
31
+ /**
32
+ * What the parser keeps after an element closes:
33
+ * - `'all'` (default) the whole document tree, returned by `end()`
34
+ * - `'root'` the root being built; a finished top-level element is emitted
35
+ * and dropped, so memory is bounded by the largest root
36
+ * - `'none'` nothing; every element is emitted and dropped, so memory is
37
+ * bounded by the nesting depth. Combine with listeners: an element
38
+ * nobody listens for is parsed and discarded.
39
+ */
40
+ retain?: Retain;
41
+ }
42
+ export declare class XmlParser {
43
+ private queryParent;
44
+ private progressive;
45
+ private retain;
46
+ private parentMap;
47
+ private evtListeners;
48
+ /**
49
+ * Number of registered open-tag (`<path>`) listeners; lets the scanner skip
50
+ * building open-event strings for the overwhelmingly common leaf elements.
51
+ */
52
+ private openListenerCount;
53
+ /**
54
+ * Exact paths that have at least one listener, the child paths keyed by
55
+ * their parent path, and top-level listeners. These let the scanner skip
56
+ * building event strings for the many `<nd>`/`<tag>` leaves that nobody
57
+ * observes.
58
+ */
59
+ private watchedPaths;
60
+ private watchedChildren;
61
+ private watchedTopLevel;
62
+ /** Input not consumed yet: pending text plus any construct split by a chunk. */
63
+ private buffer;
64
+ /** Next position to scan in `buffer`. */
65
+ private cursor;
66
+ private stack;
67
+ private roots;
68
+ private top;
69
+ private rootOwner;
70
+ private basePath;
71
+ private active;
72
+ constructor(opts?: XmlParserOptions);
73
+ /**
74
+ * Feed a chunk of XML.
75
+ *
76
+ * Chunks may split anywhere: inside a tag, an attribute value, a comment, a
77
+ * CDATA section or a text run. Anything incomplete is kept and resumed on
78
+ * the next `write()`; call `end()` when the input is exhausted.
79
+ */
80
+ write(chunk: string): this;
81
+ /**
82
+ * Finish the document: close elements still open at the end of input and
83
+ * return the top-level nodes (empty for `retain: 'root' | 'none'`).
84
+ */
85
+ end(): XmlNode[];
86
+ /**
87
+ * Parse a complete XML string in one pass.
88
+ *
89
+ * Equivalent to `write(xml).end()`, and the only entry point you need
90
+ * unless the document arrives in pieces.
91
+ *
92
+ * @param xml - the XML source to scan.
93
+ * @param parent - optional pre-existing node that owns the parsed content;
94
+ * when supplied, top-level nodes attach to its `children` instead of being
95
+ * returned as roots.
96
+ * @param dir - optional dotted tag-path prefix applied to emitted event names.
97
+ * @returns the top-level nodes (empty when a `parent` owns them).
98
+ */
99
+ parse(xml: string, parent?: XmlNode, dir?: string): XmlNode[];
100
+ private begin;
101
+ private reset;
102
+ /**
103
+ * Store a closed element according to the retention mode.
104
+ *
105
+ * `parentNode === undefined` means the element is top-level (a root), which
106
+ * `retain: 'root'` drops after emitting it.
107
+ */
108
+ private attach;
109
+ /** Scan everything in `buffer` that is complete, or all of it at `end()`. */
110
+ private consume;
111
+ /**
112
+ * Parse one opening tag at `from` (just past `<`).
113
+ *
114
+ * Returns `null` when the tag is not complete and more input may follow; at
115
+ * `final` an unterminated tag is accepted as far as it goes, which is what
116
+ * keeps truncated documents usable.
117
+ */
118
+ private parseOpenTag;
119
+ /**
120
+ * Close elements still open at the end of input instead of dropping them:
121
+ * a truncated download keeps the document parsed so far.
122
+ */
123
+ private finish;
124
+ /**
125
+ * Drop consumed input.
126
+ *
127
+ * Everything before the innermost open element is dead: its text has not
128
+ * been read yet, and ancestors with children never read theirs. Keeping the
129
+ * buffer from there is what makes a streamed document cost the pending text
130
+ * rather than the whole file.
131
+ */
132
+ private compact;
133
+ /** Parent of a node parsed with `queryParent: true`, otherwise `null`. */
134
+ getParent(node: any): any;
135
+ /**
136
+ * Register a listener for an event.
137
+ *
138
+ * Event names are the dotted tag path, prefixed with `<` for an opening
139
+ * element and `</` for a closing one: `'<osm.way>'`, `'</osm.way>'`.
140
+ * Filter inside the listener - `if (node.tagName !== 'way') return;` - and
141
+ * an unobserved path costs nothing, because the scanner skips building
142
+ * event names nobody listens for.
143
+ */
144
+ addListener(evt: string, func: XmlListener): void;
145
+ removeListener(evt: string, func: XmlListener): void;
146
+ on(evt: string, func: XmlListener): void;
147
+ off(evt: string, func: XmlListener): void;
148
+ private $addListener;
149
+ private $removeListener;
150
+ private emit;
151
+ }
152
+ export type { Retain, XmlListener, XmlNode, XmlParserOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ function e(e){switch(e){case`amp`:return`&`;case`lt`:return`<`;case`gt`:return`>`;case`quot`:return`"`;case`apos`:return`'`}if(e.charCodeAt(0)===35){let t=e.charCodeAt(1)===120||e.charCodeAt(1)===88?parseInt(e.slice(2),16):parseInt(e.slice(1),10);if(Number.isFinite(t)&&t>=0&&t<=1114111)return String.fromCodePoint(t)}return null}function t(t){let n=t.indexOf(`&`);if(n<0)return t;let r=t.slice(0,n);for(;n<t.length;){let i=t.indexOf(`;`,n+1);if(i<0){r+=t.slice(n);break}let a=e(t.slice(n+1,i));if(r+=a===null?t.slice(n,i+1):a,n=t.indexOf(`&`,i+1),n<0){r+=t.slice(i+1);break}r+=t.slice(i+1,n)}return r}function n(e){if(e.indexOf(`<![CDATA[`)<0)return e.indexOf(`&`)<0?e:t(e);let n=``,r=0;for(;r<e.length;){let i=e.indexOf(`<![CDATA[`,r);if(i<0){n+=t(e.slice(r));break}i>r&&(n+=t(e.slice(r,i)));let a=e.indexOf(`]]>`,i+9);if(a<0){n+=e.slice(i+9);break}n+=e.slice(i+9,a),r=a+3}return n}function r(e){return e===32||e===9||e===10||e===13}var i=class{constructor(e){this.queryParent=!1,this.progressive=!1,this.retain=`all`,this.parentMap=/* @__PURE__ */ new WeakMap,this.evtListeners={},this.openListenerCount=0,this.watchedPaths=/* @__PURE__ */ new Set,this.watchedChildren=/* @__PURE__ */ new Map,this.watchedTopLevel=/* @__PURE__ */ new Set,this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.basePath=``,this.active=!1,e&&(this.queryParent=e.queryParent===!0,this.progressive=e.progressive===!0,e.retain&&(this.retain=e.retain))}write(e){return this.active||this.begin(void 0,void 0),e.length===0||(this.buffer.length===0?this.buffer=e:this.buffer+=e,this.consume(!1)),this}end(){if(!this.active)return[];this.consume(!0),this.finish();let e=this.roots;return this.reset(),e}parse(e,t,n){return this.begin(t,n),this.write(e),this.end()}begin(e,t){this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.basePath=t||``,this.top=e?{node:e,fullTag:this.basePath,textStart:0}:void 0,this.rootOwner=this.top,this.parentMap=/* @__PURE__ */ new WeakMap,this.active=!0}reset(){this.buffer=``,this.cursor=0,this.stack=[],this.roots=[],this.top=void 0,this.rootOwner=void 0,this.basePath=``,this.active=!1}attach(e,t){if(this.retain===`none`)return;if(t===void 0){this.retain===`all`&&this.roots.push(e);return}let n=t.children;n?n.push(e):t.children=[e]}consume(e){let t=this.buffer,r=t.length,i=this.progressive,a=i&&this.openListenerCount>0,o=this.cursor,s=-1;for(;o<r;){let c=t.indexOf(`<`,o);if(c<0||(s=c,o=c+1,o>=r))break;let l=t.charCodeAt(o);if(l===47){let a=t.indexOf(`>`,o);if(a<0){if(!e)break;o=r;break}let l=this.stack.pop();if(o=a+1,s=-1,!l)continue;let u=l.node,d=this.stack.length?this.stack[this.stack.length-1]:this.rootOwner,f=d?d.node:void 0;!l.hasChild&&c>l.textStart&&(u.text=n(t.slice(l.textStart,c))),i&&this.watchedPaths.has(l.fullTag)&&this.emit(`</`+l.fullTag+`>`,u,f),this.attach(u,f),d&&(d.hasChild=!0),this.top=d;continue}if(l===63){let n=t.indexOf(`>`,o);if(n<0){if(!e)break;o=r;break}o=n+1,s=-1;continue}if(l===33){let n;if(t.charCodeAt(o+1)===45&&t.charCodeAt(o+2)===45?(n=t.indexOf(`-->`,o+3),n=n<0?-1:n+3):t.startsWith(`![CDATA[`,o)?(n=t.indexOf(`]]>`,o+9),n=n<0?-1:n+3):(n=t.indexOf(`>`,o),n=n<0?-1:n+1),n<0){if(!e)break;o=r;break}o=n,s=-1;continue}let u=this.parseOpenTag(t,o,r,e);if(u===null)break;o=u.end,s=-1;let d=u.node,f=u.tag,p=this.top?this.top.node:void 0;if(this.queryParent&&p&&this.parentMap.set(d,p),u.selfClose){if(i){let e=this.top?this.top.fullTag:this.basePath,t=e?this.watchedChildren.get(e):this.watchedTopLevel;if(t&&t.has(f)){let t=e?e+`.`+f:f;a&&this.emit(`<`+t+`>`,d,p),this.emit(`</`+t+`>`,d,p)}}this.attach(d,p),this.top&&(this.top.hasChild=!0);continue}let m=this.top?this.top.fullTag?this.top.fullTag+`.`+f:f:this.basePath?this.basePath+`.`+f:f;a&&this.watchedPaths.has(m)&&this.emit(`<`+m+`>`,d,p);let h={node:d,fullTag:m,textStart:o};this.stack.push(h),this.top=h}this.cursor=e?r:s>=0?s:o,e||this.compact()}parseOpenTag(e,n,i,a){let o=n;for(;o<i;){let t=e.charCodeAt(o);if(r(t)||t===47||t===62)break;o++}if(o>=i&&!a)return null;let s=e.slice(n,o),c={tagName:s},l=!1;for(;o<i;){let n=e.charCodeAt(o);for(;o<i&&r(n);)n=e.charCodeAt(++o);if(o>=i)break;if(n===62)return o++,{node:c,tag:s,selfClose:l,end:o};if(n===47){l=!0,o++;continue}let u=o;for(;o<i&&(n=e.charCodeAt(o),!(n===61||r(n)||n===62||n===47));)o++;if(o>=i&&!a)return null;let d=e.slice(u,o);for(;o<i&&r(e.charCodeAt(o));)o++;if(o>=i&&!a)return null;if(e.charCodeAt(o)===61){for(o++;o<i&&r(e.charCodeAt(o));)o++;if(o>=i&&!a)return null;let n=e.charCodeAt(o);if(n===34||n===39){o++;let r=e.indexOf(n===34?`"`:`'`,o);if(r<0){if(!a)return null;r=i}let s=e.slice(o,r);c[d]=s.indexOf(`&`)<0?s:t(s),o=r+1}}else d&&(c[d]=``)}return a?{node:c,tag:s,selfClose:l,end:o}:null}finish(){let e=this.buffer,t=e.length;for(;this.stack.length>0;){let r=this.stack.pop(),i=r.node;!r.hasChild&&t>r.textStart&&(i.text=n(e.slice(r.textStart,t)),t=r.textStart);let a=this.stack.length?this.stack[this.stack.length-1]:this.rootOwner,o=a?a.node:void 0;this.attach(i,o)}}compact(){let e=this.cursor;for(let t=this.stack.length-1;t>=0;t--){let n=this.stack[t];if(!n.hasChild){e=n.textStart;break}}if(!(e<=0)){this.buffer=this.buffer.slice(e),this.cursor-=e;for(let t=0;t<this.stack.length;t++){let n=this.stack[t];n.textStart=n.textStart>e?n.textStart-e:0}this.rootOwner&&(this.rootOwner.textStart=0)}}getParent(e){return this.queryParent?this.parentMap.get(e):null}addListener(e,t){this.$addListener(e,t)}removeListener(e,t){this.$removeListener(e,t)}on(e,t){this.addListener(e,t)}off(e,t){this.removeListener(e,t)}$addListener(e,t){let n=this.evtListeners[e];if(n?n.push(t):this.evtListeners[e]=[t],e.charCodeAt(1)!==47&&this.openListenerCount++,e.charCodeAt(0)===60){let t=e.slice(e.charCodeAt(1)===47?2:1,e.length-1);this.watchedPaths.add(t);let n=t.lastIndexOf(`.`);if(n<0)this.watchedTopLevel.add(t);else{let e=t.slice(0,n),r=this.watchedChildren.get(e);r||(r=/* @__PURE__ */ new Set,this.watchedChildren.set(e,r)),r.add(t.slice(n+1))}}}$removeListener(e,t){let n=this.evtListeners[e],r=-1;n&&(r=n.indexOf(t)),r>=0&&(n.splice(r,1),e.charCodeAt(1)!==47&&this.openListenerCount--)}emit(e,t,n){let r=this.evtListeners[e];if(r)for(let e=0;e<r.length;e++)r[e](t,n)}};export{i as XmlParser,t as decodeEntities};
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "xml-event-parser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Tiny dependency-free XML parser with a tree and path-based events in one pass",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/tibetty/xml-event-parser.git"
8
+ },
9
+ "homepage": "https://github.com/tibetty/xml-event-parser#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/tibetty/xml-event-parser/issues"
12
+ },
13
+ "keywords": [
14
+ "xml",
15
+ "parser",
16
+ "events",
17
+ "tree",
18
+ "streaming",
19
+ "zero-dependency"
20
+ ],
21
+ "main": "dist/index.cjs",
22
+ "scripts": {
23
+ "build": "rolldown -c",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit",
26
+ "bench:data": "node bench/generate.mjs",
27
+ "bench": "node --expose-gc bench/run.mjs",
28
+ "prepublishOnly": "npm run build && npm test"
29
+ },
30
+ "license": "MIT",
31
+ "type": "module",
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "devDependencies": {
41
+ "fast-xml-parser": "^5.12.1",
42
+ "htmlparser2": "^12.0.0",
43
+ "rolldown": "^1.3.2",
44
+ "rolldown-plugin-dts": "^0.30.2",
45
+ "sax": "^1.6.1",
46
+ "txml": "^6.0.3",
47
+ "typescript": "^7.0.2",
48
+ "vitest": "^5.1.0"
49
+ },
50
+ "module": "dist/index.mjs",
51
+ "types": "dist/index.d.ts",
52
+ "exports": {
53
+ ".": {
54
+ "types": "./dist/index.d.ts",
55
+ "import": "./dist/index.mjs",
56
+ "require": "./dist/index.cjs"
57
+ }
58
+ },
59
+ "sideEffects": false,
60
+ "engines": {
61
+ "node": ">=18"
62
+ }
63
+ }