@lowlighter/xml 6.0.0 → 8.0.0

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/stringify.mjs DELETED
@@ -1 +0,0 @@
1
- var t=Symbol("internal");function e(t,e){e??={},e.format??={},e.format.indent??=" ",e.format.breakline??=128;const n=e;let o="";if(o+=function(t,e){return t["~name"]??="xml",r(t,e)}(t,n),t["#instructions"])for(const e of Object.values(t["#instructions"]))for(const t of[e].flat())o+=r(t,n);t["#doctype"]&&(o+=function(t,{format:{indent:e}}){let n="";const o=l(t,arguments[1]),r=c(t,arguments[1]);if(o.length+r.length){n+="<!DOCTYPE";for(const[t]of o)n+=` ${/^[A-Za-z0-9_]+$/.test(t)?t:`"${t}"`}`;if(r.length){n+=`${e?`\n${e}`:" "}[${e?"\n":""}`;for(const t of r)n+=`${e}<!ELEMENT ${t["~name"]} (${t["#text"]})>${e?"\n":""}`;n+=`${e||""}]${e?"\n":""}`}n+=">"+(e?"\n":"")}return n}(t["#doctype"],n));const[f,...i]=c(t,n);if(!f)throw new SyntaxError("No root node detected");if(i.length)throw new SyntaxError("Multiple root node detected");return o+=a(f,{...n,depth:0}),o.trim()}function n(t){return{"~name":"~cdata","#text":t}}function o(t){return{"~name":"~comment","#text":t}}function r(t,{format:{indent:e}}){let n="";const o=l(t,arguments[1]);if(o.length){n+=`<?${t["~name"].replace(/^~/,"")}`;for(const[t,e]of o)n+=` ${t}="${e}"`;n+="?>"+(e?"\n":"")}return n}function a(t,{format:{breakline:e=0,indent:n=""},replace:o,depth:r=0}){if(o?.custom&&void 0===o.custom({name:t["~name"],key:null,value:null,node:t}))return"";let f=`${n.repeat(r)}<${t["~name"]}`;const i=l(t,arguments[1]),s=c(t,arguments[1]),u="preserve"===t["@xml:space"];for(const[t,e]of i)f+=` ${t}="${e}"`;if(s.length||"#text"in t&&t["#text"].length){const o=n&&!u&&(s.length||t["#text"].length>e-n.length*r);f+=">"+(n&&!u&&s.length?"\n":""),"#text"in t&&(o&&(f+=`\n${n.repeat(r+1)}`),f+=t["#text"],o&&(f+="\n"));for(const t of s)f+=a(t,{...arguments[1],depth:r+1});o&&(f+=n.repeat(r)),f+=`</${t["~name"]}>${n?"\n":""}`}else f+="/>"+(n?"\n":"");return f}function c(e,n){return Object.keys(e).filter((t=>/^[A-Za-z_]/.test(t))).flatMap((n=>[e[n]].flat().map((e=>{switch(!0){case null===e:return{"~name":n,"#text":""};case"object"==typeof e:{const o={...e,"~name":n};return e["~name"]?.startsWith("~")&&(o[t]=e["~name"]),o}default:return{"~name":n,"#text":`${e}`}}})))).map((e=>{if("#text"in e){const o="~cdata"===e[t],r="~comment"===e[t];e["#text"]=i(e,"#text",{...n,escape:o?[]:["<",">"]}),void 0===e["#text"]?delete e["#text"]:e["#text"]=o?`<![CDATA[${e["#text"]}]]>`:r?`\x3c!--${e["#text"]}--\x3e`:`${e["#text"]}`}return e}))}function l(t,e){return Object.entries(t).filter((([t])=>t.startsWith("@"))).map((([n])=>[n.slice(1),i(t,n,{...e,escape:['"',"'"]})])).filter((([t,e])=>void 0!==e))}var f={"&":"&amp;",'"':"&quot;","<":"&lt;",">":"&gt;","'":"&apos;"};function i(t,e,n){let o=`${t[e]}`;if(n?.escape){n?.replace?.entities&&(n.escape=Object.keys(f));for(const t of n?.escape)o=`${o}`.replaceAll(t,f[t])}return n?.replace?.custom?n.replace.custom({name:t["~name"],key:e,value:o,node:t}):o}export{n as cdata,o as comment,e as stringify};
package/stringify.ts DELETED
@@ -1,287 +0,0 @@
1
- // Imports
2
- import type { Nullable, record, rw } from "@libs/typing"
3
- import type { stringifyable, xml_document, xml_node, xml_text } from "./_types.ts"
4
- export type { Nullable, stringifyable, xml_document, xml_node, xml_text }
5
-
6
- /** XML stringifier options. */
7
- export type options = {
8
- /** Format options. */
9
- format?: {
10
- /**
11
- * Indent string (defaults to `" "`).
12
- * Set to empty string to disable indentation and enable minification.
13
- */
14
- indent?: string
15
- /** Break text node if its length is greater than this value (defaults to `128`). */
16
- breakline?: number
17
- }
18
- /** Replace options. */
19
- replace?: {
20
- /**
21
- * Force escape all XML entities.
22
- * By default, only the ones that would break the XML structure are escaped.
23
- */
24
- entities?: boolean
25
- /**
26
- * Custom replacer (this is applied after other revivals).
27
- * When it is applied on an attribute, `key` and `value` will be given.
28
- * When it is applied on a node, both `key` and `value` will be `null`.
29
- * Return `undefined` to delete either the attribute or the tag.
30
- */
31
- custom?: (args: { name: string; key: Nullable<string>; value: Nullable<string>; node: Readonly<xml_node> }) => unknown
32
- }
33
- }
34
-
35
- /** XML stringifier options (with non-nullable format options). */
36
- type _options = options & { format: NonNullable<options["format"]> }
37
-
38
- /** Internal symbol to store properties without erasing user-provided ones. */
39
- const internal = Symbol("internal")
40
-
41
- /**
42
- * Stringify an {@link xml_document} object into a XML string.
43
- *
44
- * Output can be customized using the {@link options} parameter.
45
- *
46
- * @example
47
- * ```ts
48
- * import { stringify } from "./stringify.ts"
49
- *
50
- * console.log(stringify({
51
- * "@version": "1.0",
52
- * "@standalone": "yes",
53
- * root: {
54
- * text: "hello",
55
- * array: ["world", "monde", "世界", "🌏"],
56
- * number: 42,
57
- * boolean: true,
58
- * complex: {
59
- * "@attribute": "value",
60
- * "#text": "content",
61
- * },
62
- * }
63
- * }))
64
- * ```
65
- *
66
- * @module
67
- */
68
- export function stringify(document: stringifyable, options?: options): string {
69
- options ??= {}
70
- options.format ??= {}
71
- options.format.indent ??= " "
72
- options.format.breakline ??= 128
73
- const _options = options as _options
74
- let text = ""
75
- // Add prolog
76
- text += xml_prolog(document as xml_document, _options)
77
- // Add processing instructions
78
- if (document["#instructions"]) {
79
- for (const nodes of Object.values(document["#instructions"])) {
80
- for (const node of [nodes].flat()) {
81
- text += xml_instruction(node, _options)
82
- }
83
- }
84
- }
85
- // Add doctype
86
- if (document["#doctype"]) {
87
- text += xml_doctype(document["#doctype"] as xml_node, _options)
88
- }
89
-
90
- // Add root node
91
- const [root, ...garbage] = xml_children(document as xml_document, _options)
92
- if (!root) {
93
- throw new SyntaxError("No root node detected")
94
- }
95
- if (garbage.length) {
96
- throw new SyntaxError("Multiple root node detected")
97
- }
98
- text += xml_node(root, { ..._options, depth: 0 })
99
-
100
- return text.trim()
101
- }
102
-
103
- /**
104
- * Helper to create a CDATA node.
105
- *
106
- * @example
107
- * ```ts
108
- * import { stringify, cdata } from "./stringify.ts"
109
- * stringify({ string: cdata(`hello <world>`) })
110
- * // <string><![CDATA[hello <world>]]></string>
111
- * ```
112
- */
113
- export function cdata(text: string): Omit<xml_text, "~parent"> {
114
- return {
115
- "~name": "~cdata",
116
- "#text": text,
117
- }
118
- }
119
-
120
- /**
121
- * Helper to create a comment node.
122
- *
123
- * @example
124
- * ```ts
125
- * import { stringify, comment } from "./stringify.ts"
126
- * stringify({ string: comment(`hello world`) })
127
- * // <string><!--hello world--></string>
128
- * ```
129
- */
130
- export function comment(text: string): Omit<xml_text, "~parent"> {
131
- return {
132
- "~name": "~comment",
133
- "#text": text,
134
- }
135
- }
136
-
137
- /** Create XML prolog. */
138
- function xml_prolog(document: xml_document, options: _options): string {
139
- ;(document as rw)["~name"] ??= "xml"
140
- return xml_instruction(document, options)
141
- }
142
-
143
- /** Create XML instruction. */
144
- function xml_instruction(node: xml_node, { format: { indent } }: _options): string {
145
- let text = ""
146
- const attributes = xml_attributes(node as xml_node, arguments[1])
147
- if (attributes.length) {
148
- text += `<?${node["~name"].replace(/^~/, "")}`
149
- for (const [name, value] of attributes) {
150
- text += ` ${name}="${value}"`
151
- }
152
- text += `?>${indent ? "\n" : ""}`
153
- }
154
- return text
155
- }
156
-
157
- /** Create XML doctype. */
158
- function xml_doctype(node: xml_node, { format: { indent } }: _options): string {
159
- let text = ""
160
- const attributes = xml_attributes(node, arguments[1])
161
- const elements = xml_children(node, arguments[1])
162
- if (attributes.length + elements.length) {
163
- text += `<!DOCTYPE`
164
- for (const [name] of attributes) {
165
- text += ` ${!/^[A-Za-z0-9_]+$/.test(name) ? `"${name}"` : name}`
166
- }
167
- if (elements.length) {
168
- text += `${indent ? `\n${indent}` : " "}[${indent ? "\n" : ""}`
169
- for (const element of elements) {
170
- text += `${indent}<!ELEMENT ${element["~name"]} (${element["#text"]})>${indent ? "\n" : ""}`
171
- }
172
- text += `${indent ? indent : ""}]${indent ? "\n" : ""}`
173
- }
174
- text += `>${indent ? "\n" : ""}`
175
- }
176
- return text
177
- }
178
-
179
- /** Create XML node. */
180
- function xml_node(node: xml_node, { format: { breakline = 0, indent = "" }, replace, depth = 0 }: _options & { depth?: number }): string {
181
- if (replace?.custom) {
182
- if (replace.custom({ name: node["~name"], key: null, value: null, node }) === undefined) {
183
- return ""
184
- }
185
- }
186
- let text = `${indent.repeat(depth)}<${node["~name"]}`
187
- const attributes = xml_attributes(node, arguments[1])
188
- const children = xml_children(node, arguments[1])
189
- const preserve = node["@xml:space"] === "preserve"
190
- for (const [name, value] of attributes) {
191
- text += ` ${name}="${value}"`
192
- }
193
- if ((children.length) || (("#text" in node) && (node["#text"].length))) {
194
- const inline = indent && (!preserve) && ((children.length) || (node["#text"].length > breakline - indent.length * depth))
195
- text += `>${indent && (!preserve) && (children.length) ? "\n" : ""}`
196
- if ("#text" in node) {
197
- if (inline) {
198
- text += `\n${indent.repeat(depth + 1)}`
199
- }
200
- text += node["#text"]
201
- if (inline) {
202
- text += "\n"
203
- }
204
- }
205
- for (const child of children) {
206
- text += xml_node(child, { ...arguments[1], depth: depth + 1 })
207
- }
208
- if (inline) {
209
- text += indent.repeat(depth)
210
- }
211
- text += `</${node["~name"]}>${indent ? "\n" : ""}`
212
- } else {
213
- text += `/>${indent ? "\n" : ""}`
214
- }
215
- return text
216
- }
217
-
218
- /** Extract children from node. */
219
- function xml_children(node: xml_node, options: options): Array<xml_node> {
220
- const children = Object.keys(node)
221
- .filter((key) => /^[A-Za-z_]/.test(key))
222
- .flatMap((key) =>
223
- [node![key]].flat().map((value) => {
224
- switch (true) {
225
- case value === null:
226
- return ({ ["~name"]: key, ["#text"]: "" })
227
- case typeof value === "object": {
228
- const child = { ...value as record, ["~name"]: key } as record
229
- if (((value as record)["~name"] as string)?.startsWith("~")) {
230
- child[internal] = (value as record)["~name"]
231
- }
232
- return child
233
- }
234
- default:
235
- return ({ ["~name"]: key, ["#text"]: `${value}` })
236
- }
237
- })
238
- )
239
- .map((node) => {
240
- if ("#text" in node) {
241
- const cdata = node[internal] === "~cdata"
242
- const comment = node[internal] === "~comment"
243
- node["#text"] = replace(node as xml_node, "#text", { ...options, escape: cdata ? [] : ["<", ">"] }) as string
244
- if (node["#text"] === undefined) {
245
- delete node["#text"]
246
- } else {
247
- node["#text"] = cdata ? `<![CDATA[${node["#text"]}]]>` : comment ? `<!--${node["#text"]}-->` : `${node["#text"]}`
248
- }
249
- }
250
- return node
251
- }) as ReturnType<typeof xml_children>
252
- return children
253
- }
254
-
255
- /** Extract attributes from node. */
256
- function xml_attributes(node: xml_node, options: options): Array<[string, string]> {
257
- return Object.entries(node!)
258
- .filter(([key]) => key.startsWith("@"))
259
- .map(([key]) => [key.slice(1), replace(node!, key, { ...options, escape: ['"', "'"] })])
260
- .filter(([_, value]) => value !== undefined) as ReturnType<typeof xml_attributes>
261
- }
262
-
263
- /** Entities */
264
- const entities = {
265
- "&": "&amp;", //Keep first
266
- '"': "&quot;",
267
- "<": "&lt;",
268
- ">": "&gt;",
269
- "'": "&apos;",
270
- } as const
271
-
272
- /** Replace value. */
273
- function replace(node: xml_node | xml_text, key: string, options: options & { escape?: Array<keyof typeof entities> }) {
274
- let value = `${(node as xml_node)[key]}` as string
275
- if (options?.escape) {
276
- if (options?.replace?.entities) {
277
- options.escape = Object.keys(entities) as Array<keyof typeof entities>
278
- }
279
- for (const char of options?.escape) {
280
- value = `${value}`.replaceAll(char, entities[char])
281
- }
282
- }
283
- if (options?.replace?.custom) {
284
- return options.replace.custom({ name: node["~name"], key, value, node: node as xml_node })
285
- }
286
- return value
287
- }
package/stringify_test.ts DELETED
@@ -1,303 +0,0 @@
1
- import { type options as parse_options, parse } from "./parse.ts"
2
- import { cdata, comment, type options as stringify_options, stringify } from "./stringify.ts"
3
- import { expect, test } from "@libs/testing"
4
-
5
- // This operation ensure that reforming a parsed XML will still yield same data
6
- const check = (xml: string, options?: parse_options & stringify_options) => {
7
- expect(stringify(parse(xml, options), options), xml)
8
- return parse(stringify(parse(xml, options), options), options)
9
- }
10
-
11
- test("all")("`stringify()` xml syntax xml prolog", () =>
12
- expect(
13
- check(
14
- `<?xml version="1.0" encoding="UTF-8"?>
15
- <root/>`,
16
- ),
17
- ).toEqual(
18
- {
19
- "@version": "1.0",
20
- "@encoding": "UTF-8",
21
- root: null,
22
- },
23
- ))
24
-
25
- test("all")("`stringify()` xml syntax xml stylesheet", () =>
26
- expect(
27
- check(
28
- `<?xml version="1.0" encoding="UTF-8"?>
29
- <?xml-stylesheet href="styles.xsl" type="text/xsl"?>
30
- <root/>`,
31
- ),
32
- ).toEqual(
33
- {
34
- "@version": "1.0",
35
- "@encoding": "UTF-8",
36
- "#instructions": {
37
- "xml-stylesheet": {
38
- "@href": "styles.xsl",
39
- "@type": "text/xsl",
40
- },
41
- },
42
- root: null,
43
- },
44
- ))
45
-
46
- test("all")("`stringify()` xml syntax doctype", () =>
47
- expect(
48
- check(
49
- `<!DOCTYPE type "quoted attribute" [
50
- <!ELEMENT element (value)>
51
- ]>
52
- <root/>`,
53
- ),
54
- ).toEqual(
55
- {
56
- "#doctype": {
57
- "@type": "",
58
- "@quoted attribute": "",
59
- element: "value",
60
- },
61
- root: null,
62
- },
63
- ))
64
-
65
- for (const indent of [" ", ""]) {
66
- test("all")(`\`stringify()\` xml example w3schools.com#3 (indent = "${indent}")`, () =>
67
- expect(
68
- check(
69
- `
70
- <?xml version="1.0" encoding="UTF-8"?>
71
- <?xml-stylesheet href="styles.xsl" type="text/xsl"?>
72
- <!DOCTYPE type "quoted attribute" [
73
- <!ELEMENT element (value)>
74
- ]>
75
- <bookstore>
76
- <notebook/>
77
- <book category="cooking">
78
- <!-- Comment Node -->
79
- <title lang="en">Everyday Italian</title>
80
- <author>Giada De Laurentiis</author>
81
- <year>2005</year>
82
- <price>30</price>
83
- </book>
84
- <book category="children">
85
- <!-- First Comment Node -->
86
- <!-- Second Comment Node -->
87
- <title lang="en">Harry Potter</title>
88
- <author>J K. Rowling</author>
89
- <year>2005</year>
90
- <price>29.99</price>
91
- </book>
92
- <book category="web">
93
- <title lang="en">XQuery Kick Start</title>
94
- <author>James McGovern</author>
95
- <author>Per Bothner</author>
96
- <author>Kurt Cagle</author>
97
- <author>James Linn</author>
98
- <author>Vaidyanathan Nagarajan</author>
99
- <year>2003</year>
100
- <price>49.99</price>
101
- </book>
102
- <book category="web" cover="paperback">
103
- <title lang="en">Learning XML</title>
104
- <author>Erik T. Ray</author>
105
- <year>2003</year>
106
- <price>39.95</price>
107
- </book>
108
- </bookstore>`,
109
- { revive: { booleans: true, numbers: true }, format: { indent } },
110
- ),
111
- ).toEqual(
112
- {
113
- "@version": "1.0",
114
- "@encoding": "UTF-8",
115
- "#instructions": {
116
- "xml-stylesheet": {
117
- "@href": "styles.xsl",
118
- "@type": "text/xsl",
119
- },
120
- },
121
- "#doctype": {
122
- "@type": "",
123
- "@quoted attribute": "",
124
- element: "value",
125
- },
126
- bookstore: {
127
- notebook: null,
128
- book: [
129
- {
130
- "@category": "cooking",
131
- title: { "@lang": "en", "#text": "Everyday Italian" },
132
- author: "Giada De Laurentiis",
133
- year: 2005,
134
- price: 30,
135
- },
136
- {
137
- "@category": "children",
138
- title: { "@lang": "en", "#text": "Harry Potter" },
139
- author: "J K. Rowling",
140
- year: 2005,
141
- price: 29.99,
142
- },
143
- {
144
- "@category": "web",
145
- title: { "@lang": "en", "#text": "XQuery Kick Start" },
146
- author: [
147
- "James McGovern",
148
- "Per Bothner",
149
- "Kurt Cagle",
150
- "James Linn",
151
- "Vaidyanathan Nagarajan",
152
- ],
153
- year: 2003,
154
- price: 49.99,
155
- },
156
- {
157
- "@category": "web",
158
- "@cover": "paperback",
159
- title: { "@lang": "en", "#text": "Learning XML" },
160
- author: "Erik T. Ray",
161
- year: 2003,
162
- price: 39.95,
163
- },
164
- ],
165
- },
166
- },
167
- ))
168
- }
169
-
170
- test("all")("`stringify()` xml types", () =>
171
- expect(
172
- check(
173
- `<types>
174
- <boolean>true</boolean>
175
- <null/>
176
- <string>hello</string>
177
- </types>`,
178
- { revive: { booleans: true } },
179
- ),
180
- ).toEqual(
181
- {
182
- types: {
183
- boolean: true,
184
- null: null,
185
- string: "hello",
186
- },
187
- },
188
- ))
189
-
190
- test("all")("`stringify()` xml entities", () =>
191
- expect(
192
- check(`<string>&quot; &lt; &gt; &amp; &apos;</string>`),
193
- ).toEqual(
194
- {
195
- string: `" < > & '`,
196
- },
197
- ))
198
-
199
- test("all")(
200
- "`stringify()` xml entities are escaped only where needed",
201
- () =>
202
- expect(stringify({
203
- root: {
204
- "@attribute": `<text with escaped quotes (',")>`,
205
- text: `only < and > should be escaped, not &, ", '`,
206
- },
207
- }, { format: { breakline: 0 } })).toEqual(
208
- `
209
- <root attribute="<text with escaped quotes (&apos;,&quot;)>">
210
- <text>
211
- only &lt; and &gt; should be escaped, not &, ", '
212
- </text>
213
- </root>`.trim(),
214
- ),
215
- )
216
-
217
- test("all")(
218
- "`stringify()` xml entiries are always escaped when escapeAllEntities is true",
219
- () =>
220
- expect(stringify({
221
- root: {
222
- "@attribute": `< > &, ", '`,
223
- text: `< > &, ", '`,
224
- },
225
- }, { replace: { entities: true }, format: { breakline: 0 } })).toEqual(
226
- `
227
- <root attribute="&lt; &gt; &amp;, &quot;, &apos;">
228
- <text>
229
- &lt; &gt; &amp;, &quot;, &apos;
230
- </text>
231
- </root>`.trim(),
232
- ),
233
- )
234
-
235
- test("all")("`stringify()` xml space preserve", () =>
236
- expect(
237
- check(`<text xml:space="preserve"> hello world </text>`),
238
- ).toEqual(
239
- {
240
- text: {
241
- "#text": " hello world ",
242
- "@xml:space": "preserve",
243
- },
244
- },
245
- ))
246
-
247
- test("all")("`stringify()` cdata is preserved on root nodes", () =>
248
- expect(
249
- stringify({ string: cdata(`hello <world>`) }),
250
- ).toBe("<string><![CDATA[hello <world>]]></string>"))
251
-
252
- test("all")("`stringify()` cdata is preserved on child nodes", () =>
253
- expect(
254
- stringify({ nested: { string: cdata(`hello <world>`) } }),
255
- ).toBe(`
256
- <nested>
257
- <string><![CDATA[hello <world>]]></string>
258
- </nested>`.trim()))
259
-
260
- test("all")("`stringify()` comments is preserved on root nodes", () =>
261
- expect(
262
- stringify({ string: comment(`hello world`) }),
263
- ).toBe("<string><!--hello world--></string>"))
264
-
265
- test("all")("`stringify()` comments is preserved on child nodes", () =>
266
- expect(
267
- stringify({ nested: { string: comment(`hello world`) } }),
268
- ).toBe(`
269
- <nested>
270
- <string><!--hello world--></string>
271
- </nested>`.trim()))
272
-
273
- // Custom replacer
274
-
275
- test("all")("`stringify()` xml replacer", () =>
276
- expect(
277
- stringify({ root: { not: true, yes: true, delete: true, attribute: { "@delete": true } } }, {
278
- replace: {
279
- custom: ({ name, key, value }) => {
280
- if ((name === "delete") || (key === "@delete")) {
281
- return undefined
282
- }
283
- if ((name === "not") && (key === "#text")) {
284
- return !value
285
- }
286
- return value
287
- },
288
- },
289
- }),
290
- ).toBe(
291
- `
292
- <root>
293
- <not>false</not>
294
- <yes>true</yes>
295
- <attribute/>
296
- </root>`.trim(),
297
- ))
298
-
299
- //Errors checks
300
-
301
- test("all")("`stringify()` xml syntax no root node", () => expect(() => stringify({})).toThrow(SyntaxError))
302
-
303
- test("all")("`stringify()` xml syntax multiple root nodes", () => expect(() => stringify({ root: null, garbage: null })).toThrow(SyntaxError))