@office-open/xml 0.10.15 → 0.12.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/dist/index.mjs CHANGED
@@ -1,200 +1,4 @@
1
- import { allChildren, attr, attrBool, attrMeasure, attrNum, childCount, childText, children, collectText, colorAttr, findChild, findDeep, findFirst, hasChild, isNonEmpty, textOf } from "./utils.mjs";
2
- //#region src/escape.ts
3
- /** Escape text content for XML. Fast path returns original string when no special chars. */
4
- function escapeXml(str) {
5
- for (let i = 0; i < str.length; i++) {
6
- const c = str.charCodeAt(i);
7
- if (c === 38 || c === 34 || c === 39 || c === 60 || c === 62) {
8
- let s = "";
9
- let last = 0;
10
- for (let j = i; j < str.length; j++) {
11
- const cj = str.charCodeAt(j);
12
- if (cj === 38) {
13
- s += str.slice(last, j) + "&amp;";
14
- last = j + 1;
15
- } else if (cj === 34) {
16
- s += str.slice(last, j) + "&quot;";
17
- last = j + 1;
18
- } else if (cj === 39) {
19
- s += str.slice(last, j) + "&apos;";
20
- last = j + 1;
21
- } else if (cj === 60) {
22
- s += str.slice(last, j) + "&lt;";
23
- last = j + 1;
24
- } else if (cj === 62) {
25
- s += str.slice(last, j) + "&gt;";
26
- last = j + 1;
27
- }
28
- }
29
- return s + str.slice(last);
30
- }
31
- }
32
- return str;
33
- }
34
- /**
35
- * Build an XML attribute string fragment from a record.
36
- * `undefined` values are automatically skipped.
37
- * String values are escaped via `escapeXml`.
38
- *
39
- * @example
40
- * attrs({ id: 1, name: "foo", hidden: undefined })
41
- * // => ' id="1" name="foo"'
42
- */
43
- function attrs(record) {
44
- const parts = [];
45
- for (const [key, v] of Object.entries(record)) if (v !== void 0) parts.push(` ${key}="${typeof v === "string" ? escapeXml(v) : v}"`);
46
- return parts.join("");
47
- }
48
- /**
49
- * Build an XML attribute string without escaping.
50
- *
51
- * Same as `attrs()` but skips `typeof` checks and `escapeXml` — use only when
52
- * all values are known-safe (numbers, booleans, or strings free of `& " ' < >`).
53
- * Avoids per-call array and `Object.keys()` allocation in hot loops.
54
- *
55
- * @example
56
- * attrsRaw({ r: "A1", s: 5 })
57
- * // => ' r="A1" s="5"'
58
- */
59
- function attrsRaw(record) {
60
- let s = "";
61
- for (const key in record) {
62
- const v = record[key];
63
- if (v !== void 0) s += ` ${key}="${v}"`;
64
- }
65
- return s;
66
- }
67
- /**
68
- * Build a self-closing XML element: `<tag attrStr/>`.
69
- * `attrStr` is a pre-serialized attribute string (from `attrs()`) or undefined.
70
- */
71
- function selfCloseElement(tag, attrStr) {
72
- return attrStr ? `<${tag}${attrStr}/>` : `<${tag}/>`;
73
- }
74
- /**
75
- * Build a complete XML element string from name, optional attributes, and string children.
76
- *
77
- * Replaces `new BuilderElement({...})` + `.toXml()` / `.serialize()` with a
78
- * single function call returning a string — zero object allocation.
79
- *
80
- * @param name Element tag name (e.g. `"a:srgbClr"`)
81
- * @param attrRecord Optional flat attribute map; `undefined` values are skipped
82
- * @param children Optional pre-serialized child XML strings
83
- *
84
- * @example
85
- * ```ts
86
- * element("a:solidFill", undefined, [element("a:srgbClr", { val: "FF0000" })])
87
- * // => '<a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>'
88
- * ```
89
- */
90
- function element(name, attrRecord, children) {
91
- const attrStr = attrRecord ? attrs(attrRecord) : void 0;
92
- if (!children || children.length === 0) return selfCloseElement(name, attrStr);
93
- const body = children.join("");
94
- return body.length === 0 ? selfCloseElement(name, attrStr) : `<${name}${attrStr ?? ""}>${body}</${name}>`;
95
- }
96
- //#endregion
97
- //#region src/serialize.ts
98
- const DEFAULT_INDENT = " ";
99
- /**
100
- * Serialize a Record-based XML object tree to an XML string.
101
- * @deprecated Use `stringify` (Element → string) instead. This object-tree path
102
- * will be removed once the Descriptor migration is complete.
103
- */
104
- function xml(input, options) {
105
- const opts = normalizeOptions$1(options);
106
- const parts = [];
107
- if (opts.declaration) {
108
- const declOpts = opts.declaration === true ? {} : opts.declaration;
109
- const enc = declOpts.encoding || "UTF-8";
110
- const sa = declOpts.standalone;
111
- const declParts = [`<?xml version="1.0" encoding="${enc}"`];
112
- if (sa) declParts.push(` standalone="${sa}"`);
113
- declParts.push("?>");
114
- parts.push(declParts.join(""));
115
- if (opts.indent) parts.push("\n");
116
- }
117
- const items = Array.isArray(input) ? input : [input];
118
- for (let i = 0; i < items.length; i++) {
119
- const item = items[i];
120
- if (!item) continue;
121
- const key = Object.keys(item)[0];
122
- if (!key) continue;
123
- parts.push(formatElement(key, item[key], opts.indent, 0));
124
- if (opts.indent && i < items.length - 1) parts.push("\n");
125
- }
126
- return parts.join("");
127
- }
128
- function normalizeOptions$1(options) {
129
- const opts = typeof options === "object" && !Array.isArray(options) ? options : { indent: options };
130
- let indent = "";
131
- if (opts.indent) indent = opts.indent === true ? DEFAULT_INDENT : String(opts.indent);
132
- return {
133
- indent,
134
- declaration: opts.declaration
135
- };
136
- }
137
- /**
138
- * Single-pass XML formatter: directly converts a Record-based XML object to string,
139
- * eliminating the intermediate ResolvedElement tree.
140
- */
141
- function formatElement(name, values, indent, depth) {
142
- const attrParts = [];
143
- const textParts = [];
144
- const elemParts = [];
145
- let emptyArray = false;
146
- if (values == null) {
147
- const attrStr = attrParts.length ? " " + attrParts.join(" ") : "";
148
- return `${indent ? indent.repeat(depth) : ""}<${name}${attrStr}/>`;
149
- }
150
- if (typeof values === "object") {
151
- const obj = values;
152
- if (obj._attr) {
153
- const attr = obj._attr;
154
- for (const key of Object.keys(attr)) attrParts.push(`${key}="${escapeXml(String(attr[key]))}"`);
155
- }
156
- if (obj._attributes) {
157
- const attr = obj._attributes;
158
- for (const key of Object.keys(attr)) attrParts.push(`${key}="${escapeXml(String(attr[key]))}"`);
159
- }
160
- if (obj._cdata) {
161
- const escaped = String(obj._cdata).replace(/\]\]>/g, "]]]]><![CDATA[>");
162
- textParts.push(`<![CDATA[${escaped}]]>`);
163
- }
164
- if (Array.isArray(values)) {
165
- if (values.length === 0) emptyArray = true;
166
- else for (const value of values) if (value && typeof value === "object" && "_attr" in value) {
167
- const attr = value._attr;
168
- for (const key of Object.keys(attr)) attrParts.push(`${key}="${escapeXml(String(attr[key]))}"`);
169
- } else if (value && typeof value === "object" && "_attributes" in value) {
170
- const attr = value._attributes;
171
- for (const key of Object.keys(attr)) attrParts.push(`${key}="${escapeXml(String(attr[key]))}"`);
172
- } else if (value && typeof value === "object") {
173
- const childKey = Object.keys(value)[0];
174
- if (childKey) elemParts.push(formatElement(childKey, value[childKey], indent, depth + 1));
175
- } else if (value != null) textParts.push(escapeXml(String(value)));
176
- }
177
- } else textParts.push(escapeXml(String(values)));
178
- const ind = indent ? indent.repeat(depth) : "";
179
- const attrStr = attrParts.length ? " " + attrParts.join(" ") : "";
180
- if (textParts.length + elemParts.length === 0) return emptyArray ? `${ind}<${name}${attrStr}></${name}>` : `${ind}<${name}${attrStr}/>`;
181
- if (elemParts.length === 0 && textParts.length === 1) return indent ? `${ind}<${name}${attrStr}>${textParts[0]}</${name}>` : `<${name}${attrStr}>${textParts[0]}</${name}>`;
182
- const parts = [];
183
- parts.push(`${ind}<${name}${attrStr}>`);
184
- if (indent) parts.push("\n");
185
- const childIndent = indent ? indent.repeat(depth + 1) : "";
186
- for (const t of textParts) {
187
- parts.push(`${childIndent}${t}`);
188
- if (indent) parts.push("\n");
189
- }
190
- for (const e of elemParts) {
191
- parts.push(e);
192
- if (indent) parts.push("\n");
193
- }
194
- parts.push(`${ind}</${name}>`);
195
- return parts.join("");
196
- }
197
- //#endregion
1
+ import { OOXML_XML_DECLARATION, allChildren, attr, attrBool, attrMeasure, attrNum, childCount, childText, children, collectText, colorAttr, findChild, findDeep, findFirst, hasChild, isNonEmpty, textOf } from "./utils.mjs";
198
2
  //#region src/parse.ts
