@office-open/xml 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Demo Macro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @office-open/xml
2
+
3
+ ![npm version](https://img.shields.io/npm/v/@office-open/xml)
4
+ ![npm downloads](https://img.shields.io/npm/dw/@office-open/xml)
5
+ ![npm license](https://img.shields.io/npm/l/@office-open/xml)
6
+ ![zero dependencies](https://img.shields.io/badge/dependencies-0-green)
7
+
8
+ > XML parsing and serialization for Office Open XML. Zero dependencies, drop-in replacement for `xml` + `xml-js`.
9
+
10
+ ## Features
11
+
12
+ - **Zero Dependencies** - No external runtime dependencies, pure TypeScript implementation
13
+ - **xml() Serialization** - Drop-in replacement for the `xml` package
14
+ - **xml2js() Parsing** - Drop-in replacement for `xml-js` XML parsing
15
+ - **js2xml() Stringifying** - Drop-in replacement for `xml-js` JS-to-XML conversion
16
+ - **toElement() Direct Convert** - Direct conversion from xml object format to xml-js Element, 16-33x faster than the xml→xml2js bridge
17
+ - **Complete Type Definitions** - Full type compatibility with `xml` and `xml-js`, import without changes
18
+ - **OOXML Optimized** - Implements all options needed for Office Open XML document generation
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ # Install with npm
24
+ $ npm install @office-open/xml
25
+
26
+ # Install with pnpm
27
+ $ pnpm add @office-open/xml
28
+ ```
29
+
30
+ ## Migration from xml + xml-js
31
+
32
+ Replace your existing imports:
33
+
34
+ ```typescript
35
+ // Before
36
+ import xml from "xml";
37
+ import { xml2js, js2xml } from "xml-js";
38
+ import type { Element } from "xml-js";
39
+
40
+ // After
41
+ import { xml, xml2js, js2xml } from "@office-open/xml";
42
+ import type { Element } from "@office-open/xml";
43
+ ```
44
+
45
+ No other code changes needed. All options and output formats are compatible.
46
+
47
+ ## Quick Start
48
+
49
+ ```typescript
50
+ import { xml, xml2js, js2xml, toElement } from "@office-open/xml";
51
+
52
+ // Serialize JS objects to XML
53
+ const xmlStr = xml({ "w:p": [{ _attr: { "w:val": "1" } }, { "w:r": [{ "w:t": "Hello" }] }] });
54
+ // <w:p w:val="1"><w:r><w:t>Hello</w:t></w:r></w:p>
55
+
56
+ // Parse XML to JS objects
57
+ const parsed = xml2js("<w:t>Hello</w:t>", { compact: false });
58
+
59
+ // Convert JS objects back to XML
60
+ const output = js2xml(parsed);
61
+
62
+ // Direct conversion (faster than xml → xml2js bridge)
63
+ const element = toElement({
64
+ "w:p": [{ _attr: { "w:val": "1" } }, { "w:r": [{ "w:t": "Hello" }] }],
65
+ });
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### xml(input, options?)
71
+
72
+ Serialize JavaScript objects to XML string. Compatible with the `xml` package.
73
+
74
+ ### xml2js(xmlString, options?)
75
+
76
+ Parse XML string to JavaScript object. Compatible with `xml-js`.
77
+
78
+ ### js2xml(jsObject, options?)
79
+
80
+ Convert JavaScript object (xml-js Element format) to XML string. Compatible with `xml-js`.
81
+
82
+ ### json2xml(jsObject, options?)
83
+
84
+ Alias for `js2xml`.
85
+
86
+ ### xml2json(xmlString, options?)
87
+
88
+ Convenience function that returns `JSON.stringify(xml2js(xmlString, options))`.
89
+
90
+ ### toElement(input)
91
+
92
+ Direct conversion from xml object format to xml-js Element format. Much faster than the `xml() → xml2js()` bridge path.
93
+
94
+ ### escapeXml(str) / escapeAttributeValue(str)
95
+
96
+ Low-level XML entity escaping functions.
97
+
98
+ ## Benchmark
99
+
100
+ Performance comparison against original `xml` (1.0.1) and `xml-js` (1.6.11) packages:
101
+
102
+ ### Serialization (xml)
103
+
104
+ | Scenario | @office-open/xml | xml | Speedup |
105
+ | ----------------------- | ---------------: | ---------: | --------: |
106
+ | Simple element | 4,103,029 hz | 734,685 hz | **5.58x** |
107
+ | Nested element | 940,333 hz | 321,791 hz | **2.92x** |
108
+ | Nested with declaration | 800,517 hz | 262,125 hz | **3.05x** |
109
+
110
+ ### Parsing (xml2js)
111
+
112
+ | Scenario | @office-open/xml | xml-js | Speedup |
113
+ | ------------------ | ---------------: | --------: | ---------: |
114
+ | Simple XML | 1,002,659 hz | 83,057 hz | **12.07x** |
115
+ | Complex OOXML | 357,081 hz | 48,687 hz | **7.34x** |
116
+ | With captureSpaces | 361,555 hz | 44,868 hz | **8.06x** |
117
+
118
+ ### Stringifying (js2xml)
119
+
120
+ | Scenario | @office-open/xml | xml-js | Speedup |
121
+ | -------------- | ---------------: | ---------: | --------: |
122
+ | Simple element | 780,070 hz | 161,521 hz | **4.83x** |
123
+ | Complex OOXML | 276,878 hz | 106,527 hz | **2.60x** |
124
+
125
+ ### Direct Conversion (toElement vs bridge)
126
+
127
+ | Scenario | toElement() | xml() + xml2js() bridge | Speedup |
128
+ | -------- | ------------: | ----------------------: | ---------: |
129
+ | Simple | 14,119,071 hz | 853,290 hz | **16.55x** |
130
+ | Nested | 3,745,934 hz | 422,217 hz | **8.87x** |
131
+
132
+ ## Bundle Size
133
+
134
+ | | @office-open/xml | xml + xml-js |
135
+ | ---- | ---------------: | -----------: |
136
+ | gzip | **4.22 kB** | ~15 kB |
137
+
138
+ ## License
139
+
140
+ - [MIT](LICENSE) &copy; [Demo Macro](https://imst.xyz/)
@@ -0,0 +1,161 @@
1
+ //#region src/serialize.d.ts
2
+ declare function xml(input: Record<string, any> | Record<string, any>[], options?: boolean | string | {
3
+ indent?: boolean | string;
4
+ declaration?: boolean | {
5
+ encoding?: string;
6
+ standalone?: string;
7
+ };
8
+ }): string;
9
+ //#endregion
10
+ //#region src/types.d.ts
11
+ interface Attributes {
12
+ [key: string]: string | number | undefined;
13
+ }
14
+ interface DeclarationAttributes {
15
+ version?: string | number;
16
+ encoding?: string;
17
+ standalone?: string;
18
+ }
19
+ interface Element {
20
+ declaration?: {
21
+ attributes?: DeclarationAttributes;
22
+ };
23
+ instruction?: string;
24
+ attributes?: Attributes;
25
+ cdata?: string;
26
+ doctype?: string;
27
+ comment?: string;
28
+ text?: string | number | boolean;
29
+ type?: string;
30
+ name?: string;
31
+ elements?: Element[];
32
+ parent?: Element;
33
+ }
34
+ interface ElementCompact {
35
+ [key: string]: any;
36
+ _declaration?: {
37
+ _attributes?: DeclarationAttributes;
38
+ };
39
+ _instruction?: {
40
+ [key: string]: string;
41
+ };
42
+ _attributes?: Attributes;
43
+ _cdata?: string;
44
+ _doctype?: string;
45
+ _comment?: string;
46
+ _text?: string | number;
47
+ }
48
+ interface IgnoreOptions {
49
+ ignoreDeclaration?: boolean;
50
+ ignoreInstruction?: boolean;
51
+ ignoreAttributes?: boolean;
52
+ ignoreComment?: boolean;
53
+ ignoreCdata?: boolean;
54
+ ignoreDoctype?: boolean;
55
+ ignoreText?: boolean;
56
+ }
57
+ interface Xml2JsOptions extends IgnoreOptions {
58
+ compact?: boolean;
59
+ trim?: boolean;
60
+ sanitize?: boolean;
61
+ nativeType?: boolean;
62
+ nativeTypeAttributes?: boolean;
63
+ addParent?: boolean;
64
+ alwaysArray?: boolean | string[];
65
+ alwaysChildren?: boolean;
66
+ instructionHasAttributes?: boolean;
67
+ captureSpacesBetweenElements?: boolean;
68
+ doctypeFn?: (value: string, parentElement: object) => string;
69
+ instructionFn?: (value: string, instructionName: string, parentElement: string) => string;
70
+ cdataFn?: (value: string, parentElement: object) => string;
71
+ commentFn?: (value: string, parentElement: object) => string;
72
+ textFn?: (value: string, parentElement: object) => string;
73
+ instructionNameFn?: (instructionName: string, instructionValue: string, parentElement: string) => string;
74
+ elementNameFn?: (value: string, parentElement: object) => string;
75
+ attributeNameFn?: (attributeName: string, attributeValue: string, parentElement: string) => string;
76
+ attributeValueFn?: (attributeValue: string, attributeName: string, parentElement: string) => string;
77
+ attributesFn?: (value: Attributes, parentElement: string) => Attributes;
78
+ }
79
+ interface Js2XmlOptions extends IgnoreOptions {
80
+ spaces?: number | string;
81
+ compact?: boolean;
82
+ indentText?: boolean;
83
+ indentCdata?: boolean;
84
+ indentAttributes?: boolean;
85
+ indentInstruction?: boolean;
86
+ fullTagEmptyElement?: boolean;
87
+ noQuotesForNativeAttributes?: boolean;
88
+ doctypeFn?: (value: string, currentElementName: string, currentElementObj: object) => string;
89
+ instructionFn?: (instructionValue: string, instructionName: string, currentElementName: string, currentElementObj: object) => string;
90
+ cdataFn?: (value: string, currentElementName: string, currentElementObj: object) => string;
91
+ commentFn?: (value: string, currentElementName: string, currentElementObj: object) => string;
92
+ textFn?: (value: string, currentElementName: string, currentElementObj: object) => string;
93
+ instructionNameFn?: (instructionName: string, instructionValue: string, currentElementName: string, currentElementObj: object) => string;
94
+ elementNameFn?: (value: string, currentElementName: string, currentElementObj: object) => string;
95
+ attributeNameFn?: (attributeName: string, attributeValue: string, currentElementName: string, currentElementObj: object) => string;
96
+ attributeValueFn?: (attributeValue: string, attributeName: string, currentElementName: string, currentElementObj: object) => string;
97
+ attributesFn?: (value: Attributes, currentElementName: string, currentElementObj: object) => Attributes;
98
+ fullTagEmptyElementFn?: (currentElementName: string, currentElementObj: object) => boolean;
99
+ }
100
+ interface XmlOption {
101
+ indent?: string;
102
+ stream?: boolean;
103
+ declaration?: boolean | {
104
+ encoding?: string;
105
+ standalone?: string;
106
+ };
107
+ }
108
+ interface XmlAttrs {
109
+ [attr: string]: XmlAtom;
110
+ }
111
+ type XmlAtom = string | number | boolean | null;
112
+ interface ElementObject {
113
+ push(xmlObject: XmlObject): void;
114
+ close(xmlObject?: XmlObject): void;
115
+ }
116
+ type XmlDesc = {
117
+ _attr: XmlAttrs;
118
+ } | {
119
+ _cdata: string;
120
+ } | {
121
+ _attr: XmlAttrs;
122
+ _cdata: string;
123
+ } | XmlAtom | XmlAtom[] | XmlDescArray;
124
+ interface XmlDescArray {
125
+ [index: number]: {
126
+ _attr: XmlAttrs;
127
+ } | XmlObject;
128
+ }
129
+ type XmlObject = {
130
+ [tag: string]: ElementObject | XmlDesc;
131
+ } | XmlDesc;
132
+ //#endregion
133
+ //#region src/parse.d.ts
134
+ declare function xml2js(xmlString: string, options?: Xml2JsOptions): Element;
135
+ //#endregion
136
+ //#region src/stringify.d.ts
137
+ declare function js2xml(js: Element, options?: Js2XmlOptions): string;
138
+ /** Alias for js2xml — xml-js compatible export */
139
+ declare function json2xml(json: Element, options?: Js2XmlOptions): string;
140
+ //#endregion
141
+ //#region src/convert.d.ts
142
+ /**
143
+ * Convert XmlObject (node-xml format) directly to Element (xml-js format).
144
+ * Eliminates the redundant xml() → xml2js() bridge path.
145
+ */
146
+ declare function toElement(xmlObject: Record<string, any>): Element;
147
+ //#endregion
148
+ //#region src/escape.d.ts
149
+ /** Escape text content for XML */
150
+ declare function escapeXml(str: string): string;
151
+ /**
152
+ * Escape attribute value matching xml-js's js2xml behavior.
153
+ * Handles already-escaped entities to prevent double-escaping.
154
+ */
155
+ declare function escapeAttributeValue(str: string): string;
156
+ //#endregion
157
+ //#region src/json.d.ts
158
+ /** Convert XML string to JSON string — xml-js compatible export */
159
+ declare function xml2json(xml: string, options?: Xml2JsOptions): string;
160
+ //#endregion
161
+ export { Attributes, DeclarationAttributes, Element, ElementCompact, ElementObject, IgnoreOptions, Js2XmlOptions, Xml2JsOptions, XmlAtom, XmlAttrs, XmlDesc, XmlDescArray, XmlObject, XmlOption, escapeAttributeValue, escapeXml, js2xml, json2xml, toElement, xml, xml2js, xml2json };
package/dist/index.mjs ADDED
@@ -0,0 +1,502 @@
1
+ //#region src/escape.ts
2
+ const XML_CHAR_MAP = {
3
+ "&": "&amp;",
4
+ "\"": "&quot;",
5
+ "'": "&apos;",
6
+ "<": "&lt;",
7
+ ">": "&gt;"
8
+ };
9
+ const XML_CHAR_PATTERN = /([&"<>'])/g;
10
+ /** Escape text content for XML */
11
+ function escapeXml(str) {
12
+ return str.replace(XML_CHAR_PATTERN, (ch) => XML_CHAR_MAP[ch]);
13
+ }
14
+ /**
15
+ * Escape attribute value matching xml-js's js2xml behavior.
16
+ * Handles already-escaped entities to prevent double-escaping.
17
+ */
18
+ function escapeAttributeValue(str) {
19
+ return String(str).replace(/&(?!amp;|lt;|gt;|quot;|apos;)/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
20
+ }
21
+ //#endregion
22
+ //#region src/serialize.ts
23
+ const DEFAULT_INDENT = " ";
24
+ function xml(input, options) {
25
+ const opts = normalizeOptions$1(options);
26
+ const parts = [];
27
+ if (opts.declaration) {
28
+ const declOpts = opts.declaration === true ? {} : opts.declaration;
29
+ const enc = declOpts.encoding || "UTF-8";
30
+ const sa = declOpts.standalone;
31
+ let decl = "<?xml version=\"1.0\" encoding=\"" + enc + "\"";
32
+ if (sa) decl += " standalone=\"" + sa + "\"";
33
+ decl += "?>";
34
+ parts.push(decl);
35
+ if (opts.indent) parts.push("\n");
36
+ }
37
+ const items = Array.isArray(input) ? input : [input];
38
+ for (let i = 0; i < items.length; i++) {
39
+ parts.push(formatElement(resolve(items[i], opts.indent, 0)));
40
+ if (opts.indent && i < items.length - 1) parts.push("\n");
41
+ }
42
+ return parts.join("");
43
+ }
44
+ function normalizeOptions$1(options) {
45
+ const opts = typeof options === "object" && !Array.isArray(options) ? options : { indent: options };
46
+ let indent = "";
47
+ if (opts.indent) indent = opts.indent === true ? DEFAULT_INDENT : String(opts.indent);
48
+ return {
49
+ indent,
50
+ declaration: opts.declaration
51
+ };
52
+ }
53
+ function resolve(data, indent, depth) {
54
+ const name = Object.keys(data)[0];
55
+ const values = data[name];
56
+ const attributes = [];
57
+ const content = [];
58
+ if (values == null) return {
59
+ name,
60
+ attributes,
61
+ content,
62
+ indent,
63
+ depth,
64
+ emptyArray: false
65
+ };
66
+ switch (typeof values) {
67
+ case "object":
68
+ if (values._attr) for (const key of Object.keys(values._attr)) attributes.push(`${key}="${escapeXml(String(values._attr[key]))}"`);
69
+ if (values._cdata) {
70
+ const escaped = String(values._cdata).replace(/\]\]>/g, "]]]]><![CDATA[>");
71
+ content.push(`<![CDATA[${escaped}]]>`);
72
+ }
73
+ if (Array.isArray(values)) {
74
+ if (values.length === 0) return {
75
+ name,
76
+ attributes,
77
+ content,
78
+ indent,
79
+ depth,
80
+ emptyArray: true
81
+ };
82
+ 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]))}"`);
83
+ else if (value && typeof value === "object") content.push(resolve(value, indent, depth + 1));
84
+ else if (value != null) content.push(escapeXml(String(value)));
85
+ }
86
+ break;
87
+ default: content.push(escapeXml(String(values)));
88
+ }
89
+ return {
90
+ name,
91
+ attributes,
92
+ content,
93
+ indent,
94
+ depth,
95
+ emptyArray: false
96
+ };
97
+ }
98
+ function formatElement(elem) {
99
+ const { name, attributes, content, indent, depth } = elem;
100
+ const hasChildren = content.length > 0;
101
+ const ind = indent ? indent.repeat(depth) : "";
102
+ const attrStr = attributes.length ? " " + attributes.join(" ") : "";
103
+ if (!hasChildren) {
104
+ if (elem.emptyArray) return `${ind}<${name}${attrStr}></${name}>`;
105
+ return `${ind}<${name}${attrStr}/>`;
106
+ }
107
+ const textContent = content.length === 1 && typeof content[0] === "string" ? content[0] : null;
108
+ if (textContent !== null && !indent) return `<${name}${attrStr}>${textContent}</${name}>`;
109
+ if (textContent !== null) return `${ind}<${name}${attrStr}>${textContent}</${name}>`;
110
+ const parts = [];
111
+ parts.push(`${ind}<${name}${attrStr}>`);
112
+ if (indent) parts.push("\n");
113
+ for (const child of content) {
114
+ if (typeof child === "string") parts.push(`${indent.repeat(depth + 1)}${child}`);
115
+ else parts.push(formatElement(child));
116
+ if (indent) parts.push("\n");
117
+ }
118
+ parts.push(`${ind}</${name}>`);
119
+ return parts.join("");
120
+ }
121
+ //#endregion
122
+ //#region src/parse.ts
123
+ const ENTITY_MAP = {
124
+ "&amp;": "&",
125
+ "&lt;": "<",
126
+ "&gt;": ">",
127
+ "&quot;": "\"",
128
+ "&apos;": "'"
129
+ };
130
+ const ENTITY_PATTERN = /&(amp|lt|gt|quot|apos);/g;
131
+ function unescapeXml(str) {
132
+ return str.replace(ENTITY_PATTERN, (match) => ENTITY_MAP[match]);
133
+ }
134
+ function nativeTypeValue(value) {
135
+ const n = Number(value);
136
+ if (!isNaN(n)) return n;
137
+ const lower = value.toLowerCase();
138
+ if (lower === "true") return true;
139
+ if (lower === "false") return false;
140
+ return value;
141
+ }
142
+ function xml2js(xmlString, options) {
143
+ const captureSpaces = options?.captureSpacesBetweenElements ?? false;
144
+ const trim = options?.trim ?? false;
145
+ const ignoreDeclaration = options?.ignoreDeclaration ?? false;
146
+ const ignoreText = options?.ignoreText ?? false;
147
+ const ignoreComment = options?.ignoreComment ?? false;
148
+ const ignoreCdata = options?.ignoreCdata ?? false;
149
+ const ignoreDoctype = options?.ignoreDoctype ?? false;
150
+ const nativeTypeAttributes = options?.nativeTypeAttributes ?? false;
151
+ const result = {};
152
+ const stack = [result];
153
+ let i = 0;
154
+ const len = xmlString.length;
155
+ while (i < len) {
156
+ if (!captureSpaces && isWhitespace(xmlString.charCodeAt(i))) {
157
+ i++;
158
+ continue;
159
+ }
160
+ if (xmlString.charCodeAt(i) !== 60) {
161
+ const start = i;
162
+ while (i < len && xmlString.charCodeAt(i) !== 60) i++;
163
+ let text = unescapeXml(xmlString.slice(start, i));
164
+ if (trim) text = text.trim();
165
+ if (ignoreText) continue;
166
+ if (text.length > 0) {
167
+ if (captureSpaces || text.trim().length > 0) addField(stack[stack.length - 1], "text", text);
168
+ }
169
+ continue;
170
+ }
171
+ i++;
172
+ if (xmlString.charCodeAt(i) === 63) {
173
+ const end = xmlString.indexOf("?>", i + 1);
174
+ if (end === -1) break;
175
+ const body = xmlString.slice(i + 1, end);
176
+ i = end + 2;
177
+ const xmlMatch = body.match(/^xml\s+(.*)$/s);
178
+ if (xmlMatch) {
179
+ if (!ignoreDeclaration) {
180
+ if (!result.declaration) result.declaration = {};
181
+ const attrs = parseAttributes(xmlMatch[1]);
182
+ if (nativeTypeAttributes) for (const key of Object.keys(attrs)) attrs[key] = nativeTypeValue(attrs[key]);
183
+ result.declaration.attributes = attrs;
184
+ }
185
+ }
186
+ continue;
187
+ }
188
+ if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 3) === "!--") {
189
+ const end = xmlString.indexOf("-->", i + 3);
190
+ if (end === -1) break;
191
+ const comment = xmlString.slice(i + 3, end);
192
+ i = end + 3;
193
+ if (!ignoreComment) if (trim) addField(stack[stack.length - 1], "comment", comment.trim());
194
+ else addField(stack[stack.length - 1], "comment", comment);
195
+ continue;
196
+ }
197
+ if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 8) === "![CDATA[") {
198
+ const end = xmlString.indexOf("]]>", i + 8);
199
+ if (end === -1) break;
200
+ const cdata = xmlString.slice(i + 8, end);
201
+ i = end + 3;
202
+ if (!ignoreCdata) if (trim) addField(stack[stack.length - 1], "cdata", cdata.trim());
203
+ else addField(stack[stack.length - 1], "cdata", cdata);
204
+ continue;
205
+ }
206
+ if (xmlString.charCodeAt(i) === 33 && xmlString.slice(i, i + 9) === "!DOCTYPE") {
207
+ const end = xmlString.indexOf(">", i + 9);
208
+ if (end === -1) break;
209
+ const doctype = xmlString.slice(i + 9, end).trim();
210
+ i = end + 1;
211
+ if (!ignoreDoctype) addField(stack[stack.length - 1], "doctype", doctype);
212
+ continue;
213
+ }
214
+ if (xmlString.charCodeAt(i) === 47) {
215
+ const end = xmlString.indexOf(">", i + 1);
216
+ if (end === -1) break;
217
+ i = end + 1;
218
+ stack.pop();
219
+ continue;
220
+ }
221
+ const tagNameEnd = findTagNameEnd(xmlString, i);
222
+ const tagName = xmlString.slice(i, tagNameEnd);
223
+ let pos = tagNameEnd;
224
+ const attributes = parseAttributesFromXml(xmlString, pos);
225
+ pos = attributes.pos;
226
+ if (nativeTypeAttributes) for (const key of Object.keys(attributes.attrs)) attributes.attrs[key] = nativeTypeValue(attributes.attrs[key]);
227
+ const isSelfClosing = xmlString.charCodeAt(pos) === 47;
228
+ if (isSelfClosing) pos += 2;
229
+ else pos++;
230
+ const element = {
231
+ type: "element",
232
+ name: tagName
233
+ };
234
+ if (Object.keys(attributes.attrs).length > 0) element.attributes = attributes.attrs;
235
+ const parent = stack[stack.length - 1];
236
+ if (!parent.elements) parent.elements = [];
237
+ parent.elements.push(element);
238
+ if (!isSelfClosing) stack.push(element);
239
+ i = pos;
240
+ }
241
+ if (result.elements) {
242
+ const temp = result.elements;
243
+ delete result.elements;
244
+ result.elements = temp;
245
+ delete result.text;
246
+ }
247
+ return result;
248
+ }
249
+ function findTagNameEnd(str, start) {
250
+ let i = start;
251
+ const len = str.length;
252
+ while (i < len) {
253
+ const ch = str.charCodeAt(i);
254
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13 || ch === 47 || ch === 62) return i;
255
+ i++;
256
+ }
257
+ return i;
258
+ }
259
+ function parseAttributesFromXml(str, start) {
260
+ const attrs = {};
261
+ let i = start;
262
+ const len = str.length;
263
+ while (i < len) {
264
+ while (i < len && isWhitespace(str.charCodeAt(i))) i++;
265
+ if (i >= len || str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
266
+ const nameStart = i;
267
+ while (i < len && str.charCodeAt(i) !== 61) {
268
+ if (str.charCodeAt(i) === 62 || str.charCodeAt(i) === 47) break;
269
+ i++;
270
+ }
271
+ const name = str.slice(nameStart, i);
272
+ if (str.charCodeAt(i) !== 61) break;
273
+ i++;
274
+ while (i < len && isWhitespace(str.charCodeAt(i))) i++;
275
+ const quote = str.charCodeAt(i);
276
+ if (quote !== 34 && quote !== 39) break;
277
+ i++;
278
+ const valueStart = i;
279
+ while (i < len && str.charCodeAt(i) !== quote) i++;
280
+ attrs[name] = str.slice(valueStart, i);
281
+ i++;
282
+ }
283
+ return {
284
+ attrs,
285
+ pos: i
286
+ };
287
+ }
288
+ function parseAttributes(str) {
289
+ const result = {};
290
+ let i = 0;
291
+ const len = str.length;
292
+ while (i < len) {
293
+ while (i < len && isWhitespace(str.charCodeAt(i))) i++;
294
+ if (i >= len) break;
295
+ const nameStart = i;
296
+ while (i < len && str.charCodeAt(i) !== 61) {
297
+ if (isWhitespace(str.charCodeAt(i))) break;
298
+ i++;
299
+ }
300
+ const name = str.slice(nameStart, i);
301
+ while (i < len && isWhitespace(str.charCodeAt(i))) i++;
302
+ if (i >= len || str.charCodeAt(i) !== 61) break;
303
+ i++;
304
+ while (i < len && isWhitespace(str.charCodeAt(i))) i++;
305
+ const quote = str.charCodeAt(i);
306
+ if (quote !== 34 && quote !== 39) break;
307
+ i++;
308
+ const valueStart = i;
309
+ while (i < len && str.charCodeAt(i) !== quote) i++;
310
+ result[name] = str.slice(valueStart, i);
311
+ i++;
312
+ }
313
+ return result;
314
+ }
315
+ function addField(parent, type, value) {
316
+ if (!parent.elements) parent.elements = [];
317
+ const element = { type };
318
+ element[type] = value;
319
+ parent.elements.push(element);
320
+ }
321
+ function isWhitespace(ch) {
322
+ return ch === 32 || ch === 9 || ch === 10 || ch === 13;
323
+ }
324
+ //#endregion
325
+ //#region src/stringify.ts
326
+ function js2xml(js, options) {
327
+ const opts = normalizeOptions(options);
328
+ const parts = [];
329
+ if (js.declaration && !opts.ignoreDeclaration) parts.push(writeDeclaration(js.declaration));
330
+ if (js.elements?.length) parts.push(writeElements(js.elements, opts, 0, !parts.length));
331
+ return parts.join("");
332
+ }
333
+ /** Alias for js2xml — xml-js compatible export */
334
+ function json2xml(json, options) {
335
+ return js2xml(json, options);
336
+ }
337
+ function normalizeOptions(options) {
338
+ if (!options) return {
339
+ spaces: "",
340
+ ignoreDeclaration: false,
341
+ ignoreText: false,
342
+ ignoreComment: false,
343
+ ignoreCdata: false,
344
+ ignoreDoctype: false,
345
+ fullTagEmptyElement: false,
346
+ indentText: false,
347
+ indentCdata: false
348
+ };
349
+ let spaces = "";
350
+ if (options.spaces != null) spaces = typeof options.spaces === "number" ? " ".repeat(options.spaces) : options.spaces;
351
+ return {
352
+ spaces,
353
+ ignoreDeclaration: options.ignoreDeclaration ?? false,
354
+ ignoreText: options.ignoreText ?? false,
355
+ ignoreComment: options.ignoreComment ?? false,
356
+ ignoreCdata: options.ignoreCdata ?? false,
357
+ ignoreDoctype: options.ignoreDoctype ?? false,
358
+ fullTagEmptyElement: options.fullTagEmptyElement ?? false,
359
+ indentText: options.indentText ?? false,
360
+ indentCdata: options.indentCdata ?? false,
361
+ attributeValueFn: options.attributeValueFn
362
+ };
363
+ }
364
+ function writeIndentation(spaces, depth, firstLine) {
365
+ return (!firstLine && spaces ? "\n" : "") + spaces.repeat(depth);
366
+ }
367
+ function writeDeclaration(declaration) {
368
+ const attrs = declaration.attributes;
369
+ if (!attrs) return "<?xml version=\"1.0\"?>";
370
+ let result = "<?xml version=\"1.0\"";
371
+ if (attrs.encoding) result += ` encoding="${attrs.encoding}"`;
372
+ if (attrs.standalone) result += ` standalone="${attrs.standalone}"`;
373
+ return result + "?>";
374
+ }
375
+ function writeAttributes(attributes, elementName, element, attributeValueFn) {
376
+ const parts = [];
377
+ for (const key of Object.keys(attributes)) {
378
+ const value = attributes[key];
379
+ if (value === null || value === void 0) continue;
380
+ let attr = String(value).replace(/"/g, "&quot;");
381
+ if (attributeValueFn) attr = attributeValueFn(attr, key, elementName, element);
382
+ parts.push(` ${key}="${attr}"`);
383
+ }
384
+ return parts.join("");
385
+ }
386
+ function writeElement(element, opts, depth) {
387
+ if (!element.name) return "";
388
+ const name = element.name;
389
+ const attrStr = element.attributes ? writeAttributes(element.attributes, name, element, opts.attributeValueFn) : "";
390
+ if (!((element.elements?.length ?? 0) > 0 || element.attributes?.["xml:space"] === "preserve" || opts.fullTagEmptyElement)) return `<${name}${attrStr}/>`;
391
+ const parts = [];
392
+ parts.push(`<${name}${attrStr}>`);
393
+ const hasChildElements = element.elements?.some((e) => e.type === "element") ?? false;
394
+ if (element.elements?.length) parts.push(writeElements(element.elements, opts, depth + 1, false));
395
+ if (opts.spaces && hasChildElements) parts.push("\n" + opts.spaces.repeat(depth));
396
+ parts.push(`</${name}>`);
397
+ return parts.join("");
398
+ }
399
+ function writeElements(elements, opts, depth, firstLine) {
400
+ let result = "";
401
+ for (let i = 0; i < elements.length; i++) {
402
+ const element = elements[i];
403
+ const isFirst = firstLine && i === 0;
404
+ switch (element.type) {
405
+ case "element":
406
+ result += writeIndentation(opts.spaces, depth, isFirst);
407
+ result += writeElement(element, opts, depth);
408
+ break;
409
+ case "text":
410
+ if (opts.ignoreText) continue;
411
+ if (opts.indentText) result += writeIndentation(opts.spaces, depth, isFirst);
412
+ result += writeText(element.text);
413
+ break;
414
+ case "cdata":
415
+ if (opts.ignoreCdata) continue;
416
+ if (opts.indentCdata) result += writeIndentation(opts.spaces, depth, isFirst);
417
+ result += writeCdata(element.cdata);
418
+ break;
419
+ case "comment":
420
+ if (opts.ignoreComment) continue;
421
+ result += writeIndentation(opts.spaces, depth, isFirst);
422
+ result += writeComment(element.comment);
423
+ break;
424
+ case "doctype":
425
+ if (opts.ignoreDoctype) continue;
426
+ result += writeIndentation(opts.spaces, depth, isFirst);
427
+ result += writeDoctype(element.doctype);
428
+ break;
429
+ default: break;
430
+ }
431
+ }
432
+ return result;
433
+ }
434
+ function writeText(text) {
435
+ if (text == null) return "";
436
+ return String(text).replace(/&amp;/g, "&").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
437
+ }
438
+ function writeCdata(cdata) {
439
+ if (cdata == null) return "";
440
+ return `<![CDATA[${cdata.replace(/\]\]>/g, "]]]]><![CDATA[>")}]]>`;
441
+ }
442
+ function writeComment(comment) {
443
+ if (comment == null) return "";
444
+ return `<!--${comment}-->`;
445
+ }
446
+ function writeDoctype(doctype) {
447
+ if (doctype == null) return "";
448
+ return `<!DOCTYPE ${doctype}>`;
449
+ }
450
+ //#endregion
451
+ //#region src/convert.ts
452
+ /**
453
+ * Convert XmlObject (node-xml format) directly to Element (xml-js format).
454
+ * Eliminates the redundant xml() → xml2js() bridge path.
455
+ */
456
+ function toElement(xmlObject) {
457
+ const tagName = Object.keys(xmlObject)[0];
458
+ const value = xmlObject[tagName];
459
+ const element = {
460
+ type: "element",
461
+ name: tagName
462
+ };
463
+ if (value == null) return element;
464
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
465
+ element.elements = [{
466
+ type: "text",
467
+ text: String(value)
468
+ }];
469
+ return element;
470
+ }
471
+ if (Array.isArray(value)) {
472
+ const children = [];
473
+ for (const item of value) if (item && typeof item === "object" && "_attr" in item) element.attributes = item._attr;
474
+ else if (item && typeof item === "object") if (Object.keys(item)[0] === "_cdata") children.push({
475
+ type: "cdata",
476
+ cdata: String(item._cdata)
477
+ });
478
+ else children.push(toElement(item));
479
+ else if (item != null) children.push({
480
+ type: "text",
481
+ text: String(item)
482
+ });
483
+ if (children.length > 0) element.elements = children;
484
+ return element;
485
+ }
486
+ if (typeof value === "object") {
487
+ if (value._attr) element.attributes = value._attr;
488
+ if (value._cdata) element.elements = [{
489
+ type: "cdata",
490
+ cdata: String(value._cdata)
491
+ }];
492
+ }
493
+ return element;
494
+ }
495
+ //#endregion
496
+ //#region src/json.ts
497
+ /** Convert XML string to JSON string — xml-js compatible export */
498
+ function xml2json(xml, options) {
499
+ return JSON.stringify(xml2js(xml, options));
500
+ }
501
+ //#endregion
502
+ export { escapeAttributeValue, escapeXml, js2xml, json2xml, toElement, xml, xml2js, xml2json };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@office-open/xml",
3
+ "version": "0.1.0",
4
+ "description": "XML parsing and serialization, replacing xml + xml-js",
5
+ "keywords": [
6
+ "office-open",
7
+ "ooxml",
8
+ "parse",
9
+ "serialize",
10
+ "xml"
11
+ ],
12
+ "homepage": "https://github.com/DemoMacro/office-open#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/DemoMacro/office-open/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Demo Macro",
19
+ "email": "abc@imst.xyz",
20
+ "url": "https://imst.xyz/"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/DemoMacro/office-open.git"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "main": "dist/index.mjs",
31
+ "types": "dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs"
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@types/xml": "1.0.11",
40
+ "xml": "1.0.1",
41
+ "xml-js": "1.6.11"
42
+ },
43
+ "scripts": {
44
+ "dev": "basis build --stub",
45
+ "build": "basis build"
46
+ }
47
+ }