@office-open/xml 0.3.1 → 0.3.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.d.mts CHANGED
@@ -32,9 +32,19 @@ declare function escapeXml(str: string): string;
32
32
  * Handles already-escaped entities to prevent double-escaping.
33
33
  */
34
34
  declare function escapeAttributeValue(str: string): string;
35
+ /**
36
+ * Build an XML attribute string fragment from a record.
37
+ * `undefined` values are automatically skipped.
38
+ * String values are escaped via `escapeXml`.
39
+ *
40
+ * @example
41
+ * attrs({ id: 1, name: "foo", hidden: undefined })
42
+ * // => ' id="1" name="foo"'
43
+ */
44
+ declare function attrs(record: Record<string, string | number | boolean | undefined>): string;
35
45
  //#endregion
36
46
  //#region src/json.d.ts
37
47
  /** Convert XML string to JSON string — xml-js compatible export */
38
48
  declare function xml2json(xml: string, options?: Xml2JsOptions): string;
39
49
  //#endregion
40
- export { Attributes, DeclarationAttributes, Element, ElementCompact, ElementObject, IgnoreOptions, Js2XmlOptions, Xml2JsOptions, XmlAtom, XmlAttrs, XmlDesc, XmlDescArray, XmlObject, XmlOption, allChildren, attr, attrBool, attrNum, childCount, childText, children, collectText, colorAttr, escapeAttributeValue, escapeXml, findChild, findDeep, hasChild, js2xml, json2xml, textOf, toElement, xml, xml2js, xml2json };
50
+ export { Attributes, DeclarationAttributes, Element, ElementCompact, ElementObject, IgnoreOptions, Js2XmlOptions, Xml2JsOptions, XmlAtom, XmlAttrs, XmlDesc, XmlDescArray, XmlObject, XmlOption, allChildren, attr, attrBool, attrNum, attrs, childCount, childText, children, collectText, colorAttr, escapeAttributeValue, escapeXml, findChild, findDeep, hasChild, js2xml, json2xml, textOf, toElement, xml, xml2js, xml2json };
package/dist/index.mjs CHANGED
@@ -19,6 +19,20 @@ function escapeXml(str) {
19
19
  function escapeAttributeValue(str) {
20
20
  return String(str).replace(/&(?!amp;|lt;|gt;|quot;|apos;)/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
21
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
+ }
22
36
  //#endregion
23
37
  //#region src/serialize.ts
24
38
  const DEFAULT_INDENT = " ";
@@ -37,7 +51,8 @@ function xml(input, options) {
37
51
  }
38
52
  const items = Array.isArray(input) ? input : [input];
39
53
  for (let i = 0; i < items.length; i++) {
40
- parts.push(formatElement(resolve(items[i], opts.indent, 0)));
54
+ const keys = Object.keys(items[i]);
55
+ parts.push(formatElement(keys[0], items[i][keys[0]], opts.indent, 0));
41
56
  if (opts.indent && i < items.length - 1) parts.push("\n");
42
57
  }
43
58
  return parts.join("");
@@ -51,71 +66,50 @@ function normalizeOptions$1(options) {
51
66
  declaration: opts.declaration
52
67
  };
53
68
  }
54
- function resolve(data, indent, depth) {
55
- const name = Object.keys(data)[0];
56
- const values = data[name];
57
- const attributes = [];
58
- const content = [];
59
- if (values == null) return {
60
- name,
61
- attributes,
62
- content,
63
- indent,
64
- depth,
65
- emptyArray: false
66
- };
67
- switch (typeof values) {
68
- case "object":
69
- if (values._attr) for (const key of Object.keys(values._attr)) attributes.push(`${key}="${escapeXml(String(values._attr[key]))}"`);
70
- if (values._attributes) for (const key of Object.keys(values._attributes)) attributes.push(`${key}="${escapeXml(String(values._attributes[key]))}"`);
71
- if (values._cdata) {
72
- const escaped = String(values._cdata).replace(/\]\]>/g, "]]]]><![CDATA[>");
73
- content.push(`<![CDATA[${escaped}]]>`);
74
- }
75
- if (Array.isArray(values)) {
76
- if (values.length === 0) return {
77
- name,
78
- attributes,
79
- content,
80
- indent,
81
- depth,
82
- emptyArray: true
83
- };
84
- for (const value of values) if (value && typeof value === "object" && "_attr" in value) for (const key of Object.keys(value._attr)) attributes.push(`${key}="${escapeXml(String(value._attr[key]))}"`);
85
- else if (value && typeof value === "object" && "_attributes" in value) for (const key of Object.keys(value._attributes)) attributes.push(`${key}="${escapeXml(String(value._attributes[key]))}"`);
86
- else if (value && typeof value === "object") content.push(resolve(value, indent, depth + 1));
87
- else if (value != null) content.push(escapeXml(String(value)));
88
- }
89
- break;
90
- default: content.push(escapeXml(String(values)));
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}/>`;
91
81
  }
92
- return {
93
- name,
94
- attributes,
95
- content,
96
- indent,
97
- depth,
98
- emptyArray: false
99
- };
100
- }
101
- function formatElement(elem) {
102
- const { name, attributes, content, indent, depth } = elem;
103
- const hasChildren = content.length > 0;
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)));
104
99
  const ind = indent ? indent.repeat(depth) : "";
105
- const attrStr = attributes.length ? " " + attributes.join(" ") : "";
106
- if (!hasChildren) {
107
- if (elem.emptyArray) return `${ind}<${name}${attrStr}></${name}>`;
108
- return `${ind}<${name}${attrStr}/>`;
109
- }
110
- const textContent = content.length === 1 && typeof content[0] === "string" ? content[0] : null;
111
- if (textContent !== null && !indent) return `<${name}${attrStr}>${textContent}</${name}>`;
112
- if (textContent !== null) return `${ind}<${name}${attrStr}>${textContent}</${name}>`;
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}>`;
113
103
  const parts = [];
114
104
  parts.push(`${ind}<${name}${attrStr}>`);
115
105
  if (indent) parts.push("\n");
116
- for (const child of content) {
117
- if (typeof child === "string") parts.push(`${indent.repeat(depth + 1)}${child}`);
118
- else parts.push(formatElement(child));
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);
119
113
  if (indent) parts.push("\n");
120
114
  }
121
115
  parts.push(`${ind}</${name}>`);
@@ -502,4 +496,4 @@ function xml2json(xml, options) {
502
496
  return JSON.stringify(xml2js(xml, options));
503
497
  }
504
498
  //#endregion
505
- export { allChildren, attr, attrBool, attrNum, childCount, childText, children, collectText, colorAttr, escapeAttributeValue, escapeXml, findChild, findDeep, hasChild, js2xml, json2xml, textOf, toElement, xml, xml2js, xml2json };
499
+ export { allChildren, attr, attrBool, attrNum, attrs, childCount, childText, children, collectText, colorAttr, escapeAttributeValue, escapeXml, findChild, findDeep, hasChild, js2xml, json2xml, textOf, toElement, xml, xml2js, xml2json };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-open/xml",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "XML parsing and serialization for Office Open XML. Zero dependencies, drop-in replacement for xml + xml-js.",
5
5
  "keywords": [
6
6
  "office-open",