@office-open/xml 0.5.1 → 0.5.2
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/dist/index.mjs +499 -8
- package/dist/utils.mjs +116 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,499 @@
|
|
|
1
|
-
import{allChildren
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { allChildren, attr, attrBool, attrNum, childCount, childText, children, collectText, colorAttr, findChild, findDeep, hasChild, textOf } from "./utils.mjs";
|
|
2
|
+
//#region src/escape.ts
|
|
3
|
+
const XML_CHAR_MAP = {
|
|
4
|
+
"&": "&",
|
|
5
|
+
"\"": """,
|
|
6
|
+
"'": "'",
|
|
7
|
+
"<": "<",
|
|
8
|
+
">": ">"
|
|
9
|
+
};
|
|
10
|
+
const XML_CHAR_PATTERN = /([&"<>'])/g;
|
|
11
|
+
/** Escape text content for XML */
|
|
12
|
+
function escapeXml(str) {
|
|
13
|
+
return str.replace(XML_CHAR_PATTERN, (ch) => XML_CHAR_MAP[ch]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Escape attribute value matching xml-js's js2xml behavior.
|
|
17
|
+
* Handles already-escaped entities to prevent double-escaping.
|
|
18
|
+
*/
|
|
19
|
+
function escapeAttributeValue(str) {
|
|
20
|
+
return String(str).replace(/&(?!amp;|lt;|gt;|quot;|apos;)/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build an XML attribute string fragment from a record.
|
|
24
|
+
* `undefined` values are automatically skipped.
|
|
25
|
+
* String values are escaped via `escapeXml`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* attrs({ id: 1, name: "foo", hidden: undefined })
|
|
29
|
+
* // => ' id="1" name="foo"'
|
|
30
|
+
*/
|
|
31
|
+
function attrs(record) {
|
|
32
|
+
let s = "";
|
|
33
|
+
for (const [k, v] of Object.entries(record)) if (v !== void 0) s += ` ${k}="${typeof v === "string" ? escapeXml(v) : v}"`;
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/serialize.ts
|
|
38
|
+
const DEFAULT_INDENT = " ";
|
|
39
|
+
function xml(input, options) {
|
|
40
|
+
const opts = normalizeOptions$1(options);
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (opts.declaration) {
|
|
43
|
+
const declOpts = opts.declaration === true ? {} : opts.declaration;
|
|
44
|
+
const enc = declOpts.encoding || "UTF-8";
|
|
45
|
+
const sa = declOpts.standalone;
|
|
46
|
+
let decl = "<?xml version=\"1.0\" encoding=\"" + enc + "\"";
|
|
47
|
+
if (sa) decl += " standalone=\"" + sa + "\"";
|
|
48
|
+
decl += "?>";
|
|
49
|
+
parts.push(decl);
|
|
50
|
+
if (opts.indent) parts.push("\n");
|
|
51
|
+
}
|
|
52
|
+
const items = Array.isArray(input) ? input : [input];
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
const keys = Object.keys(items[i]);
|
|
55
|
+
parts.push(formatElement(keys[0], items[i][keys[0]], opts.indent, 0));
|
|
56
|
+
if (opts.indent && i < items.length - 1) parts.push("\n");
|
|
57
|
+
}
|
|
58
|
+
return parts.join("");
|
|
59
|
+
}
|
|
60
|
+
function normalizeOptions$1(options) {
|
|
61
|
+
const opts = typeof options === "object" && !Array.isArray(options) ? options : { indent: options };
|
|
62
|
+
let indent = "";
|
|
63
|
+
if (opts.indent) indent = opts.indent === true ? DEFAULT_INDENT : String(opts.indent);
|
|
64
|
+
return {
|
|
65
|
+
indent,
|
|
66
|
+
declaration: opts.declaration
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Single-pass XML formatter: directly converts IXmlableObject to string,
|
|
71
|
+
* eliminating the intermediate ResolvedElement tree.
|
|
72
|
+
*/
|
|
73
|
+
function formatElement(name, values, indent, depth) {
|
|
74
|
+
const attrParts = [];
|
|
75
|
+
const textParts = [];
|
|
76
|
+
const elemParts = [];
|
|
77
|
+
let emptyArray = false;
|
|
78
|
+
if (values == null) {
|
|
79
|
+
const attrStr = attrParts.length ? " " + attrParts.join(" ") : "";
|
|
80
|
+
return `${indent ? indent.repeat(depth) : ""}<${name}${attrStr}/>`;
|
|
81
|
+
}
|
|
82
|
+
if (typeof values === "object") {
|
|
83
|
+
if (values._attr) for (const key of Object.keys(values._attr)) attrParts.push(`${key}="${escapeXml(String(values._attr[key]))}"`);
|
|
84
|
+
if (values._attributes) for (const key of Object.keys(values._attributes)) attrParts.push(`${key}="${escapeXml(String(values._attributes[key]))}"`);
|
|
85
|
+
if (values._cdata) {
|
|
86
|
+
const escaped = String(values._cdata).replace(/\]\]>/g, "]]]]><![CDATA[>");
|
|
87
|
+
textParts.push(`<![CDATA[${escaped}]]>`);
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(values)) {
|
|
90
|
+
if (values.length === 0) emptyArray = true;
|
|
91
|
+
else for (const value of values) if (value && typeof value === "object" && "_attr" in value) for (const key of Object.keys(value._attr)) attrParts.push(`${key}="${escapeXml(String(value._attr[key]))}"`);
|
|
92
|
+
else if (value && typeof value === "object" && "_attributes" in value) for (const key of Object.keys(value._attributes)) attrParts.push(`${key}="${escapeXml(String(value._attributes[key]))}"`);
|
|
93
|
+
else if (value && typeof value === "object") {
|
|
94
|
+
const childKeys = Object.keys(value);
|
|
95
|
+
elemParts.push(formatElement(childKeys[0], value[childKeys[0]], indent, depth + 1));
|
|
96
|
+
} else if (value != null) textParts.push(escapeXml(String(value)));
|
|
97
|
+
}
|
|
98
|
+
} else textParts.push(escapeXml(String(values)));
|
|
99
|
+
const ind = indent ? indent.repeat(depth) : "";
|
|
100
|
+
const attrStr = attrParts.length ? " " + attrParts.join(" ") : "";
|
|
101
|
+
if (textParts.length + elemParts.length === 0) return emptyArray ? `${ind}<${name}${attrStr}></${name}>` : `${ind}<${name}${attrStr}/>`;
|
|
102
|
+
if (elemParts.length === 0 && textParts.length === 1) return indent ? `${ind}<${name}${attrStr}>${textParts[0]}</${name}>` : `<${name}${attrStr}>${textParts[0]}</${name}>`;
|
|
103
|
+
const parts = [];
|
|
104
|
+
parts.push(`${ind}<${name}${attrStr}>`);
|
|
105
|
+
if (indent) parts.push("\n");
|
|
106
|
+
const childIndent = indent ? indent.repeat(depth + 1) : "";
|
|
107
|
+
for (const t of textParts) {
|
|
108
|
+
parts.push(`${childIndent}${t}`);
|
|
109
|
+
if (indent) parts.push("\n");
|
|
110
|
+
}
|
|
111
|
+
for (const e of elemParts) {
|
|
112
|
+
parts.push(e);
|
|
113
|
+
if (indent) parts.push("\n");
|
|
114
|
+
}
|
|
115
|
+
parts.push(`${ind}</${name}>`);
|
|
116
|
+
return parts.join("");
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/parse.ts
|
|
120
|
+
const ENTITY_MAP = {
|
|
121
|
+
"&": "&",
|
|
122
|
+
"<": "<",
|
|
123
|
+
">": ">",
|
|
124
|
+
""": "\"",
|
|
125
|
+
"'": "'"
|
|
126
|
+
};
|
|
127
|
+
const ENTITY_PATTERN = /&(amp|lt|gt|quot|apos);/g;
|
|
128
|
+
function unescapeXml(str) {
|
|
129
|
+
return str.replace(ENTITY_PATTERN, (match) => ENTITY_MAP[match]);
|
|
130
|
+
}
|
|
131
|
+
function nativeTypeValue(value) {
|
|
132
|
+
const n = Number(value);
|
|
133
|
+
if (!isNaN(n)) return n;
|
|
134
|
+
const lower = value.toLowerCase();
|
|
135
|
+
if (lower === "true") return true;
|
|
136
|
+
if (lower === "false") return false;
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function xml2js(xmlString, options) {
|
|
140
|
+
const captureSpaces = options?.captureSpacesBetweenElements ?? false;
|
|
141
|
+
const trim = options?.trim ?? false;
|
|
142
|
+
const ignoreDeclaration = options?.ignoreDeclaration ?? false;
|
|
143
|
+
const ignoreText = options?.ignoreText ?? false;
|
|
144
|
+
const ignoreComment = options?.ignoreComment ?? false;
|
|
145
|
+
const ignoreCdata = options?.ignoreCdata ?? false;
|
|
146
|
+
const ignoreDoctype = options?.ignoreDoctype ?? false;
|
|
147
|
+
const nativeTypeAttributes = options?.nativeTypeAttributes ?? false;
|
|
148
|
+
const result = {};
|
|
149
|
+
const stack = [result];
|
|
150
|
+
let i = 0;
|
|
151
|
+
const len = xmlString.length;
|
|
152
|
+
while (i < len) {
|
|
153
|
+
if (!captureSpaces && isWhitespace(xmlString.charCodeAt(i))) {
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (xmlString.charCodeAt(i) !== 60) {
|
|
158
|
+
const start = i;
|
|
159
|
+
while (i < len && xmlString.charCodeAt(i) !== 60) i++;
|
|
160
|
+
let text = unescapeXml(xmlString.slice(start, i));
|
|
161
|
+
if (trim) text = text.trim();
|
|
162
|
+
if (ignoreText) continue;
|
|
163
|
+
if (text.length > 0) {
|
|
164
|
+
if (captureSpaces || text.trim().length > 0) addField(stack[stack.length - 1], "text", text);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
i++;
|
|
169
|
+
if (xmlString.charCodeAt(i) === 63) {
|
|
170
|
+
const end = xmlString.indexOf("?>", i + 1);
|
|
171
|
+
if (end === -1) break;
|
|
172
|
+
const body = xmlString.slice(i + 1, end);
|
|
173
|
+
i = end + 2;
|
|
174
|
+
const xmlMatch = body.match(/^xml\s+(.*)$/s);
|
|
175
|
+
if (xmlMatch) {
|
|
176
|
+
if (!ignoreDeclaration) {
|
|
177
|
+
if (!result.declaration) result.declaration = {};
|
|
178
|
+
const attrs = parseAttributes(xmlMatch[1]);
|
|
179
|
+
if (nativeTypeAttributes) for (const key of Object.keys(attrs)) attrs[key] = nativeTypeValue(attrs[key]);
|
|
180
|
+
result.declaration.attributes = attrs;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 3) === "!--") {
|
|
186
|
+
const end = xmlString.indexOf("-->", i + 3);
|
|
187
|
+
if (end === -1) break;
|
|
188
|
+
const comment = xmlString.slice(i + 3, end);
|
|
189
|
+
i = end + 3;
|
|
190
|
+
if (!ignoreComment) if (trim) addField(stack[stack.length - 1], "comment", comment.trim());
|
|
191
|
+
else addField(stack[stack.length - 1], "comment", comment);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 8) === "![CDATA[") {
|
|
195
|
+
const end = xmlString.indexOf("]]>", i + 8);
|
|
196
|
+
if (end === -1) break;
|
|
197
|
+
const cdata = xmlString.slice(i + 8, end);
|
|
198
|
+
i = end + 3;
|
|
199
|
+
if (!ignoreCdata) if (trim) addField(stack[stack.length - 1], "cdata", cdata.trim());
|
|
200
|
+
else addField(stack[stack.length - 1], "cdata", cdata);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 9) === "!DOCTYPE") {
|
|
204
|
+
const end = xmlString.indexOf(">", i + 9);
|
|
205
|
+
if (end === -1) break;
|
|
206
|
+
const doctype = xmlString.slice(i + 9, end).trim();
|
|
207
|
+
i = end + 1;
|
|
208
|
+
if (!ignoreDoctype) addField(stack[stack.length - 1], "doctype", doctype);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (xmlString.charCodeAt(i) === 47) {
|
|
212
|
+
const end = xmlString.indexOf(">", i + 1);
|
|
213
|
+
if (end === -1) break;
|
|
214
|
+
i = end + 1;
|
|
215
|
+
stack.pop();
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const tagNameEnd = findTagNameEnd(xmlString, i);
|
|
219
|
+
const tagName = xmlString.slice(i, tagNameEnd);
|
|
220
|
+
let pos = tagNameEnd;
|
|
221
|
+
const attributes = parseAttributesFromXml(xmlString, pos);
|
|
222
|
+
pos = attributes.pos;
|
|
223
|
+
if (nativeTypeAttributes) for (const key of Object.keys(attributes.attrs)) attributes.attrs[key] = nativeTypeValue(attributes.attrs[key]);
|
|
224
|
+
const isSelfClosing = xmlString.charCodeAt(pos) === 47;
|
|
225
|
+
if (isSelfClosing) pos += 2;
|
|
226
|
+
else pos++;
|
|
227
|
+
const element = {
|
|
228
|
+
type: "element",
|
|
229
|
+
name: tagName
|
|
230
|
+
};
|
|
231
|
+
if (Object.keys(attributes.attrs).length > 0) element.attributes = attributes.attrs;
|
|
232
|
+
const parent = stack[stack.length - 1];
|
|
233
|
+
if (!parent.elements) parent.elements = [];
|
|
234
|
+
parent.elements.push(element);
|
|
235
|
+
if (!isSelfClosing) stack.push(element);
|
|
236
|
+
i = pos;
|
|
237
|
+
}
|
|
238
|
+
if (result.elements) {
|
|
239
|
+
const temp = result.elements;
|
|
240
|
+
delete result.elements;
|
|
241
|
+
result.elements = temp;
|
|
242
|
+
delete result.text;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function findTagNameEnd(str, start) {
|
|
247
|
+
let i = start;
|
|
248
|
+
const len = str.length;
|
|
249
|
+
while (i < len) {
|
|
250
|
+
const ch = str.charCodeAt(i);
|
|
251
|
+
if (ch === 32 || ch === 9 || ch === 10 || ch === 13 || ch === 47 || ch === 62) return i;
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
return i;
|
|
255
|
+
}
|
|
256
|
+
function parseAttributesFromXml(str, start) {
|
|
257
|
+
const attrs = {};
|
|
258
|
+
let i = start;
|
|
259
|
+
const len = str.length;
|
|
260
|
+
while (i < len) {
|
|
261
|
+
while (i < len && isWhitespace(str.charCodeAt(i))) i++;
|
|
262
|
+
if (i >= len || str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
|
|
263
|
+
const nameStart = i;
|
|
264
|
+
while (i < len && str.charCodeAt(i) !== 61) {
|
|
265
|
+
if (str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
const name = str.slice(nameStart, i);
|
|
269
|
+
if (str.charCodeAt(i) !== 61) break;
|
|
270
|
+
i++;
|
|
271
|
+
while (i < len && isWhitespace(str.charCodeAt(i))) i++;
|
|
272
|
+
const quote = str.charCodeAt(i);
|
|
273
|
+
if (quote !== 34 && quote !== 39) break;
|
|
274
|
+
i++;
|
|
275
|
+
const valueStart = i;
|
|
276
|
+
while (i < len && str.charCodeAt(i) !== quote) i++;
|
|
277
|
+
attrs[name] = str.slice(valueStart, i);
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
attrs,
|
|
282
|
+
pos: i
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function parseAttributes(str) {
|
|
286
|
+
const result = {};
|
|
287
|
+
let i = 0;
|
|
288
|
+
const len = str.length;
|
|
289
|
+
while (i < len) {
|
|
290
|
+
while (i < len && isWhitespace(str.charCodeAt(i))) i++;
|
|
291
|
+
if (i >= len) break;
|
|
292
|
+
const nameStart = i;
|
|
293
|
+
while (i < len && str.charCodeAt(i) !== 61) {
|
|
294
|
+
if (isWhitespace(str.charCodeAt(i))) break;
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
const name = str.slice(nameStart, i);
|
|
298
|
+
while (i < len && isWhitespace(str.charCodeAt(i))) i++;
|
|
299
|
+
if (i >= len || str.charCodeAt(i) !== 61) break;
|
|
300
|
+
i++;
|
|
301
|
+
while (i < len && isWhitespace(str.charCodeAt(i))) i++;
|
|
302
|
+
const quote = str.charCodeAt(i);
|
|
303
|
+
if (quote !== 34 && quote !== 39) break;
|
|
304
|
+
i++;
|
|
305
|
+
const valueStart = i;
|
|
306
|
+
while (i < len && str.charCodeAt(i) !== quote) i++;
|
|
307
|
+
result[name] = str.slice(valueStart, i);
|
|
308
|
+
i++;
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function addField(parent, type, value) {
|
|
313
|
+
if (!parent.elements) parent.elements = [];
|
|
314
|
+
const element = { type };
|
|
315
|
+
element[type] = value;
|
|
316
|
+
parent.elements.push(element);
|
|
317
|
+
}
|
|
318
|
+
function isWhitespace(ch) {
|
|
319
|
+
return ch === 32 || ch === 9 || ch === 10 || ch === 13;
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/stringify.ts
|
|
323
|
+
function js2xml(js, options) {
|
|
324
|
+
const opts = normalizeOptions(options);
|
|
325
|
+
const parts = [];
|
|
326
|
+
if (js.declaration && !opts.ignoreDeclaration) parts.push(writeDeclaration(js.declaration));
|
|
327
|
+
if (js.elements?.length) parts.push(writeElements(js.elements, opts, 0, !parts.length));
|
|
328
|
+
return parts.join("");
|
|
329
|
+
}
|
|
330
|
+
/** Alias for js2xml — xml-js compatible export */
|
|
331
|
+
function json2xml(json, options) {
|
|
332
|
+
return js2xml(json, options);
|
|
333
|
+
}
|
|
334
|
+
function normalizeOptions(options) {
|
|
335
|
+
if (!options) return {
|
|
336
|
+
spaces: "",
|
|
337
|
+
ignoreDeclaration: false,
|
|
338
|
+
ignoreText: false,
|
|
339
|
+
ignoreComment: false,
|
|
340
|
+
ignoreCdata: false,
|
|
341
|
+
ignoreDoctype: false,
|
|
342
|
+
fullTagEmptyElement: false,
|
|
343
|
+
indentText: false,
|
|
344
|
+
indentCdata: false
|
|
345
|
+
};
|
|
346
|
+
let spaces = "";
|
|
347
|
+
if (options.spaces != null) spaces = typeof options.spaces === "number" ? " ".repeat(options.spaces) : options.spaces;
|
|
348
|
+
return {
|
|
349
|
+
spaces,
|
|
350
|
+
ignoreDeclaration: options.ignoreDeclaration ?? false,
|
|
351
|
+
ignoreText: options.ignoreText ?? false,
|
|
352
|
+
ignoreComment: options.ignoreComment ?? false,
|
|
353
|
+
ignoreCdata: options.ignoreCdata ?? false,
|
|
354
|
+
ignoreDoctype: options.ignoreDoctype ?? false,
|
|
355
|
+
fullTagEmptyElement: options.fullTagEmptyElement ?? false,
|
|
356
|
+
indentText: options.indentText ?? false,
|
|
357
|
+
indentCdata: options.indentCdata ?? false,
|
|
358
|
+
attributeValueFn: options.attributeValueFn
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function writeIndentation(spaces, depth, firstLine) {
|
|
362
|
+
return (!firstLine && spaces ? "\n" : "") + spaces.repeat(depth);
|
|
363
|
+
}
|
|
364
|
+
function writeDeclaration(declaration) {
|
|
365
|
+
const attrs = declaration.attributes;
|
|
366
|
+
if (!attrs) return "<?xml version=\"1.0\"?>";
|
|
367
|
+
let result = "<?xml version=\"1.0\"";
|
|
368
|
+
if (attrs.encoding) result += ` encoding="${attrs.encoding}"`;
|
|
369
|
+
if (attrs.standalone) result += ` standalone="${attrs.standalone}"`;
|
|
370
|
+
return result + "?>";
|
|
371
|
+
}
|
|
372
|
+
function writeAttributes(attributes, elementName, element, attributeValueFn) {
|
|
373
|
+
const parts = [];
|
|
374
|
+
for (const key of Object.keys(attributes)) {
|
|
375
|
+
const value = attributes[key];
|
|
376
|
+
if (value === null || value === void 0) continue;
|
|
377
|
+
let attr = String(value).replace(/"/g, """);
|
|
378
|
+
if (attributeValueFn) attr = attributeValueFn(attr, key, elementName, element);
|
|
379
|
+
parts.push(` ${key}="${attr}"`);
|
|
380
|
+
}
|
|
381
|
+
return parts.join("");
|
|
382
|
+
}
|
|
383
|
+
function writeElement(element, opts, depth) {
|
|
384
|
+
if (!element.name) return "";
|
|
385
|
+
const name = element.name;
|
|
386
|
+
const attrStr = element.attributes ? writeAttributes(element.attributes, name, element, opts.attributeValueFn) : "";
|
|
387
|
+
if (!((element.elements?.length ?? 0) > 0 || element.attributes?.["xml:space"] === "preserve" || opts.fullTagEmptyElement)) return `<${name}${attrStr}/>`;
|
|
388
|
+
const parts = [];
|
|
389
|
+
parts.push(`<${name}${attrStr}>`);
|
|
390
|
+
const hasChildElements = element.elements?.some((e) => e.type === "element") ?? false;
|
|
391
|
+
if (element.elements?.length) parts.push(writeElements(element.elements, opts, depth + 1, false));
|
|
392
|
+
if (opts.spaces && hasChildElements) parts.push("\n" + opts.spaces.repeat(depth));
|
|
393
|
+
parts.push(`</${name}>`);
|
|
394
|
+
return parts.join("");
|
|
395
|
+
}
|
|
396
|
+
function writeElements(elements, opts, depth, firstLine) {
|
|
397
|
+
let result = "";
|
|
398
|
+
for (let i = 0; i < elements.length; i++) {
|
|
399
|
+
const element = elements[i];
|
|
400
|
+
const isFirst = firstLine && i === 0;
|
|
401
|
+
switch (element.type) {
|
|
402
|
+
case "element":
|
|
403
|
+
result += writeIndentation(opts.spaces, depth, isFirst);
|
|
404
|
+
result += writeElement(element, opts, depth);
|
|
405
|
+
break;
|
|
406
|
+
case "text":
|
|
407
|
+
if (opts.ignoreText) continue;
|
|
408
|
+
if (opts.indentText) result += writeIndentation(opts.spaces, depth, isFirst);
|
|
409
|
+
result += writeText(element.text);
|
|
410
|
+
break;
|
|
411
|
+
case "cdata":
|
|
412
|
+
if (opts.ignoreCdata) continue;
|
|
413
|
+
if (opts.indentCdata) result += writeIndentation(opts.spaces, depth, isFirst);
|
|
414
|
+
result += writeCdata(element.cdata);
|
|
415
|
+
break;
|
|
416
|
+
case "comment":
|
|
417
|
+
if (opts.ignoreComment) continue;
|
|
418
|
+
result += writeIndentation(opts.spaces, depth, isFirst);
|
|
419
|
+
result += writeComment(element.comment);
|
|
420
|
+
break;
|
|
421
|
+
case "doctype":
|
|
422
|
+
if (opts.ignoreDoctype) continue;
|
|
423
|
+
result += writeIndentation(opts.spaces, depth, isFirst);
|
|
424
|
+
result += writeDoctype(element.doctype);
|
|
425
|
+
break;
|
|
426
|
+
default: break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
function writeText(text) {
|
|
432
|
+
if (text == null) return "";
|
|
433
|
+
return String(text).replace(/&/g, "&").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
434
|
+
}
|
|
435
|
+
function writeCdata(cdata) {
|
|
436
|
+
if (cdata == null) return "";
|
|
437
|
+
return `<![CDATA[${cdata.replace(/\]\]>/g, "]]]]><![CDATA[>")}]]>`;
|
|
438
|
+
}
|
|
439
|
+
function writeComment(comment) {
|
|
440
|
+
if (comment == null) return "";
|
|
441
|
+
return `<!--${comment}-->`;
|
|
442
|
+
}
|
|
443
|
+
function writeDoctype(doctype) {
|
|
444
|
+
if (doctype == null) return "";
|
|
445
|
+
return `<!DOCTYPE ${doctype}>`;
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/convert.ts
|
|
449
|
+
/**
|
|
450
|
+
* Convert XmlObject (node-xml format) directly to Element (xml-js format).
|
|
451
|
+
* Eliminates the redundant xml() → xml2js() bridge path.
|
|
452
|
+
*/
|
|
453
|
+
function toElement(xmlObject) {
|
|
454
|
+
const tagName = Object.keys(xmlObject)[0];
|
|
455
|
+
const value = xmlObject[tagName];
|
|
456
|
+
const element = {
|
|
457
|
+
type: "element",
|
|
458
|
+
name: tagName
|
|
459
|
+
};
|
|
460
|
+
if (value == null) return element;
|
|
461
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
462
|
+
element.elements = [{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: String(value)
|
|
465
|
+
}];
|
|
466
|
+
return element;
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
const children = [];
|
|
470
|
+
for (const item of value) if (item && typeof item === "object" && "_attr" in item) element.attributes = item._attr;
|
|
471
|
+
else if (item && typeof item === "object") if (Object.keys(item)[0] === "_cdata") children.push({
|
|
472
|
+
type: "cdata",
|
|
473
|
+
cdata: String(item._cdata)
|
|
474
|
+
});
|
|
475
|
+
else children.push(toElement(item));
|
|
476
|
+
else if (item != null) children.push({
|
|
477
|
+
type: "text",
|
|
478
|
+
text: String(item)
|
|
479
|
+
});
|
|
480
|
+
if (children.length > 0) element.elements = children;
|
|
481
|
+
return element;
|
|
482
|
+
}
|
|
483
|
+
if (typeof value === "object") {
|
|
484
|
+
if (value._attr) element.attributes = value._attr;
|
|
485
|
+
if (value._cdata) element.elements = [{
|
|
486
|
+
type: "cdata",
|
|
487
|
+
cdata: String(value._cdata)
|
|
488
|
+
}];
|
|
489
|
+
}
|
|
490
|
+
return element;
|
|
491
|
+
}
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/json.ts
|
|
494
|
+
/** Convert XML string to JSON string — xml-js compatible export */
|
|
495
|
+
function xml2json(xml, options) {
|
|
496
|
+
return JSON.stringify(xml2js(xml, options));
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
export { allChildren, attr, attrBool, attrNum, attrs, childCount, childText, children, collectText, colorAttr, escapeAttributeValue, escapeXml, findChild, findDeep, hasChild, js2xml, json2xml, nativeTypeValue, parseAttributes, textOf, toElement, unescapeXml, xml, xml2js, xml2json };
|
package/dist/utils.mjs
CHANGED
|
@@ -1 +1,116 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Find the first direct child element with the given name.
|
|
4
|
+
*/
|
|
5
|
+
function findChild(parent, name) {
|
|
6
|
+
return parent?.elements?.find((e) => e.name === name);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get all direct child elements matching the given name.
|
|
10
|
+
*/
|
|
11
|
+
function children(parent, name) {
|
|
12
|
+
return parent?.elements?.filter((e) => e.name === name) ?? [];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get all direct child elements.
|
|
16
|
+
*/
|
|
17
|
+
function allChildren(parent) {
|
|
18
|
+
return parent?.elements ?? [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get text content of the first child element with the given name.
|
|
22
|
+
*/
|
|
23
|
+
function childText(parent, name) {
|
|
24
|
+
return textOf(findChild(parent, name));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get text content of an element.
|
|
28
|
+
* Handles cases where text may be directly on .text or in a child element.
|
|
29
|
+
*/
|
|
30
|
+
function textOf(element) {
|
|
31
|
+
if (!element) return "";
|
|
32
|
+
if (element.text !== void 0 && typeof element.text === "string") return element.text;
|
|
33
|
+
if (element.elements && element.elements.length > 0) return element.elements.map((e) => typeof e.text === "string" ? e.text : "").join("");
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collect text from all direct text nodes within an element.
|
|
38
|
+
*/
|
|
39
|
+
function collectText(element) {
|
|
40
|
+
if (!element) return "";
|
|
41
|
+
const parts = [];
|
|
42
|
+
collectTextRecursive(element, parts);
|
|
43
|
+
return parts.join("");
|
|
44
|
+
}
|
|
45
|
+
function collectTextRecursive(element, parts) {
|
|
46
|
+
if (!element) return;
|
|
47
|
+
if (element.text !== void 0 && typeof element.text === "string") parts.push(element.text);
|
|
48
|
+
if (element.elements) for (const child of element.elements) collectTextRecursive(child, parts);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get an attribute value as a string.
|
|
52
|
+
*/
|
|
53
|
+
function attr(element, name) {
|
|
54
|
+
const v = element?.attributes?.[name];
|
|
55
|
+
return v !== void 0 ? String(v) : void 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get an attribute value as a number.
|
|
59
|
+
*/
|
|
60
|
+
function attrNum(element, name) {
|
|
61
|
+
const v = element?.attributes?.[name];
|
|
62
|
+
if (v === void 0) return void 0;
|
|
63
|
+
const n = Number(v);
|
|
64
|
+
return isNaN(n) ? void 0 : n;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get an attribute value as a boolean.
|
|
68
|
+
*/
|
|
69
|
+
function attrBool(element, name) {
|
|
70
|
+
const v = element?.attributes?.[name];
|
|
71
|
+
if (v === void 0) return void 0;
|
|
72
|
+
if (typeof v === "boolean") return v;
|
|
73
|
+
const lower = String(v).toLowerCase();
|
|
74
|
+
if (lower === "true" || lower === "1") return true;
|
|
75
|
+
if (lower === "false" || lower === "0") return false;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get a hex color attribute, handling nativeTypeValue coercion.
|
|
79
|
+
* nativeTypeAttributes converts "000000" → 0 (number); this recovers
|
|
80
|
+
* the original 6-digit hex string by zero-padding numeric values.
|
|
81
|
+
*/
|
|
82
|
+
function colorAttr(element, name) {
|
|
83
|
+
const raw = element?.attributes?.[name];
|
|
84
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
85
|
+
if (typeof raw === "boolean") return void 0;
|
|
86
|
+
if (typeof raw === "number") return String(raw).padStart(6, "0");
|
|
87
|
+
if (raw === "auto") return "auto";
|
|
88
|
+
if (/^[0-9A-Fa-f]{6}$/.test(raw)) return raw;
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if an element has a specific child element.
|
|
93
|
+
*/
|
|
94
|
+
function hasChild(parent, name) {
|
|
95
|
+
return parent?.elements?.some((e) => e.name === name) ?? false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find deep descendant elements matching the given name.
|
|
99
|
+
*/
|
|
100
|
+
function findDeep(parent, name) {
|
|
101
|
+
const result = [];
|
|
102
|
+
if (!parent) return result;
|
|
103
|
+
for (const child of parent.elements ?? []) {
|
|
104
|
+
if (child.name === name) result.push(child);
|
|
105
|
+
result.push(...findDeep(child, name));
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the number of direct child elements.
|
|
111
|
+
*/
|
|
112
|
+
function childCount(parent) {
|
|
113
|
+
return parent?.elements?.length ?? 0;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { allChildren, attr, attrBool, attrNum, childCount, childText, children, collectText, colorAttr, findChild, findDeep, hasChild, textOf };
|