@sdxc/xml 0.0.0-pre.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.md +21 -0
- package/README.md +250 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +167 -0
- package/dist/lib/clone-declaration.d.ts +14 -0
- package/dist/lib/clone-declaration.js +17 -0
- package/dist/lib/clone-element.d.ts +14 -0
- package/dist/lib/clone-element.js +21 -0
- package/dist/lib/decode-entities.d.ts +16 -0
- package/dist/lib/decode-entities.js +90 -0
- package/dist/lib/escape-xml.d.ts +25 -0
- package/dist/lib/escape-xml.js +44 -0
- package/dist/lib/html-entities.d.ts +17 -0
- package/dist/lib/html-entities.js +269 -0
- package/dist/lib/parse-document.d.ts +17 -0
- package/dist/lib/parse-document.js +296 -0
- package/dist/lib/stringify-document.d.ts +17 -0
- package/dist/lib/stringify-document.js +120 -0
- package/dist/lib/traversal.d.ts +47 -0
- package/dist/lib/traversal.js +90 -0
- package/dist/lib/xml-names.d.ts +25 -0
- package/dist/lib/xml-names.js +38 -0
- package/package.json +22 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Xalambrí
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# @sdxc/xml
|
|
2
|
+
|
|
3
|
+
XML parser and serializer for RSS-style feeds.
|
|
4
|
+
|
|
5
|
+
It reads XML text into an `XML` document instance and writes that instance back out. The
|
|
6
|
+
target is the subset of XML feeds actually use: one root element, attributes, nested
|
|
7
|
+
elements, text nodes, CDATA sections, namespace-prefixed names, and an XML declaration.
|
|
8
|
+
|
|
9
|
+
Real feeds are written by tools that emit ` ` and `—` without declaring a DTD,
|
|
10
|
+
so the parser resolves the five entities XML predefines, numeric character references such
|
|
11
|
+
as `’`, and the 248 named entities the three XHTML 1.0 entity sets declare. A name
|
|
12
|
+
outside those sets is reported as a parse error. Serialization escapes only the five
|
|
13
|
+
predefines, so a document that arrives with ` ` round-trips as the character itself.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm add @sdxc/xml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Parsing and serialization report failures as a `Result` from
|
|
22
|
+
[`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result), which installs alongside this
|
|
23
|
+
package and supplies `isFailure`, `isSuccess`, and `unwrap`.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Parse A Document
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { isFailure } from "@sdxc/result";
|
|
31
|
+
import { XML } from "@sdxc/xml";
|
|
32
|
+
|
|
33
|
+
let result = XML.parse(
|
|
34
|
+
`<?xml version="1.0"?><rss version="2.0"><channel><title>Feed</title></channel></rss>`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (isFailure(result)) throw result.error;
|
|
38
|
+
|
|
39
|
+
let xml = result.data;
|
|
40
|
+
xml.query("channel/title"); // { name: "title", children: ["Feed"] }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Whitespace-only text nodes are indentation rather than content, so the parser drops them and
|
|
44
|
+
a traversal never has to step over them.
|
|
45
|
+
|
|
46
|
+
### Traverse A Document
|
|
47
|
+
|
|
48
|
+
`query` and `queryAll` take a `/`-delimited path rooted at the document's root element;
|
|
49
|
+
`find` and `findAll` take a predicate and walk depth-first.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
let channel = xml.query("channel");
|
|
53
|
+
let items = xml.queryAll("channel/item");
|
|
54
|
+
|
|
55
|
+
let firstLink = xml.find((element) => element.name === "link");
|
|
56
|
+
let allLinks = xml.findAll((element) => element.name === "link");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Write A Document
|
|
60
|
+
|
|
61
|
+
`XML.stringify` takes a root element and returns the text for it:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { isFailure } from "@sdxc/result";
|
|
65
|
+
import { XML } from "@sdxc/xml";
|
|
66
|
+
|
|
67
|
+
let result = XML.stringify({
|
|
68
|
+
name: "rss",
|
|
69
|
+
attributes: { version: "2.0" },
|
|
70
|
+
children: [
|
|
71
|
+
{
|
|
72
|
+
name: "channel",
|
|
73
|
+
children: [{ name: "title", children: ["Feed"] }],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (isFailure(result)) throw result.error;
|
|
79
|
+
|
|
80
|
+
let source = result.data; // <rss version="2.0"><channel><title>Feed</title></channel></rss>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A declaration belongs to a document rather than to an element, so pass the whole document to
|
|
84
|
+
write one:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
let result = XML.stringify({
|
|
88
|
+
declaration: { version: "1.0", encoding: "UTF-8" },
|
|
89
|
+
root: { name: "rss", attributes: { version: "2.0" } },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// <?xml version="1.0" encoding="UTF-8"?>
|
|
93
|
+
// <rss version="2.0"/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Round-Trip A Document
|
|
97
|
+
|
|
98
|
+
`toJSON` hands back plain data that both the `XML` constructor and `XML.stringify` accept,
|
|
99
|
+
and `toString` serializes the instance in place.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
let json = xml.toJSON();
|
|
103
|
+
|
|
104
|
+
let copy = new XML(json);
|
|
105
|
+
let source = copy.toString();
|
|
106
|
+
|
|
107
|
+
XML.stringify(json); // the same text, as a Result
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
### `XML.parse(source: string): Result<XML, XMLParseError>`
|
|
113
|
+
|
|
114
|
+
Parses XML text into an `XML` instance.
|
|
115
|
+
|
|
116
|
+
### `XML.stringify(input: XML | XML.Input): Result<string, XMLStringifyError>`
|
|
117
|
+
|
|
118
|
+
Serializes an instance, whole document data, or a bare root element into XML text. An element
|
|
119
|
+
carries no declaration; pass a document or an instance to write one.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
XML.stringify({ name: "rss", attributes: { version: "2.0" } });
|
|
123
|
+
// success('<rss version="2.0"/>')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `new XML(document: XML.Document)`
|
|
127
|
+
|
|
128
|
+
Wraps a document that is already plain data, such as one from `toJSON`.
|
|
129
|
+
|
|
130
|
+
### `xml.declaration: XML.Declaration | undefined`
|
|
131
|
+
|
|
132
|
+
The parsed XML declaration, when the document carried one.
|
|
133
|
+
|
|
134
|
+
### `xml.root: XML.Element`
|
|
135
|
+
|
|
136
|
+
The root element.
|
|
137
|
+
|
|
138
|
+
### `xml.find(predicate: XML.Predicate): XML.Element | undefined`
|
|
139
|
+
|
|
140
|
+
The first element the predicate accepts, in depth-first order.
|
|
141
|
+
|
|
142
|
+
### `xml.findAll(predicate: XML.Predicate): XML.Element[]`
|
|
143
|
+
|
|
144
|
+
Every element the predicate accepts, in depth-first order.
|
|
145
|
+
|
|
146
|
+
### `xml.query(path: string): XML.Element | undefined`
|
|
147
|
+
|
|
148
|
+
The first element at a `/`-delimited path such as `channel/item/title`.
|
|
149
|
+
|
|
150
|
+
### `xml.queryAll(path: string): XML.Element[]`
|
|
151
|
+
|
|
152
|
+
Every element at a `/`-delimited path such as `channel/item`.
|
|
153
|
+
|
|
154
|
+
### `xml.toJSON(): XML.Document`
|
|
155
|
+
|
|
156
|
+
The document as plain serializable data.
|
|
157
|
+
|
|
158
|
+
### `xml.toString(): string`
|
|
159
|
+
|
|
160
|
+
The document as XML text. This is the one entry point that throws an `XMLStringifyError`
|
|
161
|
+
rather than returning it, because `toString` has no room for a `Result`; reach for
|
|
162
|
+
`XML.stringify` where a failure is a value you want to handle.
|
|
163
|
+
|
|
164
|
+
### `XMLParseError`
|
|
165
|
+
|
|
166
|
+
The source is malformed, carries no root element, or names an entity outside the resolved
|
|
167
|
+
sets.
|
|
168
|
+
|
|
169
|
+
### `XMLStringifyError`
|
|
170
|
+
|
|
171
|
+
The tree cannot be expressed as valid XML: a name that fails the XML `Name` production, or a
|
|
172
|
+
prefix with no namespace declared in scope.
|
|
173
|
+
|
|
174
|
+
### Types
|
|
175
|
+
|
|
176
|
+
Every public type lives in the `XML` namespace: `XML.Declaration`, `XML.Element`,
|
|
177
|
+
`XML.Node`, `XML.Document`, `XML.Input`, and `XML.Predicate`. `XML.Input` is what
|
|
178
|
+
`stringify` accepts: a whole `XML.Document` or the root `XML.Element` alone.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import type { XML } from "@sdxc/xml";
|
|
182
|
+
|
|
183
|
+
function titleOf(element: XML.Element): string | undefined {
|
|
184
|
+
let [text] = element.children ?? [];
|
|
185
|
+
return typeof text === "string" ? text : undefined;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
An `XML.Node` is either an `XML.Element` or a string, and a string is always a text node.
|
|
190
|
+
Markup belongs in child elements: a string holding `<p>Hi</p>` serializes as escaped text,
|
|
191
|
+
which is what makes the output parse back into the tree it was given.
|
|
192
|
+
|
|
193
|
+
## Pattern: Reading A Feed's Channel
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { isFailure } from "@sdxc/result";
|
|
197
|
+
import { XML } from "@sdxc/xml";
|
|
198
|
+
|
|
199
|
+
let result = XML.parse(source);
|
|
200
|
+
if (isFailure(result)) throw result.error;
|
|
201
|
+
|
|
202
|
+
let xml = result.data;
|
|
203
|
+
|
|
204
|
+
for (let item of xml.queryAll("channel/item")) {
|
|
205
|
+
let title = item.children?.find((child) => typeof child !== "string" && child.name === "title");
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Pattern: Preserving Namespaces
|
|
210
|
+
|
|
211
|
+
A prefix is resolved against the `xmlns:*` attributes in scope, so declare one on the same
|
|
212
|
+
element tree that uses it. Serialization fails on a prefix with no declaration rather than
|
|
213
|
+
writing XML that will not parse.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
XML.stringify({
|
|
217
|
+
name: "rss",
|
|
218
|
+
attributes: {
|
|
219
|
+
version: "2.0",
|
|
220
|
+
"xmlns:content": "http://purl.org/rss/1.0/modules/content/",
|
|
221
|
+
},
|
|
222
|
+
children: [{ name: "content:encoded", children: ["<p>HTML</p>"] }],
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Versioning
|
|
227
|
+
|
|
228
|
+
Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
|
|
229
|
+
|
|
230
|
+
Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.
|
|
231
|
+
|
|
232
|
+
Depend on one exact date, and move it when you are ready to take the change:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"dependencies": {
|
|
237
|
+
"@sdxc/xml": "2026.9.4"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
|
247
|
+
|
|
248
|
+
## Author
|
|
249
|
+
|
|
250
|
+
[Sergio Xalambrí](https://sergiodxa.com)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides the XML class and error types for parsing, traversing, and
|
|
3
|
+
* serializing XML documents.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import type { Result } from "@sdxc/result";
|
|
9
|
+
/**
|
|
10
|
+
* Signals that XML source could not be converted into the package tree format.
|
|
11
|
+
*/
|
|
12
|
+
export declare class XMLParseError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Signals that an XML tree could not be serialized into a valid XML string.
|
|
17
|
+
*/
|
|
18
|
+
export declare class XMLStringifyError extends Error {
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Groups the public XML types under a single import surface.
|
|
23
|
+
*/
|
|
24
|
+
export declare namespace XML {
|
|
25
|
+
/**
|
|
26
|
+
* Stores XML declaration attributes that should appear before the root element.
|
|
27
|
+
*/
|
|
28
|
+
interface Declaration {
|
|
29
|
+
version?: string;
|
|
30
|
+
encoding?: string;
|
|
31
|
+
standalone?: "yes" | "no";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Stores one XML element with its raw tag name, attributes, and ordered children.
|
|
35
|
+
*/
|
|
36
|
+
interface Element {
|
|
37
|
+
name: string;
|
|
38
|
+
attributes?: Record<string, string>;
|
|
39
|
+
children?: Node[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Represents either a text node or a nested XML element.
|
|
43
|
+
*/
|
|
44
|
+
type Node = string | Element;
|
|
45
|
+
/**
|
|
46
|
+
* Stores a parsed XML document with the declaration and single root element.
|
|
47
|
+
*/
|
|
48
|
+
interface Document {
|
|
49
|
+
declaration?: Declaration;
|
|
50
|
+
root: Element;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Accepts either a full XML document or a single root element for serialization.
|
|
54
|
+
*/
|
|
55
|
+
type Input = Document | Element;
|
|
56
|
+
/**
|
|
57
|
+
* Checks one element while traversing an XML tree.
|
|
58
|
+
*/
|
|
59
|
+
type Predicate = (element: Element) => boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Wraps one parsed XML document and provides traversal and serialization helpers.
|
|
63
|
+
*/
|
|
64
|
+
export declare class XML {
|
|
65
|
+
#private;
|
|
66
|
+
/**
|
|
67
|
+
* Stores one XML document instance around a declaration and root element.
|
|
68
|
+
*
|
|
69
|
+
* @param document - The plain XML document data to wrap
|
|
70
|
+
*/
|
|
71
|
+
constructor(document: XML.Document);
|
|
72
|
+
/**
|
|
73
|
+
* Parses XML into an `XML` instance.
|
|
74
|
+
*
|
|
75
|
+
* @param source - Raw XML text to parse
|
|
76
|
+
* @returns A Result containing an `XML` instance or a parse error
|
|
77
|
+
*/
|
|
78
|
+
static parse(source: string): Result<XML, XMLParseError>;
|
|
79
|
+
/**
|
|
80
|
+
* Serializes an XML instance, plain document data, or a root element into XML text.
|
|
81
|
+
*
|
|
82
|
+
* @param input - The XML instance, document data, or root element to serialize
|
|
83
|
+
* @returns A Result containing the XML string or a serialization error
|
|
84
|
+
*/
|
|
85
|
+
static stringify(input: XML | XML.Input): Result<string, XMLStringifyError>;
|
|
86
|
+
/**
|
|
87
|
+
* Exposes the XML declaration as cloned data so callers cannot mutate internals.
|
|
88
|
+
*/
|
|
89
|
+
get declaration(): XML.Declaration | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Exposes the root element as cloned data so callers cannot mutate internals.
|
|
92
|
+
*/
|
|
93
|
+
get root(): XML.Element;
|
|
94
|
+
/**
|
|
95
|
+
* Returns the wrapped XML document as plain serializable data.
|
|
96
|
+
*/
|
|
97
|
+
toJSON(): XML.Document;
|
|
98
|
+
/**
|
|
99
|
+
* Serializes the current XML instance into a string.
|
|
100
|
+
*/
|
|
101
|
+
toString(): string;
|
|
102
|
+
/**
|
|
103
|
+
* Returns the first element in the document tree that matches the predicate.
|
|
104
|
+
*
|
|
105
|
+
* @param predicate - Receives each visited element in depth-first order
|
|
106
|
+
* @returns The first matching element, if one exists
|
|
107
|
+
*/
|
|
108
|
+
find(predicate: XML.Predicate): XML.Element | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Returns every element in the document tree that matches the predicate.
|
|
111
|
+
*
|
|
112
|
+
* @param predicate - Receives each visited element in depth-first order
|
|
113
|
+
* @returns All matching elements in traversal order
|
|
114
|
+
*/
|
|
115
|
+
findAll(predicate: XML.Predicate): XML.Element[];
|
|
116
|
+
/**
|
|
117
|
+
* Resolves the first element that matches a simple `/`-delimited path.
|
|
118
|
+
*
|
|
119
|
+
* @param path - Path such as `channel/item/title` or `rss/channel`
|
|
120
|
+
* @returns The first matching element, if one exists
|
|
121
|
+
*/
|
|
122
|
+
query(path: string): XML.Element | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Resolves all elements that match a simple `/`-delimited path.
|
|
125
|
+
*
|
|
126
|
+
* @param path - Path such as `channel/item/title` or `rss/channel`
|
|
127
|
+
* @returns All matching elements in document order
|
|
128
|
+
*/
|
|
129
|
+
queryAll(path: string): XML.Element[];
|
|
130
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides the XML class and error types for parsing, traversing, and
|
|
3
|
+
* serializing XML documents.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { failure, success } from "@sdxc/result";
|
|
9
|
+
import { cloneDeclaration } from "./lib/clone-declaration.js";
|
|
10
|
+
import { cloneElement } from "./lib/clone-element.js";
|
|
11
|
+
import { parseDocument } from "./lib/parse-document.js";
|
|
12
|
+
import { stringifyDocument } from "./lib/stringify-document.js";
|
|
13
|
+
import { collectInElement, findInElement, normalizePath, queryFromElements, startsWithRoot, } from "./lib/traversal.js";
|
|
14
|
+
/**
|
|
15
|
+
* Signals that XML source could not be converted into the package tree format.
|
|
16
|
+
*/
|
|
17
|
+
export class XMLParseError extends Error {
|
|
18
|
+
name = "XMLParseError";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Signals that an XML tree could not be serialized into a valid XML string.
|
|
22
|
+
*/
|
|
23
|
+
export class XMLStringifyError extends Error {
|
|
24
|
+
name = "XMLStringifyError";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wraps one parsed XML document and provides traversal and serialization helpers.
|
|
28
|
+
*/
|
|
29
|
+
export class XML {
|
|
30
|
+
#declaration;
|
|
31
|
+
#root;
|
|
32
|
+
/**
|
|
33
|
+
* Stores one XML document instance around a declaration and root element.
|
|
34
|
+
*
|
|
35
|
+
* @param document - The plain XML document data to wrap
|
|
36
|
+
*/
|
|
37
|
+
constructor(document) {
|
|
38
|
+
this.#declaration = cloneDeclaration(document.declaration);
|
|
39
|
+
this.#root = cloneElement(document.root);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parses XML into an `XML` instance.
|
|
43
|
+
*
|
|
44
|
+
* @param source - Raw XML text to parse
|
|
45
|
+
* @returns A Result containing an `XML` instance or a parse error
|
|
46
|
+
*/
|
|
47
|
+
static parse(source) {
|
|
48
|
+
let result = parseDocument(source);
|
|
49
|
+
if (result.status === "failure")
|
|
50
|
+
return failure(new XMLParseError(result.error.message));
|
|
51
|
+
return success(new XML(result.data));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Serializes an XML instance, plain document data, or a root element into XML text.
|
|
55
|
+
*
|
|
56
|
+
* @param input - The XML instance, document data, or root element to serialize
|
|
57
|
+
* @returns A Result containing the XML string or a serialization error
|
|
58
|
+
*/
|
|
59
|
+
static stringify(input) {
|
|
60
|
+
let document = toDocument(input);
|
|
61
|
+
if (document.status === "failure")
|
|
62
|
+
return document;
|
|
63
|
+
let result = stringifyDocument(document.data);
|
|
64
|
+
if (result.status === "failure") {
|
|
65
|
+
return failure(new XMLStringifyError(result.error.message));
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Exposes the XML declaration as cloned data so callers cannot mutate internals.
|
|
71
|
+
*/
|
|
72
|
+
get declaration() {
|
|
73
|
+
return cloneDeclaration(this.#declaration);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Exposes the root element as cloned data so callers cannot mutate internals.
|
|
77
|
+
*/
|
|
78
|
+
get root() {
|
|
79
|
+
return cloneElement(this.#root);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the wrapped XML document as plain serializable data.
|
|
83
|
+
*/
|
|
84
|
+
toJSON() {
|
|
85
|
+
return {
|
|
86
|
+
declaration: cloneDeclaration(this.#declaration),
|
|
87
|
+
root: cloneElement(this.#root),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Serializes the current XML instance into a string.
|
|
92
|
+
*/
|
|
93
|
+
toString() {
|
|
94
|
+
let result = stringifyDocument(this.toJSON());
|
|
95
|
+
if (result.status === "failure")
|
|
96
|
+
throw new XMLStringifyError(result.error.message);
|
|
97
|
+
return result.data;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Returns the first element in the document tree that matches the predicate.
|
|
101
|
+
*
|
|
102
|
+
* @param predicate - Receives each visited element in depth-first order
|
|
103
|
+
* @returns The first matching element, if one exists
|
|
104
|
+
*/
|
|
105
|
+
find(predicate) {
|
|
106
|
+
return findInElement(this.#root, predicate);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns every element in the document tree that matches the predicate.
|
|
110
|
+
*
|
|
111
|
+
* @param predicate - Receives each visited element in depth-first order
|
|
112
|
+
* @returns All matching elements in traversal order
|
|
113
|
+
*/
|
|
114
|
+
findAll(predicate) {
|
|
115
|
+
let matches = [];
|
|
116
|
+
collectInElement(this.#root, predicate, matches);
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolves the first element that matches a simple `/`-delimited path.
|
|
121
|
+
*
|
|
122
|
+
* @param path - Path such as `channel/item/title` or `rss/channel`
|
|
123
|
+
* @returns The first matching element, if one exists
|
|
124
|
+
*/
|
|
125
|
+
query(path) {
|
|
126
|
+
return this.queryAll(path).at(0);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolves all elements that match a simple `/`-delimited path.
|
|
130
|
+
*
|
|
131
|
+
* @param path - Path such as `channel/item/title` or `rss/channel`
|
|
132
|
+
* @returns All matching elements in document order
|
|
133
|
+
*/
|
|
134
|
+
queryAll(path) {
|
|
135
|
+
let segments = normalizePath(path);
|
|
136
|
+
if (segments.length === 0)
|
|
137
|
+
return [];
|
|
138
|
+
let roots = startsWithRoot(segments, this.#root.name)
|
|
139
|
+
? queryFromElements([this.#root], segments.slice(1))
|
|
140
|
+
: queryFromElements([this.#root], segments);
|
|
141
|
+
return roots.map(cloneElement);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Recognizes the element shape, which is what tells a bare root element apart from
|
|
146
|
+
* the document that holds one, and what a value arriving from JavaScript is checked
|
|
147
|
+
* against before anything reads a name off it.
|
|
148
|
+
*/
|
|
149
|
+
function isElement(value) {
|
|
150
|
+
if (typeof value !== "object" || value === null)
|
|
151
|
+
return false;
|
|
152
|
+
return typeof value.name === "string";
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Reads the document out of everything `stringify` accepts, wrapping a bare root
|
|
156
|
+
* element in the document that holds it. An input carrying no root element is the
|
|
157
|
+
* failure the Result promises, rather than a throw from deeper in serialization.
|
|
158
|
+
*/
|
|
159
|
+
function toDocument(input) {
|
|
160
|
+
if (input instanceof XML)
|
|
161
|
+
return success(input.toJSON());
|
|
162
|
+
if (isElement(input))
|
|
163
|
+
return success({ root: input });
|
|
164
|
+
if (isElement(input.root))
|
|
165
|
+
return success(input);
|
|
166
|
+
return failure(new XMLStringifyError("Expected a document carrying a root element, or the element itself."));
|
|
167
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a helper for cloning XML declaration data.
|
|
3
|
+
*
|
|
4
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
5
|
+
* @copyright Sergio Xalambrí 2026
|
|
6
|
+
*/
|
|
7
|
+
import type { XML } from "../index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Clones declaration data so external code cannot mutate the stored value.
|
|
10
|
+
*
|
|
11
|
+
* @param declaration - The declaration data to clone
|
|
12
|
+
* @returns A shallow clone of the declaration or `undefined`
|
|
13
|
+
*/
|
|
14
|
+
export declare function cloneDeclaration(declaration?: XML.Declaration): XML.Declaration | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a helper for cloning XML declaration data.
|
|
3
|
+
*
|
|
4
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
5
|
+
* @copyright Sergio Xalambrí 2026
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Clones declaration data so external code cannot mutate the stored value.
|
|
9
|
+
*
|
|
10
|
+
* @param declaration - The declaration data to clone
|
|
11
|
+
* @returns A shallow clone of the declaration or `undefined`
|
|
12
|
+
*/
|
|
13
|
+
export function cloneDeclaration(declaration) {
|
|
14
|
+
if (!declaration)
|
|
15
|
+
return undefined;
|
|
16
|
+
return structuredClone(declaration);
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively clones XML element trees.
|
|
3
|
+
*
|
|
4
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
5
|
+
* @copyright Sergio Xalambrí 2026
|
|
6
|
+
*/
|
|
7
|
+
import type { XML } from "../index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Clones one XML element tree recursively.
|
|
10
|
+
*
|
|
11
|
+
* @param element - The element tree to clone
|
|
12
|
+
* @returns A deep clone of the provided element
|
|
13
|
+
*/
|
|
14
|
+
export declare function cloneElement(element: XML.Element): XML.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively clones XML element trees.
|
|
3
|
+
*
|
|
4
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
5
|
+
* @copyright Sergio Xalambrí 2026
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Clones one XML element tree recursively.
|
|
9
|
+
*
|
|
10
|
+
* @param element - The element tree to clone
|
|
11
|
+
* @returns A deep clone of the provided element
|
|
12
|
+
*/
|
|
13
|
+
export function cloneElement(element) {
|
|
14
|
+
let attributes = element.attributes ? { ...element.attributes } : undefined;
|
|
15
|
+
let children = element.children?.map((child) => {
|
|
16
|
+
if (typeof child === "string")
|
|
17
|
+
return child;
|
|
18
|
+
return cloneElement(child);
|
|
19
|
+
});
|
|
20
|
+
return { name: element.name, attributes, children };
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the character references and named entities that appear inside text nodes
|
|
3
|
+
* and attribute values, covering XML's predefines, numeric forms and the XHTML entity
|
|
4
|
+
* sets, so a caller always receives text that is fully decoded.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Result } from "@sdxc/result";
|
|
10
|
+
/**
|
|
11
|
+
* Replaces every reference in one run of parsed character data.
|
|
12
|
+
*
|
|
13
|
+
* @param value - Raw text taken straight from the source, still encoded
|
|
14
|
+
* @returns A Result with the decoded text, or the first reference that failed
|
|
15
|
+
*/
|
|
16
|
+
export declare function decodeEntities(value: string): Result<string, Error>;
|