199
3
  const ENTITY_MAP = {
200
4
  "&amp;": "&",
@@ -205,6 +9,7 @@ const ENTITY_MAP = {
205
9
  };
206
10
  const ENTITY_PATTERN = /&(?:amp|lt|gt|quot|apos|#x[0-9a-fA-F]+|#[0-9]+);/g;
207
11
  function unescapeXml(str) {
12
+ if (str.indexOf("&") === -1) return str;
208
13
  return str.replace(ENTITY_PATTERN, (match) => {
209
14
  if (ENTITY_MAP[match] !== void 0) return ENTITY_MAP[match];
210
15
  const body = match.slice(2, -1);
@@ -214,11 +19,31 @@ function unescapeXml(str) {
214
19
  }
215
20
  function nativeTypeValue(value) {
216
21
  if (value === "") return value;
22
+ const neg = value.charCodeAt(0) === 45;
23
+ const start = neg ? 1 : 0;
24
+ const digits = value.length - start;
25
+ if (digits > 0 && digits <= 15) {
26
+ if (value.charCodeAt(start) !== 48 || !neg && digits === 1) {
27
+ let n = 0;
28
+ let allDigits = true;
29
+ for (let i = start; i < value.length; i++) {
30
+ const c = value.charCodeAt(i);
31
+ if (c < 48 || c > 57) {
32
+ allDigits = false;
33
+ break;
34
+ }
35
+ n = n * 10 + (c - 48);
36
+ }
37
+ if (allDigits) return neg ? -n : n;
38
+ }
39
+ }
217
40
  const n = Number(value);
218
41
  if (!isNaN(n) && String(n) === value) return n;
219
- const lower = value.toLowerCase();
220
- if (lower === "true") return true;
221
- if (lower === "false") return false;
42
+ if (value.length === 4 || value.length === 5) {
43
+ const lower = value.toLowerCase();
44
+ if (lower === "true") return true;
45
+ if (lower === "false") return false;
46
+ }
222
47
  return value;
223
48
  }
224
49
  function parse(xmlString, options) {
@@ -230,6 +55,12 @@ function parse(xmlString, options) {
230
55
  const ignoreCdata = options?.ignoreCdata ?? false;
231
56
  const ignoreDoctype = options?.ignoreDoctype ?? false;
232
57
  const nativeTypeAttributes = options?.nativeTypeAttributes ?? false;
58
+ const deferSet = options?.deferElements !== void 0 && options.deferElements.length > 0 ? new Set(options.deferElements) : void 0;
59
+ const nsTable = options?.normalizeNamespaces !== void 0 ? new Map(Object.entries(options.normalizeNamespaces)) : void 0;
60
+ const nsStack = [{
61
+ prefixes: void 0,
62
+ defaultCanonical: void 0
63
+ }];
233
64
  const result = {};
234
65
  const stack = [result];
235
66
  let i = 0;
@@ -237,12 +68,26 @@ function parse(xmlString, options) {
237
68
  while (i < len) {
238
69
  if (xmlString.charCodeAt(i) !== 60) {
239
70
  const start = i;
240
- while (i < len && xmlString.charCodeAt(i) !== 60) i++;
71
+ const lt = xmlString.indexOf("<", i);
72
+ i = lt === -1 ? len : lt;
241
73
  let text = unescapeXml(xmlString.slice(start, i));
242
74
  if (trim) text = text.trim();
243
75
  if (ignoreText) continue;
244
76
  if (text.length > 0) {
245
- if (captureSpaces || text.trim().length > 0 || isPreserveContext(stack)) addField(peek(stack), "text", text);
77
+ if (captureSpaces || text.trim().length > 0 || isPreserveContext(stack)) {
78
+ const parent = stack[stack.length - 1];
79
+ const elements = parent.elements;
80
+ const last = elements === void 0 ? void 0 : elements[elements.length - 1];
81
+ if (last !== void 0 && last.type === "text") last.text = last.text + text;
82
+ else {
83
+ const node = {
84
+ type: "text",
85
+ text
86
+ };
87
+ if (elements === void 0) parent.elements = [node];
88
+ else elements.push(node);
89
+ }
90
+ }
246
91
  }
247
92
  continue;
248
93
  }
@@ -268,8 +113,10 @@ function parse(xmlString, options) {
268
113
  if (end === -1) break;
269
114
  const comment = xmlString.slice(i + 3, end);
270
115
  i = end + 3;
271
- if (!ignoreComment) if (trim) addField(peek(stack), "comment", comment.trim());
272
- else addField(peek(stack), "comment", comment);
116
+ if (!ignoreComment) {
117
+ if (trim) addField(peek(stack), "comment", comment.trim());
118
+ else addField(peek(stack), "comment", comment);
119
+ }
273
120
  continue;
274
121
  }
275
122
  if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 8) === "![CDATA[") {
@@ -277,8 +124,10 @@ function parse(xmlString, options) {
277
124
  if (end === -1) break;
278
125
  const cdata = xmlString.slice(i + 8, end);
279
126
  i = end + 3;
280
- if (!ignoreCdata) if (trim) addField(peek(stack), "cdata", cdata.trim());
281
- else addField(peek(stack), "cdata", cdata);
127
+ if (!ignoreCdata) {
128
+ if (trim) addField(peek(stack), "cdata", cdata.trim());
129
+ else addField(peek(stack), "cdata", cdata);
130
+ }
282
131
  continue;
283
132
  }
284
133
  if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 9) === "!DOCTYPE") {
@@ -294,26 +143,136 @@ function parse(xmlString, options) {
294
143
  if (end === -1) break;
295
144
  i = end + 1;
296
145
  stack.pop();
146
+ if (nsTable !== void 0) nsStack.pop();
297
147
  continue;
298
148
  }
299
149
  const tagNameEnd = findTagNameEnd(xmlString, i);
300
150
  const tagName = xmlString.slice(i, tagNameEnd);
301
151
  let pos = tagNameEnd;
302
- const attributes = parseAttributesFromXml(xmlString, pos);
303
- pos = attributes.pos;
304
- if (nativeTypeAttributes) for (const key in attributes.attrs) attributes.attrs[key] = nativeTypeValue(attributes.attrs[key]);
152
+ const nsParent = nsTable !== void 0 ? nsStack[nsStack.length - 1] : void 0;
153
+ let nsPrefixes = nsParent?.prefixes;
154
+ let nsDefault = nsParent?.defaultCanonical;
155
+ let nsDeclared = false;
156
+ let attrs;
157
+ while (pos < len) {
158
+ while (pos < len && isWhitespace(xmlString.charCodeAt(pos))) pos++;
159
+ if (pos >= len || xmlString.charCodeAt(pos) === 62 || xmlString.charCodeAt(pos) === 47) break;
160
+ const nameStart = pos;
161
+ while (pos < len && xmlString.charCodeAt(pos) !== 61) {
162
+ if (xmlString.charCodeAt(pos) === 62 || xmlString.charCodeAt(pos) === 47) break;
163
+ pos++;
164
+ }
165
+ const name = xmlString.slice(nameStart, pos).trim();
166
+ if (xmlString.charCodeAt(pos) !== 61) break;
167
+ pos++;
168
+ while (pos < len && isWhitespace(xmlString.charCodeAt(pos))) pos++;
169
+ const quote = xmlString.charCodeAt(pos);
170
+ if (quote !== 34 && quote !== 39) break;
171
+ pos++;
172
+ const valueStart = pos;
173
+ while (pos < len && xmlString.charCodeAt(pos) !== quote) pos++;
174
+ const value = unescapeXml(xmlString.slice(valueStart, pos));
175
+ pos++;
176
+ if (nsTable !== void 0 && (name === "xmlns" || name.startsWith("xmlns:"))) {
177
+ if (attrs === void 0) attrs = {};
178
+ const canonical = nsTable.get(value);
179
+ if (name === "xmlns") {
180
+ nsDefault = canonical;
181
+ if (canonical !== void 0 && canonical !== "") attrs[`xmlns:${canonical}`] = value;
182
+ else attrs[name] = value;
183
+ } else {
184
+ const prefix = name.slice(6);
185
+ if (canonical !== void 0 && canonical !== "") attrs[`xmlns:${canonical}`] = value;
186
+ else if (canonical === "" && !("xmlns" in attrs)) attrs.xmlns = value;
187
+ else attrs[name] = value;
188
+ if (!nsDeclared) {
189
+ nsPrefixes = new Map(nsPrefixes ?? []);
190
+ nsDeclared = true;
191
+ }
192
+ nsPrefixes.set(prefix, canonical ?? prefix);
193
+ }
194
+ continue;
195
+ }
196
+ if (attrs === void 0) attrs = {};
197
+ attrs[name] = value;
198
+ }
199
+ if (attrs && nativeTypeAttributes) for (const key in attrs) attrs[key] = nativeTypeValue(attrs[key]);
200
+ let finalName = tagName;
201
+ if (nsTable !== void 0) {
202
+ const colon = tagName.indexOf(":");
203
+ if (colon > 0) {
204
+ const p = tagName.slice(0, colon);
205
+ if (p !== "xml") {
206
+ const c = nsPrefixes?.get(p);
207
+ if (c !== void 0 && c !== p) finalName = c === "" ? tagName.slice(colon + 1) : c + tagName.slice(colon);
208
+ }
209
+ } else if (colon < 0 && nsDefault !== void 0 && nsDefault !== "") finalName = nsDefault + ":" + tagName;
210
+ if (attrs !== void 0 && nsPrefixes !== void 0) for (const key of Object.keys(attrs)) {
211
+ if (key === "xmlns" || key.startsWith("xmlns:")) continue;
212
+ const kcolon = key.indexOf(":");
213
+ if (kcolon <= 0) continue;
214
+ const p = key.slice(0, kcolon);
215
+ if (p === "xml") continue;
216
+ const c = nsPrefixes.get(p);
217
+ if (c !== void 0 && c !== p) {
218
+ const renamed = c === "" ? key.slice(kcolon + 1) : c + key.slice(kcolon);
219
+ const attrValue = attrs[key];
220
+ if (attrValue !== void 0) attrs[renamed] = attrValue;
221
+ delete attrs[key];
222
+ }
223
+ }
224
+ }
305
225
  const isSelfClosing = xmlString.charCodeAt(pos) === 47;
306
226
  if (isSelfClosing) pos += 2;
307
227
  else pos++;
308
228
  const element = {
309
229
  type: "element",
310
- name: tagName
230
+ name: finalName,
231
+ attributes: attrs,
232
+ elements: void 0
311
233
  };
312
- if (Object.keys(attributes.attrs).length > 0) element.attributes = attributes.attrs;
313
234
  const parent = peek(stack);
314
235
  if (!parent.elements) parent.elements = [];
315
236
  parent.elements.push(element);
316
- if (!isSelfClosing) stack.push(element);
237
+ if (!isSelfClosing) {
238
+ if (deferSet !== void 0 && deferSet.has(tagName)) {
239
+ const closeTag = `</${tagName}>`;
240
+ let depth = 1;
241
+ let scan = pos;
242
+ let closeIdx = -1;
243
+ for (;;) {
244
+ closeIdx = xmlString.indexOf(closeTag, scan);
245
+ if (closeIdx === -1) break;
246
+ let p = scan;
247
+ for (;;) {
248
+ const openIdx = xmlString.indexOf(`<${tagName}`, p);
249
+ if (openIdx === -1 || openIdx >= closeIdx) break;
250
+ const after = xmlString.charCodeAt(openIdx + tagName.length + 1);
251
+ if (after === 32 || after === 9 || after === 10 || after === 13 || after === 47 || after === 62) depth++;
252
+ p = openIdx + tagName.length + 1;
253
+ }
254
+ scan = closeIdx + closeTag.length;
255
+ depth--;
256
+ if (depth === 0) break;
257
+ }
258
+ if (closeIdx === -1) {
259
+ element.raw = xmlString.slice(pos);
260
+ i = len;
261
+ } else {
262
+ element.raw = xmlString.slice(pos, closeIdx);
263
+ i = scan;
264
+ }
265
+ continue;
266
+ }
267
+ stack.push(element);
268
+ if (nsTable !== void 0) {
269
+ const layerChanged = nsDeclared || nsDefault !== nsParent.defaultCanonical;
270
+ nsStack.push(layerChanged ? {
271
+ prefixes: nsPrefixes,
272
+ defaultCanonical: nsDefault
273
+ } : nsParent);
274
+ }
275
+ }
317
276
  i = pos;
318
277
  }
319
278
  if (result.elements) {
@@ -334,35 +293,6 @@ function findTagNameEnd(str, start) {
334
293
  }
335
294
  return i;
336
295
  }
337
- function parseAttributesFromXml(str, start) {
338
- const attrs = {};
339
- let i = start;
340
- const len = str.length;
341
- while (i < len) {
342
- while (i < len && isWhitespace(str.charCodeAt(i))) i++;
343
- if (i >= len || str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
344
- const nameStart = i;
345
- while (i < len && str.charCodeAt(i) !== 61) {
346
- if (str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
347
- i++;
348
- }
349
- const name = str.slice(nameStart, i);
350
- if (str.charCodeAt(i) !== 61) break;
351
- i++;
352
- while (i < len && isWhitespace(str.charCodeAt(i))) i++;
353
- const quote = str.charCodeAt(i);
354
- if (quote !== 34 && quote !== 39) break;
355
- i++;
356
- const valueStart = i;
357
- while (i < len && str.charCodeAt(i) !== quote) i++;
358
- attrs[name] = unescapeXml(str.slice(valueStart, i));
359
- i++;
360
- }
361
- return {
362
- attrs,
363
- pos: i
364
- };
365
- }
366
296
  function parseAttributes(str) {
367
297
  const result = {};
368
298
  let i = 0;
@@ -429,7 +359,83 @@ function isWhitespace(ch) {
429
359
  return ch === 32 || ch === 9 || ch === 10 || ch === 13;
430
360
  }
431
361
  //#endregion
362
+ //#region src/escape.ts
363
+ const XML_SPECIALS = /[&"'<>]/;
364
+ /** Escape text content for XML. Fast path returns original string when no special chars. */
365
+ function escapeXml(str) {
366
+ if (!XML_SPECIALS.test(str)) return str;
367
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
368
+ }
369
+ /**
370
+ * Build an XML attribute string fragment from a record.
371
+ * `undefined` values are automatically skipped.
372
+ * String values are escaped via `escapeXml`. Booleans serialize as 0/1 —
373
+ * the spelling Office itself writes for xsd:boolean attributes and
374
+ * ST_OnOff unions alike.
375
+ *
376
+ * @example
377
+ * attrs({ id: 1, name: "foo", hidden: undefined })
378
+ * // => ' id="1" name="foo"'
379
+ */
380
+ function attrs(record) {
381
+ const parts = [];
382
+ for (const [key, v] of Object.entries(record)) if (v !== void 0) {
383
+ const value = typeof v === "string" ? escapeXml(v) : typeof v === "boolean" ? v ? 1 : 0 : v;
384
+ parts.push(` ${key}="${value}"`);
385
+ }
386
+ return parts.join("");
387
+ }
388
+ /**
389
+ * Build an XML attribute string without escaping.
390
+ *
391
+ * Same as `attrs()` but skips `typeof` checks and `escapeXml` — use only when
392
+ * all values are known-safe (numbers, booleans, or strings free of `& " ' < >`).
393
+ * Avoids per-call array and `Object.keys()` allocation in hot loops.
394
+ *
395
+ * @example
396
+ * attrsRaw({ r: "A1", s: 5 })
397
+ * // => ' r="A1" s="5"'
398
+ */
399
+ function attrsRaw(record) {
400
+ let s = "";
401
+ for (const key in record) {
402
+ const v = record[key];
403
+ if (v !== void 0) s += ` ${key}="${v}"`;
404
+ }
405
+ return s;
406
+ }
407
+ /**
408
+ * Build a self-closing XML element: `<tag attrStr/>`.
409
+ * `attrStr` is a pre-serialized attribute string (from `attrs()`) or undefined.
410
+ */
411
+ function selfCloseElement(tag, attrStr) {
412
+ return attrStr ? `<${tag}${attrStr}/>` : `<${tag}/>`;
413
+ }
414
+ /**
415
+ * Build a complete XML element string from name, optional attributes, and string children.
416
+ *
417
+ * Replaces `new BuilderElement({...})` + `.toXml()` / `.serialize()` with a
418
+ * single function call returning a string — zero object allocation.
419
+ *
420
+ * @param name Element tag name (e.g. `"a:srgbClr"`)
421
+ * @param attrRecord Optional flat attribute map; `undefined` values are skipped
422
+ * @param children Optional pre-serialized child XML strings
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * element("a:solidFill", undefined, [element("a:srgbClr", { val: "FF0000" })])
427
+ * // => '<a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>'
428
+ * ```
429
+ */
430
+ function element(name, attrRecord, children) {
431
+ const attrStr = attrRecord ? attrs(attrRecord) : void 0;
432
+ if (!children || children.length === 0) return selfCloseElement(name, attrStr);
433
+ const body = children.join("");
434
+ return body.length === 0 ? selfCloseElement(name, attrStr) : `<${name}${attrStr ?? ""}>${body}</${name}>`;
435
+ }
436
+ //#endregion
432
437
  //#region src/stringify.ts
438
+ const TEXT_SPECIALS = /[&<>]/;
433
439
  function stringify(js, options) {
434
440
  const opts = normalizeOptions(options);
435
441
  const parts = [];
@@ -437,22 +443,19 @@ function stringify(js, options) {
437
443
  if (js.elements?.length) parts.push(writeElements(js.elements, opts, 0, !parts.length));
438
444
  return parts.join("");
439
445
  }
440
- /** @deprecated Use `stringify` instead. xml-js compatible alias. */
441
- function json2xml(json, options) {
442
- return stringify(json, options);
443
- }
446
+ const DEFAULT_OPTIONS = {
447
+ spaces: "",
448
+ ignoreDeclaration: false,
449
+ ignoreText: false,
450
+ ignoreComment: false,
451
+ ignoreCdata: false,
452
+ ignoreDoctype: false,
453
+ fullTagEmptyElement: false,
454
+ indentText: false,
455
+ indentCdata: false
456
+ };
444
457
  function normalizeOptions(options) {
445
- if (!options) return {
446
- spaces: "",
447
- ignoreDeclaration: false,
448
- ignoreText: false,
449
- ignoreComment: false,
450
- ignoreCdata: false,
451
- ignoreDoctype: false,
452
- fullTagEmptyElement: false,
453
- indentText: false,
454
- indentCdata: false
455
- };
458
+ if (!options) return DEFAULT_OPTIONS;
456
459
  let spaces = "";
457
460
  if (options.spaces != null) spaces = typeof options.spaces === "number" ? " ".repeat(options.spaces) : options.spaces;
458
461
  return {
@@ -469,7 +472,8 @@ function normalizeOptions(options) {
469
472
  };
470
473
  }
471
474
  function writeIndentation(spaces, depth, firstLine) {
472
- return (!firstLine && spaces ? "\n" : "") + spaces.repeat(depth);
475
+ if (!spaces) return "";
476
+ return (!firstLine ? "\n" : "") + spaces.repeat(depth);
473
477
  }
474
478
  function writeDeclaration(declaration) {
475
479
  const attrs = declaration.attributes;
@@ -480,90 +484,66 @@ function writeDeclaration(declaration) {
480
484
  return parts.join("") + "?>";
481
485
  }
482
486
  function writeAttributes(attributes, elementName, element, attributeValueFn) {
483
- const parts = [];
484
- for (const key of Object.keys(attributes)) {
487
+ let s = "";
488
+ for (const key in attributes) {
485
489
  const value = attributes[key];
486
490
  if (value === null || value === void 0) continue;
487
491
  const raw = String(value);
488
492
  const attr = attributeValueFn ? attributeValueFn(raw, key, elementName, element) : escapeXml(raw);
489
- parts.push(` ${key}="${attr}"`);
493
+ s += ` ${key}="${attr}"`;
490
494
  }
491
- return parts.join("");
492
- }
493
- function writeElement(element, opts, depth) {
494
- if (!element.name) return "";
495
- const name = element.name;
496
- const attrStr = element.attributes ? writeAttributes(element.attributes, name, element, opts.attributeValueFn) : "";
497
- if (!((element.elements?.length ?? 0) > 0 || element.attributes?.["xml:space"] === "preserve" || opts.fullTagEmptyElement)) return `<${name}${attrStr}/>`;
498
- const parts = [];
499
- parts.push(`<${name}${attrStr}>`);
500
- const hasChildElements = element.elements?.some((e) => e.type === "element") ?? false;
501
- if (element.elements?.length) parts.push(writeElements(element.elements, opts, depth + 1, false));
502
- if (opts.spaces && hasChildElements) parts.push("\n" + opts.spaces.repeat(depth));
503
- parts.push(`</${name}>`);
504
- return parts.join("");
495
+ return s;
505
496
  }
506
497
  function writeElements(elements, opts, depth, firstLine) {
507
- const parts = [];
498
+ let s = "";
508
499
  for (let i = 0; i < elements.length; i++) {
509
500
  const element = elements[i];
510
501
  if (!element) continue;
511
502
  const isFirst = firstLine && i === 0;
512
- switch (element.type) {
513
- case "element":
514
- parts.push(writeIndentation(opts.spaces, depth, isFirst));
515
- parts.push(writeElement(element, opts, depth));
516
- break;
517
- case "text":
518
- if (opts.ignoreText) continue;
519
- if (opts.indentText) parts.push(writeIndentation(opts.spaces, depth, isFirst));
520
- parts.push(writeText(element.text));
521
- break;
522
- case "cdata":
523
- if (opts.ignoreCdata) continue;
524
- if (opts.indentCdata) parts.push(writeIndentation(opts.spaces, depth, isFirst));
525
- parts.push(writeCdata(element.cdata));
526
- break;
527
- case "comment":
528
- if (opts.ignoreComment) continue;
529
- parts.push(writeIndentation(opts.spaces, depth, isFirst));
530
- parts.push(writeComment(element.comment));
531
- break;
532
- case "doctype":
533
- if (opts.ignoreDoctype) continue;
534
- parts.push(writeIndentation(opts.spaces, depth, isFirst));
535
- parts.push(writeDoctype(element.doctype));
536
- break;
537
- default: break;
538
- }
539
- }
540
- return parts.join("");
541
- }
542
- function writeText(text) {
543
- if (text == null) return "";
544
- const str = String(text);
545
- for (let i = 0; i < str.length; i++) {
546
- const c = str.charCodeAt(i);
547
- if (c === 38 || c === 60 || c === 62) {
548
- let s = "";
549
- let last = 0;
550
- for (let j = i; j < str.length; j++) {
551
- const cj = str.charCodeAt(j);
552
- if (cj === 38) {
553
- s += str.slice(last, j) + "&amp;";
554
- last = j + 1;
555
- } else if (cj === 60) {
556
- s += str.slice(last, j) + "&lt;";
557
- last = j + 1;
558
- } else if (cj === 62) {
559
- s += str.slice(last, j) + "&gt;";
560
- last = j + 1;
561
- }
503
+ const type = element.type;
504
+ if (type === "element") {
505
+ const name = element.name;
506
+ if (!name) continue;
507
+ if (opts.spaces) s += writeIndentation(opts.spaces, depth, isFirst);
508
+ const attributes = element.attributes;
509
+ const attrStr = attributes ? writeAttributes(attributes, name, element, opts.attributeValueFn) : "";
510
+ if (element.raw !== void 0) {
511
+ s += `<${name}${attrStr}>${element.raw}</${name}>`;
512
+ continue;
513
+ }
514
+ const children = element.elements;
515
+ if (!(children !== void 0 && children.length > 0 || opts.fullTagEmptyElement || attributes?.["xml:space"] === "preserve")) {
516
+ s += `<${name}${attrStr}/>`;
517
+ continue;
562
518
  }
563
- return s + str.slice(last);
519
+ const open = `<${name}${attrStr}>`;
520
+ if (children !== void 0 && children.length > 0) {
521
+ const inner = writeElements(children, opts, depth + 1, false);
522
+ if (opts.spaces && children.some((e) => e.type === "element")) s += open + inner + "\n" + opts.spaces.repeat(depth) + `</${name}>`;
523
+ else s += open + inner + `</${name}>`;
524
+ } else s += open + `</${name}>`;
525
+ } else if (type === "text") {
526
+ if (opts.ignoreText) continue;
527
+ if (opts.indentText && opts.spaces) s += writeIndentation(opts.spaces, depth, isFirst);
528
+ const text = element.text;
529
+ if (text == null) continue;
530
+ const str = String(text);
531
+ s += TEXT_SPECIALS.test(str) ? str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") : str;
532
+ } else if (type === "cdata") {
533
+ if (opts.ignoreCdata) continue;
534
+ if (opts.indentCdata && opts.spaces) s += writeIndentation(opts.spaces, depth, isFirst);
535
+ s += writeCdata(element.cdata);
536
+ } else if (type === "comment") {
537
+ if (opts.ignoreComment) continue;
538
+ if (opts.spaces) s += writeIndentation(opts.spaces, depth, isFirst);
539
+ s += writeComment(element.comment);
540
+ } else if (type === "doctype") {
541
+ if (opts.ignoreDoctype) continue;
542
+ if (opts.spaces) s += writeIndentation(opts.spaces, depth, isFirst);
543
+ s += writeDoctype(element.doctype);
564
544
  }
565
545
  }
566
- return str;
546
+ return s;
567
547
  }
568
548
  function writeCdata(cdata) {
569
549
  if (cdata == null) return "";
@@ -578,62 +558,26 @@ function writeDoctype(doctype) {
578
558
  return `<!DOCTYPE ${doctype}>`;
579
559
  }
580
560
  //#endregion
581
- //#region src/convert.ts
561
+ //#region src/stringify-element.ts
582
562
  /**
583
- * Convert XmlObject (node-xml format) directly to Element (xml-js format).
584
- * Eliminates the redundant xml() → xml2js() bridge path.
563
+ * Serialize an Element including its own opening/closing tag.
564
+ *
565
+ * `stringify` serializes only an element's children (it treats its input as
566
+ * a document root). Raw-XML round-trip of whole elements needs the element's
567
+ * own tag wrapped around its serialized children.
585
568
  */
586
- function toElement(xmlObject) {
587
- const tagName = Object.keys(xmlObject)[0];
588
- if (!tagName) return {
589
- type: "element",
590
- name: ""
591
- };
592
- const value = xmlObject[tagName];
593
- const element = {
594
- type: "element",
595
- name: tagName
596
- };
597
- if (value == null) return element;
598
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
599
- element.elements = [{
600
- type: "text",
601
- text: String(value)
602
- }];
603
- return element;
604
- }
605
- if (Array.isArray(value)) {
606
- const children = [];
607
- for (const item of value) if (item && typeof item === "object" && "_attr" in item) element.attributes = item._attr;
608
- else if (item && typeof item === "object") if (Object.keys(item)[0] === "_cdata") children.push({
609
- type: "cdata",
610
- cdata: String(item._cdata)
611
- });
612
- else children.push(toElement(item));
613
- else if (item != null) children.push({
614
- type: "text",
615
- text: String(item)
616
- });
617
- if (children.length > 0) element.elements = children;
618
- return element;
569
+ function stringifyElement(el) {
570
+ if (!el.name) return "";
571
+ let attrStr = "";
572
+ if (el.attributes) for (const key of Object.keys(el.attributes)) {
573
+ const v = el.attributes[key];
574
+ if (v === null || v === void 0) continue;
575
+ attrStr += ` ${key}="${escapeXml(String(v))}"`;
619
576
  }
620
- if (typeof value === "object" && value !== null) {
621
- const obj = value;
622
- if (obj._attr) element.attributes = obj._attr;
623
- if (obj._cdata) element.elements = [{
624
- type: "cdata",
625
- cdata: String(obj._cdata)
626
- }];
627
- }
628
- return element;
629
- }
630
- //#endregion
631
- //#region src/json.ts
632
- /** Convert XML string to JSON string — xml-js compatible export */
633
- function xml2json(xml, options) {
634
- return JSON.stringify(parse(xml, options));
577
+ if (!((el.elements?.length ?? 0) > 0 || el.attributes?.["xml:space"] === "preserve")) return `<${el.name}${attrStr}/>`;
578
+ return `<${el.name}${attrStr}>${stringify(el)}</${el.name}>`;
635
579
  }
636
580
  //#endregion
637
- export { allChildren, attr, attrBool, attrMeasure, attrNum, attrs, attrsRaw, childCount, childText, children, collectText, colorAttr, element, escapeXml, findChild, findDeep, findFirst, hasChild, isNonEmpty, stringify as js2xml, stringify, json2xml, nativeTypeValue, parse, parse as xml2js, parseAttributes, selfCloseElement, textOf, toElement, unescapeXml, xml, xml2json };
581
+ export { OOXML_XML_DECLARATION, allChildren, attr, attrBool, attrMeasure, attrNum, attrs, attrsRaw, childCount, childText, children, collectText, colorAttr, element, escapeXml, findChild, findDeep, findFirst, hasChild, isNonEmpty, nativeTypeValue, parse, parseAttributes, selfCloseElement, stringify, stringifyElement, textOf, unescapeXml };
638
582
 
639
583
  //# sourceMappingURL=index.mjs.map