@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.
@@ -0,0 +1,90 @@
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 { failure, success } from "@sdxc/result";
10
+ import { HTML_ENTITIES } from "./html-entities.js";
11
+ /**
12
+ * Matches a complete reference: an ampersand, a run of reference characters and
13
+ * a semicolon. Prose containing a loose ampersand therefore reads as text.
14
+ */
15
+ const REFERENCE_PATTERN = /&([^;&<\s]*);/g;
16
+ const DECIMAL_PATTERN = /^#([0-9]+)$/;
17
+ const HEXADECIMAL_PATTERN = /^#[xX]([0-9a-fA-F]+)$/;
18
+ const MAXIMUM_CODE_POINT = 0x10_ff_ff;
19
+ const FIRST_SURROGATE = 0xd8_00;
20
+ const LAST_SURROGATE = 0xdf_ff;
21
+ /**
22
+ * The five entities XML predefines, and so the complete set available to a
23
+ * document that declares its own entities through a DTD.
24
+ */
25
+ const PREDEFINED_ENTITIES = {
26
+ lt: "<",
27
+ gt: ">",
28
+ amp: "&",
29
+ quot: '"',
30
+ apos: "'",
31
+ };
32
+ /**
33
+ * Replaces every reference in one run of parsed character data.
34
+ *
35
+ * @param value - Raw text taken straight from the source, still encoded
36
+ * @returns A Result with the decoded text, or the first reference that failed
37
+ */
38
+ export function decodeEntities(value) {
39
+ if (!value.includes("&"))
40
+ return success(value);
41
+ let output = "";
42
+ let cursor = 0;
43
+ REFERENCE_PATTERN.lastIndex = 0;
44
+ let match = REFERENCE_PATTERN.exec(value);
45
+ while (match) {
46
+ let resolved = resolveReference(match[1] ?? "");
47
+ if (resolved.status === "failure")
48
+ return resolved;
49
+ output += value.slice(cursor, match.index) + resolved.data;
50
+ cursor = match.index + match[0].length;
51
+ match = REFERENCE_PATTERN.exec(value);
52
+ }
53
+ return success(output + value.slice(cursor));
54
+ }
55
+ /**
56
+ * Resolves the text between `&` and `;` into the character it stands for.
57
+ *
58
+ * The XHTML sets are consulted after the predefines and after numeric references, so
59
+ * a document declaring none of them still decodes the prose feeds are written in.
60
+ */
61
+ function resolveReference(reference) {
62
+ let predefined = PREDEFINED_ENTITIES[reference];
63
+ if (predefined)
64
+ return success(predefined);
65
+ let decimal = reference.match(DECIMAL_PATTERN);
66
+ if (decimal?.[1])
67
+ return resolveCodePoint(reference, Number.parseInt(decimal[1], 10));
68
+ let hexadecimal = reference.match(HEXADECIMAL_PATTERN);
69
+ if (hexadecimal?.[1])
70
+ return resolveCodePoint(reference, Number.parseInt(hexadecimal[1], 16));
71
+ if (reference.startsWith("#")) {
72
+ return failure(new Error(`entity not matching Reference production: &${reference};`));
73
+ }
74
+ let named = HTML_ENTITIES[reference];
75
+ if (named)
76
+ return success(named);
77
+ return failure(new Error(`entity not found:&${reference};`));
78
+ }
79
+ /**
80
+ * Turns a numeric reference into its character, refusing the values that cannot
81
+ * stand alone in a string: unpaired surrogates and anything past the last plane.
82
+ */
83
+ function resolveCodePoint(reference, codePoint) {
84
+ let outOfRange = codePoint === 0 || codePoint > MAXIMUM_CODE_POINT;
85
+ let surrogate = codePoint >= FIRST_SURROGATE && codePoint <= LAST_SURROGATE;
86
+ if (outOfRange || surrogate) {
87
+ return failure(new Error(`entity not matching Reference production: &${reference};`));
88
+ }
89
+ return success(String.fromCodePoint(codePoint));
90
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Escapes the characters that carry meaning as markup when XML text and
3
+ * attribute values are serialized, so the output re-parses into the tree it came
4
+ * from.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Escapes a text node. `>` is escaped alongside `<` and `&` so that a literal
11
+ * `]]>` in the content reaches the reader as text.
12
+ *
13
+ * @param value - The text node content
14
+ * @returns The escaped text
15
+ */
16
+ export declare function escapeText(value: string): string;
17
+ /**
18
+ * Escapes an attribute value. Tabs and line breaks become numeric references so
19
+ * they survive the round trip intact, since a parser reads the literal
20
+ * characters as spaces.
21
+ *
22
+ * @param value - The attribute value
23
+ * @returns The escaped value, safe to place between double quotes
24
+ */
25
+ export declare function escapeAttribute(value: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Escapes the characters that carry meaning as markup when XML text and
3
+ * attribute values are serialized, so the output re-parses into the tree it came
4
+ * from.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ const TEXT_PATTERN = /[<>&]/g;
10
+ const ATTRIBUTE_PATTERN = /[<>&"\t\n\r]/g;
11
+ const NAMED_REPLACEMENTS = {
12
+ "<": "&lt;",
13
+ ">": "&gt;",
14
+ "&": "&amp;",
15
+ '"': "&quot;",
16
+ };
17
+ /**
18
+ * Escapes a text node. `>` is escaped alongside `<` and `&` so that a literal
19
+ * `]]>` in the content reaches the reader as text.
20
+ *
21
+ * @param value - The text node content
22
+ * @returns The escaped text
23
+ */
24
+ export function escapeText(value) {
25
+ return value.replace(TEXT_PATTERN, replaceCharacter);
26
+ }
27
+ /**
28
+ * Escapes an attribute value. Tabs and line breaks become numeric references so
29
+ * they survive the round trip intact, since a parser reads the literal
30
+ * characters as spaces.
31
+ *
32
+ * @param value - The attribute value
33
+ * @returns The escaped value, safe to place between double quotes
34
+ */
35
+ export function escapeAttribute(value) {
36
+ return value.replace(ATTRIBUTE_PATTERN, replaceCharacter);
37
+ }
38
+ /**
39
+ * Maps one character to its entity, falling back to a decimal numeric reference
40
+ * for the whitespace that attribute values escape.
41
+ */
42
+ function replaceCharacter(character) {
43
+ return NAMED_REPLACEMENTS[character] ?? `&#${character.charCodeAt(0)};`;
44
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The named character entities the three XHTML 1.0 entity sets declare, so text
3
+ * carrying `&nbsp;` or `&mdash;` decodes instead of failing the document. Feeds
4
+ * are authored by publishing tools that emit these without declaring a DTD.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Every entity from `xhtml-lat1`, `xhtml-special` and `xhtml-symbol`, mapped to the
11
+ * character it stands for. Values are written as escapes throughout because a third
12
+ * of the set is whitespace or zero-width and would otherwise be invisible in review.
13
+ *
14
+ * The five XML predefines its own parser resolves are absent here on purpose: they
15
+ * are the document's regardless of any DTD, so they resolve before this table is read.
16
+ */
17
+ export declare const HTML_ENTITIES: Record<string, string>;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The named character entities the three XHTML 1.0 entity sets declare, so text
3
+ * carrying `&nbsp;` or `&mdash;` decodes instead of failing the document. Feeds
4
+ * are authored by publishing tools that emit these without declaring a DTD.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Every entity from `xhtml-lat1`, `xhtml-special` and `xhtml-symbol`, mapped to the
11
+ * character it stands for. Values are written as escapes throughout because a third
12
+ * of the set is whitespace or zero-width and would otherwise be invisible in review.
13
+ *
14
+ * The five XML predefines its own parser resolves are absent here on purpose: they
15
+ * are the document's regardless of any DTD, so they resolve before this table is read.
16
+ */
17
+ export const HTML_ENTITIES = {
18
+ /** xhtml-lat1: the Latin-1 supplement, U+00A0 through U+00FF. */
19
+ nbsp: " ",
20
+ iexcl: "¡",
21
+ cent: "¢",
22
+ pound: "£",
23
+ curren: "¤",
24
+ yen: "¥",
25
+ brvbar: "¦",
26
+ sect: "§",
27
+ uml: "¨",
28
+ copy: "©",
29
+ ordf: "ª",
30
+ laquo: "«",
31
+ not: "¬",
32
+ shy: "­",
33
+ reg: "®",
34
+ macr: "¯",
35
+ deg: "°",
36
+ plusmn: "±",
37
+ sup2: "²",
38
+ sup3: "³",
39
+ acute: "´",
40
+ micro: "µ",
41
+ para: "¶",
42
+ middot: "·",
43
+ cedil: "¸",
44
+ sup1: "¹",
45
+ ordm: "º",
46
+ raquo: "»",
47
+ frac14: "¼",
48
+ frac12: "½",
49
+ frac34: "¾",
50
+ iquest: "¿",
51
+ Agrave: "À",
52
+ Aacute: "Á",
53
+ Acirc: "Â",
54
+ Atilde: "Ã",
55
+ Auml: "Ä",
56
+ Aring: "Å",
57
+ AElig: "Æ",
58
+ Ccedil: "Ç",
59
+ Egrave: "È",
60
+ Eacute: "É",
61
+ Ecirc: "Ê",
62
+ Euml: "Ë",
63
+ Igrave: "Ì",
64
+ Iacute: "Í",
65
+ Icirc: "Î",
66
+ Iuml: "Ï",
67
+ ETH: "Ð",
68
+ Ntilde: "Ñ",
69
+ Ograve: "Ò",
70
+ Oacute: "Ó",
71
+ Ocirc: "Ô",
72
+ Otilde: "Õ",
73
+ Ouml: "Ö",
74
+ times: "×",
75
+ Oslash: "Ø",
76
+ Ugrave: "Ù",
77
+ Uacute: "Ú",
78
+ Ucirc: "Û",
79
+ Uuml: "Ü",
80
+ Yacute: "Ý",
81
+ THORN: "Þ",
82
+ szlig: "ß",
83
+ agrave: "à",
84
+ aacute: "á",
85
+ acirc: "â",
86
+ atilde: "ã",
87
+ auml: "ä",
88
+ aring: "å",
89
+ aelig: "æ",
90
+ ccedil: "ç",
91
+ egrave: "è",
92
+ eacute: "é",
93
+ ecirc: "ê",
94
+ euml: "ë",
95
+ igrave: "ì",
96
+ iacute: "í",
97
+ icirc: "î",
98
+ iuml: "ï",
99
+ eth: "ð",
100
+ ntilde: "ñ",
101
+ ograve: "ò",
102
+ oacute: "ó",
103
+ ocirc: "ô",
104
+ otilde: "õ",
105
+ ouml: "ö",
106
+ divide: "÷",
107
+ oslash: "ø",
108
+ ugrave: "ù",
109
+ uacute: "ú",
110
+ ucirc: "û",
111
+ uuml: "ü",
112
+ yacute: "ý",
113
+ thorn: "þ",
114
+ yuml: "ÿ",
115
+ /** xhtml-special: Latin Extended-A, spacing modifiers, punctuation and the euro. */
116
+ OElig: "Œ",
117
+ oelig: "œ",
118
+ Scaron: "Š",
119
+ scaron: "š",
120
+ Yuml: "Ÿ",
121
+ circ: "ˆ",
122
+ tilde: "˜",
123
+ ensp: " ",
124
+ emsp: " ",
125
+ thinsp: " ",
126
+ zwnj: "‌",
127
+ zwj: "‍",
128
+ lrm: "‎",
129
+ rlm: "‏",
130
+ ndash: "–",
131
+ mdash: "—",
132
+ lsquo: "‘",
133
+ rsquo: "’",
134
+ sbquo: "‚",
135
+ ldquo: "“",
136
+ rdquo: "”",
137
+ bdquo: "„",
138
+ dagger: "†",
139
+ Dagger: "‡",
140
+ permil: "‰",
141
+ lsaquo: "‹",
142
+ rsaquo: "›",
143
+ euro: "€",
144
+ /** xhtml-symbol: Greek, general punctuation, arrows and mathematical operators. */
145
+ fnof: "ƒ",
146
+ Alpha: "Α",
147
+ Beta: "Β",
148
+ Gamma: "Γ",
149
+ Delta: "Δ",
150
+ Epsilon: "Ε",
151
+ Zeta: "Ζ",
152
+ Eta: "Η",
153
+ Theta: "Θ",
154
+ Iota: "Ι",
155
+ Kappa: "Κ",
156
+ Lambda: "Λ",
157
+ Mu: "Μ",
158
+ Nu: "Ν",
159
+ Xi: "Ξ",
160
+ Omicron: "Ο",
161
+ Pi: "Π",
162
+ Rho: "Ρ",
163
+ Sigma: "Σ",
164
+ Tau: "Τ",
165
+ Upsilon: "Υ",
166
+ Phi: "Φ",
167
+ Chi: "Χ",
168
+ Psi: "Ψ",
169
+ Omega: "Ω",
170
+ alpha: "α",
171
+ beta: "β",
172
+ gamma: "γ",
173
+ delta: "δ",
174
+ epsilon: "ε",
175
+ zeta: "ζ",
176
+ eta: "η",
177
+ theta: "θ",
178
+ iota: "ι",
179
+ kappa: "κ",
180
+ lambda: "λ",
181
+ mu: "μ",
182
+ nu: "ν",
183
+ xi: "ξ",
184
+ omicron: "ο",
185
+ pi: "π",
186
+ rho: "ρ",
187
+ sigmaf: "ς",
188
+ sigma: "σ",
189
+ tau: "τ",
190
+ upsilon: "υ",
191
+ phi: "φ",
192
+ chi: "χ",
193
+ psi: "ψ",
194
+ omega: "ω",
195
+ thetasym: "ϑ",
196
+ upsih: "ϒ",
197
+ piv: "ϖ",
198
+ bull: "•",
199
+ hellip: "…",
200
+ prime: "′",
201
+ Prime: "″",
202
+ oline: "‾",
203
+ frasl: "⁄",
204
+ weierp: "℘",
205
+ image: "ℑ",
206
+ real: "ℜ",
207
+ trade: "™",
208
+ alefsym: "ℵ",
209
+ larr: "←",
210
+ uarr: "↑",
211
+ rarr: "→",
212
+ darr: "↓",
213
+ harr: "↔",
214
+ crarr: "↵",
215
+ lArr: "⇐",
216
+ uArr: "⇑",
217
+ rArr: "⇒",
218
+ dArr: "⇓",
219
+ hArr: "⇔",
220
+ forall: "∀",
221
+ part: "∂",
222
+ exist: "∃",
223
+ empty: "∅",
224
+ nabla: "∇",
225
+ isin: "∈",
226
+ notin: "∉",
227
+ ni: "∋",
228
+ prod: "∏",
229
+ sum: "∑",
230
+ minus: "−",
231
+ lowast: "∗",
232
+ radic: "√",
233
+ prop: "∝",
234
+ infin: "∞",
235
+ ang: "∠",
236
+ and: "∧",
237
+ or: "∨",
238
+ cap: "∩",
239
+ cup: "∪",
240
+ int: "∫",
241
+ there4: "∴",
242
+ sim: "∼",
243
+ cong: "≅",
244
+ asymp: "≈",
245
+ ne: "≠",
246
+ equiv: "≡",
247
+ le: "≤",
248
+ ge: "≥",
249
+ sub: "⊂",
250
+ sup: "⊃",
251
+ nsub: "⊄",
252
+ sube: "⊆",
253
+ supe: "⊇",
254
+ oplus: "⊕",
255
+ otimes: "⊗",
256
+ perp: "⊥",
257
+ sdot: "⋅",
258
+ lceil: "⌈",
259
+ rceil: "⌉",
260
+ lfloor: "⌊",
261
+ rfloor: "⌋",
262
+ lang: "〈",
263
+ rang: "〉",
264
+ loz: "◊",
265
+ spades: "♠",
266
+ clubs: "♣",
267
+ hearts: "♥",
268
+ diams: "♦",
269
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Parses XML text into plain document data by scanning the source directly, so
3
+ * the package runs anywhere JavaScript does, workerd included. Covers the subset
4
+ * RSS and similar feeds use: one root, attributes, text, CDATA, prefixed names.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { XML } from "../index.js";
11
+ /**
12
+ * Parses XML into plain document data.
13
+ *
14
+ * @param source - Raw XML text to parse
15
+ * @returns A Result containing XML document data or an error
16
+ */
17
+ export declare function parseDocument(source: string): Result<XML.Document, Error>;