@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 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 drops them and
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 `&#32;` 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`, and `XML.Predicate`. `XML.Input` is what
178
- `stringify` accepts: a whole `XML.Document` or the root `XML.Element` alone.
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
- * Text and CDATA that hold only whitespace are dropped, which keeps indentation
35
- * out of the tree and leaves feed traversal working on elements alone.
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[0])
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 first so a
125
- * run that decodes to nothing but whitespace is dropped along with plain indentation.
126
+ * Reads the character data up to the next `<`, resolving references before the
127
+ * run is weighed, so `&#32;` 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
- let kept = decoded.data.trim().length > 0 ? [decoded.data] : [];
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
- let kept = content.trim().length > 0 ? [content] : [];
147
- return success({ value: kept, next: end + "]]>".length });
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.15",
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": "d56f75a79171aab3be101d8fa624d51972cd6028",
13
+ "gitHead": "00c5691b2e444a348956d1879ab676b07d3f9c8f",
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  },