@lowlighter/xml 5.2.1 → 5.3.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/LICENSE +21 -0
- package/README.md +33 -0
- package/_types.ts +37 -0
- package/_types_test.ts +1 -0
- package/bench/assets/medium.xml +291 -0
- package/bench/assets/small.xml +7 -0
- package/bench/bench.ts +17 -0
- package/deno.jsonc +93 -0
- package/deno.lock +86 -0
- package/mod.mjs +1 -1
- package/mod.ts +3 -0
- package/package.json +7 -1
- package/parse.mjs +1 -1
- package/parse.ts +445 -0
- package/parse_test.ts +1303 -0
- package/stringify.ts +241 -0
- package/stringify_test.ts +284 -0
- package/wasm_xml_parser/Cargo.lock +159 -0
- package/wasm_xml_parser/Cargo.toml +16 -0
- package/wasm_xml_parser/src/lib.rs +232 -0
- package/wasm_xml_parser/wasm_xml_parser.js +2 -0
package/stringify.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Imports
|
|
2
|
+
import type { Nullable, record, rw } from "@libs/typing"
|
|
3
|
+
import type { xml_document, xml_node, xml_text } from "./_types.ts"
|
|
4
|
+
|
|
5
|
+
// Re-exports
|
|
6
|
+
export type { xml_document, xml_node, xml_text }
|
|
7
|
+
|
|
8
|
+
/** XML stringifier options. */
|
|
9
|
+
export type options = {
|
|
10
|
+
/** Format options. */
|
|
11
|
+
format?: {
|
|
12
|
+
/**
|
|
13
|
+
* Indent string (defaults to `" "`).
|
|
14
|
+
* Set to empty string to disable indentation and enable minification.
|
|
15
|
+
*/
|
|
16
|
+
indent?: string
|
|
17
|
+
/** Break text node if its length is greater than this value (defaults to `128`). */
|
|
18
|
+
breakline?: number
|
|
19
|
+
}
|
|
20
|
+
/** Replace options. */
|
|
21
|
+
replace?: {
|
|
22
|
+
/**
|
|
23
|
+
* Force escape all XML entities.
|
|
24
|
+
* By default, only the ones that would break the XML structure are escaped.
|
|
25
|
+
*/
|
|
26
|
+
entities?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Custom replacer (this is applied after other revivals).
|
|
29
|
+
* When it is applied on an attribute, `key` and `value` will be given.
|
|
30
|
+
* When it is applied on a node, both `key` and `value` will be `null`.
|
|
31
|
+
* Return `undefined` to delete either the attribute or the tag.
|
|
32
|
+
*/
|
|
33
|
+
custom?: (args: { name: string; key: Nullable<string>; value: Nullable<string>; node: Readonly<xml_node> }) => unknown
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** XML stringifier options (with non-nullable format options). */
|
|
38
|
+
type _options = options & { format: NonNullable<options["format"]> }
|
|
39
|
+
|
|
40
|
+
//* - `readonly ["~children"]: Array<xml_node|xml_text>`: node children
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stringify an {@link xml_document} object into a XML string.
|
|
44
|
+
*
|
|
45
|
+
* Output can be customized using the {@link options} parameter.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { stringify } from "./stringify.ts"
|
|
50
|
+
*
|
|
51
|
+
* console.log(stringify({
|
|
52
|
+
* root: {
|
|
53
|
+
* text: "hello",
|
|
54
|
+
* array: ["world", "monde", "世界", "🌏"],
|
|
55
|
+
* number: 42,
|
|
56
|
+
* boolean: true,
|
|
57
|
+
* complex: {
|
|
58
|
+
* "@attribute": "value",
|
|
59
|
+
* "#text": "content",
|
|
60
|
+
* },
|
|
61
|
+
* }
|
|
62
|
+
* }))
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function stringify(document: Partial<xml_document>, options?: options): string {
|
|
66
|
+
options ??= {}
|
|
67
|
+
options.format ??= {}
|
|
68
|
+
options.format.indent ??= " "
|
|
69
|
+
options.format.breakline ??= 128
|
|
70
|
+
const _options = options as _options
|
|
71
|
+
let text = ""
|
|
72
|
+
// Add prolog
|
|
73
|
+
text += xml_prolog(document as xml_document, _options)
|
|
74
|
+
// Add processing instructions
|
|
75
|
+
if (document["#instructions"]) {
|
|
76
|
+
for (const nodes of Object.values(document["#instructions"])) {
|
|
77
|
+
for (const node of [nodes].flat()) {
|
|
78
|
+
text += xml_instruction(node, _options)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Add doctype
|
|
83
|
+
if (document["#doctype"]) {
|
|
84
|
+
text += xml_doctype(document["#doctype"] as xml_node, _options)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add root node
|
|
88
|
+
const [root, ...garbage] = xml_children(document as xml_document, _options)
|
|
89
|
+
if (garbage.length) {
|
|
90
|
+
throw new SyntaxError("Multiple root node detected")
|
|
91
|
+
}
|
|
92
|
+
text += xml_node(root, { ..._options, depth: 0 })
|
|
93
|
+
|
|
94
|
+
return text.trim()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Create XML prolog. */
|
|
98
|
+
function xml_prolog(document: xml_document, options: _options): string {
|
|
99
|
+
;(document as rw)["~name"] ??= "xml"
|
|
100
|
+
return xml_instruction(document, options)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Create XML instruction. */
|
|
104
|
+
function xml_instruction(node: xml_node, { format: { indent } }: _options): string {
|
|
105
|
+
let text = ""
|
|
106
|
+
const attributes = xml_attributes(node as xml_node, arguments[1])
|
|
107
|
+
if (attributes.length) {
|
|
108
|
+
text += `<?${node["~name"].replace(/^~/, "")}`
|
|
109
|
+
for (const [name, value] of attributes) {
|
|
110
|
+
text += ` ${name}="${value}"`
|
|
111
|
+
}
|
|
112
|
+
text += `?>${indent ? "\n" : ""}`
|
|
113
|
+
}
|
|
114
|
+
return text
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Create XML doctype. */
|
|
118
|
+
function xml_doctype(node: xml_node, { format: { indent } }: _options): string {
|
|
119
|
+
let text = ""
|
|
120
|
+
const attributes = xml_attributes(node, arguments[1])
|
|
121
|
+
const elements = xml_children(node, arguments[1])
|
|
122
|
+
if (attributes.length + elements.length) {
|
|
123
|
+
text += `<!DOCTYPE`
|
|
124
|
+
for (const [name] of attributes) {
|
|
125
|
+
text += ` ${!/^[A-Za-z0-9_]+$/.test(name) ? `"${name}"` : name}`
|
|
126
|
+
}
|
|
127
|
+
if (elements.length) {
|
|
128
|
+
text += `${indent ? `\n${indent}` : " "}[${indent ? "\n" : ""}`
|
|
129
|
+
for (const element of elements) {
|
|
130
|
+
text += `${indent}<!ELEMENT ${element["~name"]} (${element["#text"]})>${indent ? "\n" : ""}`
|
|
131
|
+
}
|
|
132
|
+
text += `${indent ? indent : ""}]${indent ? "\n" : ""}`
|
|
133
|
+
}
|
|
134
|
+
text += `>${indent ? "\n" : ""}`
|
|
135
|
+
}
|
|
136
|
+
return text
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Create XML node. */
|
|
140
|
+
function xml_node(node: xml_node, { format: { breakline = 0, indent = "" }, replace, depth = 0 }: _options & { depth?: number }): string {
|
|
141
|
+
if (replace?.custom) {
|
|
142
|
+
if (replace.custom({ name: node["~name"], key: null, value: null, node }) === undefined) {
|
|
143
|
+
return ""
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let text = `${indent.repeat(depth)}<${node["~name"]}`
|
|
147
|
+
const attributes = xml_attributes(node, arguments[1])
|
|
148
|
+
const children = xml_children(node, arguments[1])
|
|
149
|
+
const preserve = node["@xml:space"] === "preserve"
|
|
150
|
+
for (const [name, value] of attributes) {
|
|
151
|
+
text += ` ${name}="${value}"`
|
|
152
|
+
}
|
|
153
|
+
if ((children.length) || (("#text" in node) && (node["#text"].length))) {
|
|
154
|
+
const inline = indent && (!preserve) && ((children.length) || (node["#text"].length > breakline - indent.length * depth))
|
|
155
|
+
text += `>${indent && (!preserve) && (children.length) ? "\n" : ""}`
|
|
156
|
+
if ("#text" in node) {
|
|
157
|
+
if (inline) {
|
|
158
|
+
text += `\n${indent.repeat(depth + 1)}`
|
|
159
|
+
}
|
|
160
|
+
text += node["#text"]
|
|
161
|
+
if (inline) {
|
|
162
|
+
text += "\n"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const child of children) {
|
|
166
|
+
text += xml_node(child, { ...arguments[1], depth: depth + 1 })
|
|
167
|
+
}
|
|
168
|
+
if (inline) {
|
|
169
|
+
text += indent.repeat(depth)
|
|
170
|
+
}
|
|
171
|
+
text += `</${node["~name"]}>${indent ? "\n" : ""}`
|
|
172
|
+
} else {
|
|
173
|
+
text += `/>${indent ? "\n" : ""}`
|
|
174
|
+
}
|
|
175
|
+
return text
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Extract children from node. */
|
|
179
|
+
function xml_children(node: xml_node, options: options): Array<xml_node> {
|
|
180
|
+
const children = Object.keys(node)
|
|
181
|
+
.filter((key) => /^[A-Za-z_]/.test(key))
|
|
182
|
+
.flatMap((key) =>
|
|
183
|
+
[node![key]].flat().map((value) => {
|
|
184
|
+
switch (true) {
|
|
185
|
+
case value === null:
|
|
186
|
+
return ({ ["~name"]: key, ["#text"]: "" })
|
|
187
|
+
case typeof value === "object":
|
|
188
|
+
return ({ ["~name"]: key, ...value as record })
|
|
189
|
+
default:
|
|
190
|
+
return ({ ["~name"]: key, ["#text"]: `${value}` })
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
)
|
|
194
|
+
.map((node) => {
|
|
195
|
+
if ("#text" in node) {
|
|
196
|
+
node["#text"] = replace(node as xml_node, "#text", { ...options, escape: ["<", ">"] }) as string
|
|
197
|
+
if (node["#text"] === undefined) {
|
|
198
|
+
delete node["#text"]
|
|
199
|
+
} else {
|
|
200
|
+
node["#text"] = `${node["#text"]}`
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return node
|
|
204
|
+
}) as ReturnType<typeof xml_children>
|
|
205
|
+
//TODO(@lowlighter): Eventually add node["~children"] support for ordering and possibly bypass the children search
|
|
206
|
+
return children
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Extract attributes from node. */
|
|
210
|
+
function xml_attributes(node: xml_node, options: options): Array<[string, string]> {
|
|
211
|
+
return Object.entries(node!)
|
|
212
|
+
.filter(([key]) => key.startsWith("@"))
|
|
213
|
+
.map(([key]) => [key.slice(1), replace(node!, key, { ...options, escape: ['"', "'"] })])
|
|
214
|
+
.filter(([_, value]) => value !== undefined) as ReturnType<typeof xml_attributes>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Entities */
|
|
218
|
+
const entities = {
|
|
219
|
+
"&": "&", //Keep first
|
|
220
|
+
'"': """,
|
|
221
|
+
"<": "<",
|
|
222
|
+
">": ">",
|
|
223
|
+
"'": "'",
|
|
224
|
+
} as const
|
|
225
|
+
|
|
226
|
+
/** Replace value. */
|
|
227
|
+
function replace(node: xml_node | xml_text, key: string, options: options & { escape?: Array<keyof typeof entities> }) {
|
|
228
|
+
let value = `${(node as xml_node)[key]}` as string
|
|
229
|
+
if (options?.escape) {
|
|
230
|
+
if (options?.replace?.entities) {
|
|
231
|
+
options.escape = Object.keys(entities) as Array<keyof typeof entities>
|
|
232
|
+
}
|
|
233
|
+
for (const char of options?.escape) {
|
|
234
|
+
value = `${value}`.replaceAll(char, entities[char])
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (options?.replace?.custom) {
|
|
238
|
+
return options.replace.custom({ name: node["~name"], key, value, node: node as xml_node })
|
|
239
|
+
}
|
|
240
|
+
return value
|
|
241
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { type options as parse_options, parse } from "./parse.ts"
|
|
2
|
+
import { 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>" < > & '</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 (',")>">
|
|
210
|
+
<text>
|
|
211
|
+
only < and > 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="< > &, ", '">
|
|
228
|
+
<text>
|
|
229
|
+
< > &, ", '
|
|
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", () =>
|
|
248
|
+
expect(
|
|
249
|
+
check(`<string><![CDATA[hello <world>]]></string>`),
|
|
250
|
+
).toEqual(
|
|
251
|
+
{
|
|
252
|
+
string: `hello <world>`,
|
|
253
|
+
},
|
|
254
|
+
))
|
|
255
|
+
|
|
256
|
+
// Custom replacer
|
|
257
|
+
|
|
258
|
+
test("all")("stringify(): xml replacer", () =>
|
|
259
|
+
expect(
|
|
260
|
+
stringify({ root: { not: true, yes: true, delete: true, attribute: { "@delete": true } } }, {
|
|
261
|
+
replace: {
|
|
262
|
+
custom: ({ name, key, value }) => {
|
|
263
|
+
if ((name === "delete") || (key === "@delete")) {
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
if ((name === "not") && (key === "#text")) {
|
|
267
|
+
return !value
|
|
268
|
+
}
|
|
269
|
+
return value
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
).toBe(
|
|
274
|
+
`
|
|
275
|
+
<root>
|
|
276
|
+
<not>false</not>
|
|
277
|
+
<yes>true</yes>
|
|
278
|
+
<attribute/>
|
|
279
|
+
</root>`.trim(),
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
//Errors checks
|
|
283
|
+
|
|
284
|
+
test("all")("stringify(): xml syntax unique root", () => expect(() => stringify({ root: null, garbage: null })).toThrow(SyntaxError))
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 3
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "bumpalo"
|
|
7
|
+
version = "3.16.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "cfg-if"
|
|
13
|
+
version = "1.0.0"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "js-sys"
|
|
19
|
+
version = "0.3.69"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"wasm-bindgen",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "log"
|
|
28
|
+
version = "0.4.21"
|
|
29
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
+
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "memchr"
|
|
34
|
+
version = "2.7.2"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
|
37
|
+
|
|
38
|
+
[[package]]
|
|
39
|
+
name = "once_cell"
|
|
40
|
+
version = "1.19.0"
|
|
41
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
+
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|
43
|
+
|
|
44
|
+
[[package]]
|
|
45
|
+
name = "proc-macro2"
|
|
46
|
+
version = "1.0.82"
|
|
47
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
48
|
+
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
|
49
|
+
dependencies = [
|
|
50
|
+
"unicode-ident",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[[package]]
|
|
54
|
+
name = "quick-xml"
|
|
55
|
+
version = "0.31.0"
|
|
56
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
57
|
+
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
|
58
|
+
dependencies = [
|
|
59
|
+
"memchr",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[[package]]
|
|
63
|
+
name = "quote"
|
|
64
|
+
version = "1.0.36"
|
|
65
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
66
|
+
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
|
67
|
+
dependencies = [
|
|
68
|
+
"proc-macro2",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "syn"
|
|
73
|
+
version = "2.0.64"
|
|
74
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
+
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
|
|
76
|
+
dependencies = [
|
|
77
|
+
"proc-macro2",
|
|
78
|
+
"quote",
|
|
79
|
+
"unicode-ident",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[[package]]
|
|
83
|
+
name = "unicode-ident"
|
|
84
|
+
version = "1.0.12"
|
|
85
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
86
|
+
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|
87
|
+
|
|
88
|
+
[[package]]
|
|
89
|
+
name = "wasm-bindgen"
|
|
90
|
+
version = "0.2.92"
|
|
91
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
92
|
+
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
|
93
|
+
dependencies = [
|
|
94
|
+
"cfg-if",
|
|
95
|
+
"wasm-bindgen-macro",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[[package]]
|
|
99
|
+
name = "wasm-bindgen-backend"
|
|
100
|
+
version = "0.2.92"
|
|
101
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
102
|
+
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
|
103
|
+
dependencies = [
|
|
104
|
+
"bumpalo",
|
|
105
|
+
"log",
|
|
106
|
+
"once_cell",
|
|
107
|
+
"proc-macro2",
|
|
108
|
+
"quote",
|
|
109
|
+
"syn",
|
|
110
|
+
"wasm-bindgen-shared",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "wasm-bindgen-macro"
|
|
115
|
+
version = "0.2.92"
|
|
116
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
117
|
+
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
|
118
|
+
dependencies = [
|
|
119
|
+
"quote",
|
|
120
|
+
"wasm-bindgen-macro-support",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[[package]]
|
|
124
|
+
name = "wasm-bindgen-macro-support"
|
|
125
|
+
version = "0.2.92"
|
|
126
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
127
|
+
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
|
128
|
+
dependencies = [
|
|
129
|
+
"proc-macro2",
|
|
130
|
+
"quote",
|
|
131
|
+
"syn",
|
|
132
|
+
"wasm-bindgen-backend",
|
|
133
|
+
"wasm-bindgen-shared",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[[package]]
|
|
137
|
+
name = "wasm-bindgen-shared"
|
|
138
|
+
version = "0.2.92"
|
|
139
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
140
|
+
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
|
141
|
+
|
|
142
|
+
[[package]]
|
|
143
|
+
name = "wasm_xml_parser"
|
|
144
|
+
version = "0.1.0"
|
|
145
|
+
dependencies = [
|
|
146
|
+
"quick-xml",
|
|
147
|
+
"wasm-bindgen",
|
|
148
|
+
"web-sys",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
[[package]]
|
|
152
|
+
name = "web-sys"
|
|
153
|
+
version = "0.3.69"
|
|
154
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
+
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
|
156
|
+
dependencies = [
|
|
157
|
+
"js-sys",
|
|
158
|
+
"wasm-bindgen",
|
|
159
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "wasm_xml_parser"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
authors = ["lowlighter"]
|
|
5
|
+
edition = "2021"
|
|
6
|
+
description = "WASM XML parser"
|
|
7
|
+
repository = "https://github.com/lowlighter/libs"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
quick-xml = "0.31"
|
|
12
|
+
wasm-bindgen = "0.2"
|
|
13
|
+
web-sys = { version = "0.3" }
|
|
14
|
+
|
|
15
|
+
[lib]
|
|
16
|
+
crate-type = ["cdylib", "rlib"]
|