@sdxc/xml 2026.9.15 → 2026.9.23
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 +23 -6
- package/dist/index.d.ts +19 -1
- package/dist/index.js +3 -2
- package/dist/lib/parse-document.d.ts +2 -1
- package/dist/lib/parse-document.js +35 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ let xml = result.data;
|
|
|
40
40
|
xml.query("channel/title"); // { name: "title", children: ["Feed"] }
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Whitespace-only text nodes are indentation rather than content, so the parser
|
|
44
|
-
a traversal never has to step over them.
|
|
43
|
+
Whitespace-only text nodes are indentation rather than content, so by default the parser
|
|
44
|
+
drops them and a traversal never has to step over them.
|
|
45
45
|
|
|
46
46
|
### Traverse A Document
|
|
47
47
|
|
|
@@ -107,11 +107,27 @@ let source = copy.toString();
|
|
|
107
107
|
XML.stringify(json); // the same text, as a Result
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Keep The Source's Whitespace
|
|
111
|
+
|
|
112
|
+
By default a run of character data holding nothing but whitespace is indentation, and stays
|
|
113
|
+
out of the tree, so traversal sees elements and the text that carries meaning. A caller that
|
|
114
|
+
hashes a document needs the opposite, because a byte of indentation changes the digest:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
let xml = XML.parse("<r>\n\t<t>x</t>\n</r>", { whitespace: "preserve" });
|
|
118
|
+
|
|
119
|
+
// children: ["\n\t", { name: "t", children: ["x"] }, "\n"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Both modes resolve references first, so ` ` counts as the space it decodes to, and both
|
|
123
|
+
allow whitespace around the root element.
|
|
124
|
+
|
|
110
125
|
## API
|
|
111
126
|
|
|
112
|
-
### `XML.parse(source: string): Result<XML, XMLParseError>`
|
|
127
|
+
### `XML.parse(source: string, options?: XML.ParseOptions): Result<XML, XMLParseError>`
|
|
113
128
|
|
|
114
|
-
Parses XML text into an `XML` instance.
|
|
129
|
+
Parses XML text into an `XML` instance. `options.whitespace` is `"collapse"` by default and
|
|
130
|
+
`"preserve"` to keep whitespace-only text and CDATA in the tree.
|
|
115
131
|
|
|
116
132
|
### `XML.stringify(input: XML | XML.Input): Result<string, XMLStringifyError>`
|
|
117
133
|
|
|
@@ -174,8 +190,9 @@ prefix with no namespace declared in scope.
|
|
|
174
190
|
### Types
|
|
175
191
|
|
|
176
192
|
Every public type lives in the `XML` namespace: `XML.Declaration`, `XML.Element`,
|
|
177
|
-
`XML.Node`, `XML.Document`, `XML.Input`,
|
|
178
|
-
`stringify` accepts: a whole `XML.Document` or the
|
|
193
|
+
`XML.Node`, `XML.Document`, `XML.Input`, `XML.Predicate`, `XML.Whitespace`, and
|
|
194
|
+
`XML.ParseOptions`. `XML.Input` is what `stringify` accepts: a whole `XML.Document` or the
|
|
195
|
+
root `XML.Element` alone.
|
|
179
196
|
|
|
180
197
|
```typescript
|
|
181
198
|
import type { XML } from "@sdxc/xml";
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,23 @@ export declare namespace XML {
|
|
|
57
57
|
* Checks one element while traversing an XML tree.
|
|
58
58
|
*/
|
|
59
59
|
type Predicate = (element: Element) => boolean;
|
|
60
|
+
/**
|
|
61
|
+
* How whitespace-only character data is treated while parsing. `collapse`
|
|
62
|
+
* leaves indentation out of the tree; `preserve` keeps every run, which is
|
|
63
|
+
* what a caller digesting the document needs, since spacing changes the hash.
|
|
64
|
+
*/
|
|
65
|
+
type Whitespace = "collapse" | "preserve";
|
|
66
|
+
/**
|
|
67
|
+
* How one `parse` call reads its source.
|
|
68
|
+
*/
|
|
69
|
+
interface ParseOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Whether whitespace-only text and CDATA reach the tree.
|
|
72
|
+
*
|
|
73
|
+
* @default "collapse"
|
|
74
|
+
*/
|
|
75
|
+
whitespace?: Whitespace;
|
|
76
|
+
}
|
|
60
77
|
}
|
|
61
78
|
/**
|
|
62
79
|
* Wraps one parsed XML document and provides traversal and serialization helpers.
|
|
@@ -73,9 +90,10 @@ export declare class XML {
|
|
|
73
90
|
* Parses XML into an `XML` instance.
|
|
74
91
|
*
|
|
75
92
|
* @param source - Raw XML text to parse
|
|
93
|
+
* @param options - Reading choices; the default drops indentation
|
|
76
94
|
* @returns A Result containing an `XML` instance or a parse error
|
|
77
95
|
*/
|
|
78
|
-
static parse(source: string): Result<XML, XMLParseError>;
|
|
96
|
+
static parse(source: string, options?: XML.ParseOptions): Result<XML, XMLParseError>;
|
|
79
97
|
/**
|
|
80
98
|
* Serializes an XML instance, plain document data, or a root element into XML text.
|
|
81
99
|
*
|
package/dist/index.js
CHANGED
|
@@ -42,10 +42,11 @@ export class XML {
|
|
|
42
42
|
* Parses XML into an `XML` instance.
|
|
43
43
|
*
|
|
44
44
|
* @param source - Raw XML text to parse
|
|
45
|
+
* @param options - Reading choices; the default drops indentation
|
|
45
46
|
* @returns A Result containing an `XML` instance or a parse error
|
|
46
47
|
*/
|
|
47
|
-
static parse(source) {
|
|
48
|
-
let result = parseDocument(source);
|
|
48
|
+
static parse(source, options = {}) {
|
|
49
|
+
let result = parseDocument(source, options.whitespace ?? "collapse");
|
|
49
50
|
if (result.status === "failure")
|
|
50
51
|
return failure(new XMLParseError(result.error.message));
|
|
51
52
|
return success(new XML(result.data));
|
|
@@ -12,6 +12,7 @@ import type { XML } from "../index.js";
|
|
|
12
12
|
* Parses XML into plain document data.
|
|
13
13
|
*
|
|
14
14
|
* @param source - Raw XML text to parse
|
|
15
|
+
* @param whitespace - Whether whitespace-only character data reaches the tree
|
|
15
16
|
* @returns A Result containing XML document data or an error
|
|
16
17
|
*/
|
|
17
|
-
export declare function parseDocument(source: string): Result<XML.Document, Error>;
|
|
18
|
+
export declare function parseDocument(source: string, whitespace?: XML.Whitespace): Result<XML.Document, Error>;
|
|
@@ -21,33 +21,35 @@ const WHITESPACE_PATTERN = /\s/;
|
|
|
21
21
|
* Parses XML into plain document data.
|
|
22
22
|
*
|
|
23
23
|
* @param source - Raw XML text to parse
|
|
24
|
+
* @param whitespace - Whether whitespace-only character data reaches the tree
|
|
24
25
|
* @returns A Result containing XML document data or an error
|
|
25
26
|
*/
|
|
26
|
-
export function parseDocument(source) {
|
|
27
|
-
let root = parseRoot(source);
|
|
27
|
+
export function parseDocument(source, whitespace = "collapse") {
|
|
28
|
+
let root = parseRoot(source, whitespace);
|
|
28
29
|
if (root.status === "failure")
|
|
29
30
|
return root;
|
|
30
31
|
return success({ declaration: parseDeclaration(source), root: root.data });
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Walks the source once, building the element tree on a stack of open elements.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* Under `collapse` the tree holds elements and meaningful text alone, which is
|
|
36
|
+
* what feed traversal wants; under `preserve` it holds the source's own spacing.
|
|
36
37
|
*/
|
|
37
|
-
function parseRoot(source) {
|
|
38
|
+
function parseRoot(source, whitespace) {
|
|
38
39
|
let stack = [];
|
|
39
40
|
let root;
|
|
40
41
|
let index = 0;
|
|
41
42
|
while (index < source.length) {
|
|
42
43
|
if (source[index] !== "<") {
|
|
43
|
-
let text = readText(source, index);
|
|
44
|
+
let text = readText(source, index, whitespace);
|
|
44
45
|
if (text.status === "failure")
|
|
45
46
|
return text;
|
|
46
47
|
let parent = stack.at(-1);
|
|
47
48
|
if (parent)
|
|
48
49
|
parent.children?.push(...text.data.value);
|
|
49
|
-
else if (text.data.value
|
|
50
|
-
return failure(strayContent(root, text.data.value[0]));
|
|
50
|
+
else if (outsideRoot(text.data.value)) {
|
|
51
|
+
return failure(strayContent(root, text.data.value[0] ?? ""));
|
|
52
|
+
}
|
|
51
53
|
index = text.data.next;
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
@@ -59,7 +61,7 @@ function parseRoot(source) {
|
|
|
59
61
|
continue;
|
|
60
62
|
}
|
|
61
63
|
if (source.startsWith("<![CDATA[", index)) {
|
|
62
|
-
let section = readCDATA(source, index);
|
|
64
|
+
let section = readCDATA(source, index, whitespace);
|
|
63
65
|
if (section.status === "failure")
|
|
64
66
|
return section;
|
|
65
67
|
stack.at(-1)?.children?.push(...section.data.value);
|
|
@@ -121,30 +123,46 @@ function parseRoot(source) {
|
|
|
121
123
|
return success(root);
|
|
122
124
|
}
|
|
123
125
|
/**
|
|
124
|
-
* Reads the character data up to the next `<`, resolving references
|
|
125
|
-
* run
|
|
126
|
+
* Reads the character data up to the next `<`, resolving references before the
|
|
127
|
+
* run is weighed, so ` ` counts as the space it decodes to either way.
|
|
126
128
|
*/
|
|
127
|
-
function readText(source, index) {
|
|
129
|
+
function readText(source, index, whitespace) {
|
|
128
130
|
let end = source.indexOf("<", index);
|
|
129
131
|
let stop = end === -1 ? source.length : end;
|
|
130
132
|
let decoded = decodeEntities(source.slice(index, stop));
|
|
131
133
|
if (decoded.status === "failure")
|
|
132
134
|
return decoded;
|
|
133
|
-
|
|
134
|
-
return success({ value: kept, next: stop });
|
|
135
|
+
return success({ value: keep(decoded.data, whitespace), next: stop });
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
137
138
|
* Reads a CDATA section, whose content reaches the tree verbatim because CDATA
|
|
138
139
|
* exists precisely to carry markup as literal text.
|
|
139
140
|
*/
|
|
140
|
-
function readCDATA(source, index) {
|
|
141
|
+
function readCDATA(source, index, whitespace) {
|
|
141
142
|
let start = index + "<![CDATA[".length;
|
|
142
143
|
let end = source.indexOf("]]>", start);
|
|
143
144
|
if (end === -1)
|
|
144
145
|
return failure(new Error("Unterminated CDATA section"));
|
|
145
146
|
let content = source.slice(start, end);
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
return success({ value: keep(content, whitespace), next: end + "]]>".length });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reports character data that sits beside the root rather than inside it. The
|
|
151
|
+
* prolog and the epilog may hold whitespace, so only a run carrying something
|
|
152
|
+
* else is content XML puts nowhere.
|
|
153
|
+
*/
|
|
154
|
+
function outsideRoot(text) {
|
|
155
|
+
return text.some((run) => run.trim().length > 0);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Decides whether one run of character data reaches the tree. A run of nothing
|
|
159
|
+
* but whitespace is indentation under `collapse` and part of what a digest
|
|
160
|
+
* covers under `preserve`, so only the caller's mode can tell them apart.
|
|
161
|
+
*/
|
|
162
|
+
function keep(text, whitespace) {
|
|
163
|
+
if (whitespace === "preserve")
|
|
164
|
+
return text.length > 0 ? [text] : [];
|
|
165
|
+
return text.trim().length > 0 ? [text] : [];
|
|
148
166
|
}
|
|
149
167
|
/**
|
|
150
168
|
* Skips past a construct the tree leaves out, such as a comment or a processing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/xml",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.23",
|
|
4
4
|
"description": "XML parser and serializer for RSS-style feeds",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@sdxc/result": "2026.9.15"
|
|
12
12
|
},
|
|
13
|
-
"gitHead": "
|
|
13
|
+
"gitHead": "00c5691b2e444a348956d1879ab676b07d3f9c8f",
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|