@wonderwhy-er/desktop-commander 0.2.35 → 0.2.36

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.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/dist/handlers/filesystem-handlers.js +58 -11
  3. package/dist/handlers/history-handlers.d.ts +7 -0
  4. package/dist/handlers/history-handlers.js +33 -1
  5. package/dist/server.js +30 -4
  6. package/dist/tools/docx/builders/image.d.ts +14 -0
  7. package/dist/tools/docx/builders/image.js +84 -0
  8. package/dist/tools/docx/builders/index.d.ts +9 -3
  9. package/dist/tools/docx/builders/index.js +9 -3
  10. package/dist/tools/docx/builders/paragraph.d.ts +12 -0
  11. package/dist/tools/docx/builders/paragraph.js +29 -0
  12. package/dist/tools/docx/builders/table.d.ts +8 -0
  13. package/dist/tools/docx/builders/table.js +94 -0
  14. package/dist/tools/docx/builders/utils.d.ts +5 -0
  15. package/dist/tools/docx/builders/utils.js +18 -0
  16. package/dist/tools/docx/constants.d.ts +28 -32
  17. package/dist/tools/docx/constants.js +56 -52
  18. package/dist/tools/docx/create.d.ts +21 -0
  19. package/dist/tools/docx/create.js +386 -0
  20. package/dist/tools/docx/dom.d.ts +66 -0
  21. package/dist/tools/docx/dom.js +228 -0
  22. package/dist/tools/docx/index.d.ts +8 -12
  23. package/dist/tools/docx/index.js +8 -14
  24. package/dist/tools/docx/modify.d.ts +28 -0
  25. package/dist/tools/docx/modify.js +271 -0
  26. package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +11 -0
  27. package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +23 -0
  28. package/dist/tools/docx/ops/header-replace-text-exact.d.ts +13 -0
  29. package/dist/tools/docx/ops/header-replace-text-exact.js +55 -0
  30. package/dist/tools/docx/ops/index.d.ts +17 -0
  31. package/dist/tools/docx/ops/index.js +67 -0
  32. package/dist/tools/docx/ops/insert-image-after-text.d.ts +24 -0
  33. package/dist/tools/docx/ops/insert-image-after-text.js +128 -0
  34. package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +12 -0
  35. package/dist/tools/docx/ops/insert-paragraph-after-text.js +74 -0
  36. package/dist/tools/docx/ops/insert-table-after-text.d.ts +19 -0
  37. package/dist/tools/docx/ops/insert-table-after-text.js +57 -0
  38. package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +12 -0
  39. package/dist/tools/docx/ops/replace-hyperlink-url.js +37 -0
  40. package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +9 -0
  41. package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +25 -0
  42. package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +9 -0
  43. package/dist/tools/docx/ops/replace-paragraph-text-exact.js +21 -0
  44. package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +8 -0
  45. package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +23 -0
  46. package/dist/tools/docx/ops/set-color-for-style.d.ts +9 -0
  47. package/dist/tools/docx/ops/set-color-for-style.js +27 -0
  48. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +8 -0
  49. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +57 -0
  50. package/dist/tools/docx/ops/table-set-cell-text.d.ts +9 -0
  51. package/dist/tools/docx/ops/table-set-cell-text.js +72 -0
  52. package/dist/tools/docx/read.d.ts +27 -0
  53. package/dist/tools/docx/read.js +188 -0
  54. package/dist/tools/docx/relationships.d.ts +22 -0
  55. package/dist/tools/docx/relationships.js +76 -0
  56. package/dist/tools/docx/types.d.ts +174 -104
  57. package/dist/tools/docx/types.js +2 -5
  58. package/dist/tools/docx/validate.d.ts +33 -0
  59. package/dist/tools/docx/validate.js +49 -0
  60. package/dist/tools/docx/write.d.ts +17 -0
  61. package/dist/tools/docx/write.js +88 -0
  62. package/dist/tools/docx/zip.d.ts +21 -0
  63. package/dist/tools/docx/zip.js +35 -0
  64. package/dist/tools/schemas.d.ts +13 -0
  65. package/dist/tools/schemas.js +5 -0
  66. package/dist/types.d.ts +10 -0
  67. package/dist/ui/contracts.d.ts +14 -0
  68. package/dist/ui/contracts.js +18 -0
  69. package/dist/ui/file-preview/index.html +16 -0
  70. package/dist/ui/file-preview/preview-runtime.js +13977 -0
  71. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  72. package/dist/ui/file-preview/shared/preview-file-types.js +57 -0
  73. package/dist/ui/file-preview/src/app.d.ts +4 -0
  74. package/dist/ui/file-preview/src/app.js +800 -0
  75. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  76. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  77. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  78. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  79. package/dist/ui/file-preview/src/components/html-renderer.d.ts +9 -0
  80. package/dist/ui/file-preview/src/components/html-renderer.js +63 -0
  81. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  82. package/dist/ui/file-preview/src/components/markdown-renderer.js +21 -0
  83. package/dist/ui/file-preview/src/components/toolbar.d.ts +6 -0
  84. package/dist/ui/file-preview/src/components/toolbar.js +75 -0
  85. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  86. package/dist/ui/file-preview/src/image-preview.js +21 -0
  87. package/dist/ui/file-preview/src/main.d.ts +1 -0
  88. package/dist/ui/file-preview/src/main.js +5 -0
  89. package/dist/ui/file-preview/src/types.d.ts +1 -0
  90. package/dist/ui/file-preview/src/types.js +1 -0
  91. package/dist/ui/file-preview/styles.css +764 -0
  92. package/dist/ui/resources.d.ts +21 -0
  93. package/dist/ui/resources.js +72 -0
  94. package/dist/ui/shared/escape-html.d.ts +4 -0
  95. package/dist/ui/shared/escape-html.js +11 -0
  96. package/dist/ui/shared/host-lifecycle.d.ts +16 -0
  97. package/dist/ui/shared/host-lifecycle.js +35 -0
  98. package/dist/ui/shared/rpc-client.d.ts +14 -0
  99. package/dist/ui/shared/rpc-client.js +72 -0
  100. package/dist/ui/shared/theme-adaptation.d.ts +10 -0
  101. package/dist/ui/shared/theme-adaptation.js +118 -0
  102. package/dist/ui/shared/tool-header.d.ts +9 -0
  103. package/dist/ui/shared/tool-header.js +25 -0
  104. package/dist/ui/shared/tool-shell.d.ts +16 -0
  105. package/dist/ui/shared/tool-shell.js +65 -0
  106. package/dist/ui/shared/widget-state.d.ts +28 -0
  107. package/dist/ui/shared/widget-state.js +60 -0
  108. package/dist/utils/capture.d.ts +1 -0
  109. package/dist/utils/capture.js +6 -0
  110. package/dist/utils/files/docx.d.ts +8 -15
  111. package/dist/utils/files/docx.js +76 -176
  112. package/dist/utils/files/text.js +9 -1
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.js +1 -1
  115. package/package.json +5 -2
@@ -0,0 +1,228 @@
1
+ /**
2
+ * DOM utilities for DOCX XML manipulation.
3
+ *
4
+ * Single Responsibility: XML parsing, navigation, and minimal element
5
+ * mutation. No file I/O — every function works on in-memory DOM nodes.
6
+ *
7
+ * Uses @xmldom/xmldom for parsing and serialisation so that the
8
+ * document-order of nodes is always preserved.
9
+ */
10
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
11
+ // ═══════════════════════════════════════════════════════════════════════
12
+ // XML parse / serialize
13
+ // ═══════════════════════════════════════════════════════════════════════
14
+ export function parseXml(xmlStr) {
15
+ return new DOMParser().parseFromString(xmlStr, 'application/xml');
16
+ }
17
+ export function serializeXml(doc) {
18
+ return new XMLSerializer().serializeToString(doc);
19
+ }
20
+ // ═══════════════════════════════════════════════════════════════════════
21
+ // Generic DOM helpers
22
+ // ═══════════════════════════════════════════════════════════════════════
23
+ /**
24
+ * Convert any NodeList / HTMLCollection-like object into a real array.
25
+ */
26
+ export function nodeListToArray(nl) {
27
+ const arr = [];
28
+ for (let i = 0; i < nl.length; i++) {
29
+ const n = nl.item(i);
30
+ if (n)
31
+ arr.push(n);
32
+ }
33
+ return arr;
34
+ }
35
+ // ═══════════════════════════════════════════════════════════════════════
36
+ // Body access
37
+ // ═══════════════════════════════════════════════════════════════════════
38
+ /** Return the single <w:body> element from a parsed document.xml DOM. */
39
+ export function getBody(doc) {
40
+ const body = doc.getElementsByTagName('w:body').item(0);
41
+ if (!body)
42
+ throw new Error('Invalid DOCX DOM: missing <w:body>');
43
+ return body;
44
+ }
45
+ /**
46
+ * Return ALL direct element children of w:body **in document order**.
47
+ * Includes w:p, w:tbl, w:sdt, w:sectPr, etc.
48
+ */
49
+ export function getBodyChildren(body) {
50
+ const out = [];
51
+ for (const node of nodeListToArray(body.childNodes)) {
52
+ if (node.nodeType === 1)
53
+ out.push(node);
54
+ }
55
+ return out;
56
+ }
57
+ // ═══════════════════════════════════════════════════════════════════════
58
+ // Body signature
59
+ // ═══════════════════════════════════════════════════════════════════════
60
+ /**
61
+ * Build a compact signature string from the body children array.
62
+ * Maps each node's qualified name to a short local name:
63
+ * w:p → p, w:tbl → tbl, w:sdt → sdt, w:sectPr → sectPr, …
64
+ * Returns e.g. "p,tbl,p,p,sectPr".
65
+ */
66
+ export function bodySignature(children) {
67
+ return children
68
+ .map((ch) => {
69
+ const name = ch.nodeName;
70
+ const idx = name.indexOf(':');
71
+ return idx >= 0 ? name.substring(idx + 1) : name;
72
+ })
73
+ .join(',');
74
+ }
75
+ // ═══════════════════════════════════════════════════════════════════════
76
+ // Paragraph text helpers
77
+ // ═══════════════════════════════════════════════════════════════════════
78
+ /** Concatenate text from every <w:t> descendant of a paragraph. */
79
+ export function getParagraphText(p) {
80
+ const tNodes = p.getElementsByTagName('w:t');
81
+ let out = '';
82
+ for (let i = 0; i < tNodes.length; i++) {
83
+ out += tNodes.item(i)?.textContent ?? '';
84
+ }
85
+ return out;
86
+ }
87
+ /** Read the style id from w:pPr/w:pStyle/@w:val, or null if absent. */
88
+ export function getParagraphStyle(p) {
89
+ for (const child of nodeListToArray(p.childNodes)) {
90
+ if (child.nodeType === 1 && child.nodeName === 'w:pPr') {
91
+ const pPr = child;
92
+ for (const prChild of nodeListToArray(pPr.childNodes)) {
93
+ if (prChild.nodeType === 1 && prChild.nodeName === 'w:pStyle') {
94
+ return prChild.getAttribute('w:val');
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ // ═══════════════════════════════════════════════════════════════════════
103
+ // Minimal text replacement
104
+ // ═══════════════════════════════════════════════════════════════════════
105
+ /**
106
+ * Replace the text of a paragraph with minimal DOM changes.
107
+ * Sets the FIRST w:t to `text`, clears every subsequent w:t.
108
+ * Sets xml:space="preserve" so leading/trailing spaces survive.
109
+ * Does NOT recreate runs or remove paragraph properties.
110
+ */
111
+ export function setParagraphTextMinimal(p, text) {
112
+ const tNodes = p.getElementsByTagName('w:t');
113
+ if (tNodes.length === 0)
114
+ return;
115
+ const first = tNodes.item(0);
116
+ first.textContent = text;
117
+ first.setAttribute('xml:space', 'preserve');
118
+ for (let i = 1; i < tNodes.length; i++) {
119
+ tNodes.item(i).textContent = '';
120
+ }
121
+ }
122
+ // ═══════════════════════════════════════════════════════════════════════
123
+ // Run-level formatting helpers
124
+ // ═══════════════════════════════════════════════════════════════════════
125
+ /**
126
+ * Ensure a <w:r> element has w:rPr/w:color[@w:val=hex].
127
+ * Creates w:rPr and w:color if they don't exist.
128
+ * Only touches the colour — leaves every other run property intact.
129
+ */
130
+ export function ensureRunColor(run, hex) {
131
+ const doc = run.ownerDocument;
132
+ if (!doc)
133
+ return;
134
+ let rPr = findDirectChild(run, 'w:rPr');
135
+ if (!rPr) {
136
+ rPr = doc.createElement('w:rPr');
137
+ if (run.firstChild) {
138
+ run.insertBefore(rPr, run.firstChild);
139
+ }
140
+ else {
141
+ run.appendChild(rPr);
142
+ }
143
+ }
144
+ let colorEl = findDirectChild(rPr, 'w:color');
145
+ if (!colorEl) {
146
+ colorEl = doc.createElement('w:color');
147
+ rPr.appendChild(colorEl);
148
+ }
149
+ colorEl.setAttribute('w:val', hex);
150
+ }
151
+ /**
152
+ * Apply run-level colour to every <w:r> in a paragraph.
153
+ */
154
+ export function colorParagraphRuns(p, color) {
155
+ const runs = nodeListToArray(p.getElementsByTagName('w:r'));
156
+ for (const r of runs) {
157
+ ensureRunColor(r, color);
158
+ }
159
+ }
160
+ /**
161
+ * Apply bold / italic / color to every <w:r> in a paragraph.
162
+ * Preserves all existing w:rPr children; only modifies specified props.
163
+ */
164
+ export function styleParagraphRuns(p, style) {
165
+ const doc = p.ownerDocument;
166
+ if (!doc)
167
+ return;
168
+ const runs = nodeListToArray(p.getElementsByTagName('w:r'));
169
+ for (const r of runs) {
170
+ let rPr = findDirectChild(r, 'w:rPr');
171
+ if (!rPr) {
172
+ rPr = doc.createElement('w:rPr');
173
+ if (r.firstChild) {
174
+ r.insertBefore(rPr, r.firstChild);
175
+ }
176
+ else {
177
+ r.appendChild(rPr);
178
+ }
179
+ }
180
+ if (style.color) {
181
+ let colorNode = findDirectChild(rPr, 'w:color');
182
+ if (!colorNode) {
183
+ colorNode = doc.createElement('w:color');
184
+ rPr.appendChild(colorNode);
185
+ }
186
+ colorNode.setAttribute('w:val', style.color);
187
+ }
188
+ if (style.bold !== undefined) {
189
+ toggleElement(doc, rPr, 'w:b', style.bold);
190
+ }
191
+ if (style.italic !== undefined) {
192
+ toggleElement(doc, rPr, 'w:i', style.italic);
193
+ }
194
+ }
195
+ }
196
+ // ═══════════════════════════════════════════════════════════════════════
197
+ // Counting helpers
198
+ // ═══════════════════════════════════════════════════════════════════════
199
+ /** Count direct w:tbl children of body. */
200
+ export function countTables(children) {
201
+ return children.filter((ch) => ch.nodeName === 'w:tbl').length;
202
+ }
203
+ /** Count <w:drawing> descendants (rough image count). */
204
+ export function countImages(body) {
205
+ return body.getElementsByTagName('w:drawing').length;
206
+ }
207
+ // ═══════════════════════════════════════════════════════════════════════
208
+ // Private helpers (DRY: used by multiple public functions)
209
+ // ═══════════════════════════════════════════════════════════════════════
210
+ /** Find the first direct child element with the given nodeName. */
211
+ function findDirectChild(parent, nodeName) {
212
+ for (const child of nodeListToArray(parent.childNodes)) {
213
+ if (child.nodeType === 1 && child.nodeName === nodeName) {
214
+ return child;
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+ /** Add or remove a simple flag element (e.g. w:b, w:i) inside a parent. */
220
+ function toggleElement(doc, parent, nodeName, enabled) {
221
+ const existing = findDirectChild(parent, nodeName);
222
+ if (enabled && !existing) {
223
+ parent.appendChild(doc.createElement(nodeName));
224
+ }
225
+ else if (!enabled && existing) {
226
+ parent.removeChild(existing);
227
+ }
228
+ }
@@ -1,14 +1,10 @@
1
1
  /**
2
- * DOCX Operations LibraryPublic API
3
- *
4
- * Re-exports only the symbols that external consumers need.
5
- * Internal modules (styled-html-parser, validators, converters, etc.)
6
- * are consumed by sibling files and are NOT part of the public surface.
7
- *
8
- * @module docx
2
+ * DOCX file manipulation tools barrel exports.
9
3
  */
10
- export { parseDocxToHtml } from './html.js';
11
- export { createDocxFromHtml } from './builders/html-builder.js';
12
- export { editDocxWithOperations } from './operations/index.js';
13
- export type { DocxParseResult, DocxMetadata, DocxImage, DocxSection, DocxOperation, DocxDocumentDefaults, } from './types.js';
14
- export { DocxError, DocxErrorCode } from './errors.js';
4
+ export { readDocxOutline } from './read.js';
5
+ export { writeDocxPatched } from './write.js';
6
+ export { createDocxNew } from './create.js';
7
+ export type { DocxContentStructure, DocxContentItem, DocxContentParagraph, DocxContentTable, DocxContentImage, } from './types.js';
8
+ export { readDocx, extractTextFromDocx, getDocxMetadata, extractBodyXml } from './read.js';
9
+ export { writeDocx, modifyDocxContent, replaceBodyXml } from './modify.js';
10
+ export type { DocxMetadata, DocxParagraph, DocxRun, DocxModification, ParagraphOutline, ReadDocxResult, WriteDocxStats, WriteDocxResult, BodySnapshot, DocxOp, OpResult, } from './types.js';
@@ -1,16 +1,10 @@
1
1
  /**
2
- * DOCX Operations LibraryPublic API
3
- *
4
- * Re-exports only the symbols that external consumers need.
5
- * Internal modules (styled-html-parser, validators, converters, etc.)
6
- * are consumed by sibling files and are NOT part of the public surface.
7
- *
8
- * @module docx
2
+ * DOCX file manipulation tools barrel exports.
9
3
  */
10
- // ── Reading ─────────────────────────────────────────────────────────────────
11
- export { parseDocxToHtml } from './html.js';
12
- // ── Writing / Editing ───────────────────────────────────────────────────────
13
- export { createDocxFromHtml } from './builders/html-builder.js';
14
- export { editDocxWithOperations } from './operations/index.js';
15
- // ── Errors ──────────────────────────────────────────────────────────────────
16
- export { DocxError, DocxErrorCode } from './errors.js';
4
+ // Patch-based tools (read_docx / write_docx)
5
+ export { readDocxOutline } from './read.js';
6
+ export { writeDocxPatched } from './write.js';
7
+ export { createDocxNew } from './create.js';
8
+ // Legacy functions (used by read_file, write_file, edit_block handlers)
9
+ export { readDocx, extractTextFromDocx, getDocxMetadata, extractBodyXml } from './read.js';
10
+ export { writeDocx, modifyDocxContent, replaceBodyXml } from './modify.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Legacy DOCX modification operations.
3
+ *
4
+ * These functions support the older write_file / edit_block paths that
5
+ * modify DOCX via simple operations (replace, insert, delete, style).
6
+ * They are distinct from the new patch-based writeDocxPatched pipeline.
7
+ *
8
+ * Single Responsibility: create / modify DOCX content using the legacy
9
+ * DocxModification interface. Delegates XML parsing and element
10
+ * manipulation to the shared dom.ts module.
11
+ */
12
+ import type { DocxModification } from './types.js';
13
+ /**
14
+ * Open an existing DOCX, apply an ordered list of modifications to
15
+ * word/document.xml, and write the result to outputPath.
16
+ * Every other file in the ZIP (styles, images, rels, …) is preserved.
17
+ */
18
+ export declare function modifyDocxContent(inputPath: string, outputPath: string, modifications: DocxModification[]): Promise<void>;
19
+ /**
20
+ * Replace the entire w:body content of a DOCX with new body XML.
21
+ * Used by the body-XML replacement mode of write_file.
22
+ */
23
+ export declare function replaceBodyXml(inputPath: string, outputPath: string, newBodyXml: string): Promise<void>;
24
+ /**
25
+ * Create a brand-new minimal DOCX from a plain-text string.
26
+ * Double-newlines are treated as paragraph separators.
27
+ */
28
+ export declare function writeDocx(outputPath: string, content: string | DocxModification[]): Promise<void>;
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Legacy DOCX modification operations.
3
+ *
4
+ * These functions support the older write_file / edit_block paths that
5
+ * modify DOCX via simple operations (replace, insert, delete, style).
6
+ * They are distinct from the new patch-based writeDocxPatched pipeline.
7
+ *
8
+ * Single Responsibility: create / modify DOCX content using the legacy
9
+ * DocxModification interface. Delegates XML parsing and element
10
+ * manipulation to the shared dom.ts module.
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+ import os from 'os';
15
+ import PizZip from 'pizzip';
16
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
17
+ import { nodeListToArray, getParagraphText, setParagraphTextMinimal, colorParagraphRuns, styleParagraphRuns, } from './dom.js';
18
+ // ═══════════════════════════════════════════════════════════════════════
19
+ // Helpers (private to this module)
20
+ // ═══════════════════════════════════════════════════════════════════════
21
+ /** Get all direct w:p children of body in document order. */
22
+ function getParagraphs(body) {
23
+ const paragraphs = [];
24
+ for (const child of nodeListToArray(body.childNodes)) {
25
+ if (child.nodeType === 1 && child.nodeName === 'w:p') {
26
+ paragraphs.push(child);
27
+ }
28
+ }
29
+ return paragraphs;
30
+ }
31
+ /** Parse DOCX and return { zip, dom, body }. */
32
+ function parseDocument(inputBuf) {
33
+ const zip = new PizZip(inputBuf);
34
+ const docFile = zip.file('word/document.xml');
35
+ if (!docFile)
36
+ throw new Error('Invalid DOCX: missing word/document.xml');
37
+ const dom = new DOMParser().parseFromString(docFile.asText(), 'application/xml');
38
+ const body = dom.getElementsByTagName('w:body').item(0);
39
+ if (!body)
40
+ throw new Error('Invalid DOCX: missing w:body');
41
+ return { zip, dom, body };
42
+ }
43
+ // ═══════════════════════════════════════════════════════════════════════
44
+ // modifyDocxContent — apply legacy modifications
45
+ // ═══════════════════════════════════════════════════════════════════════
46
+ /**
47
+ * Open an existing DOCX, apply an ordered list of modifications to
48
+ * word/document.xml, and write the result to outputPath.
49
+ * Every other file in the ZIP (styles, images, rels, …) is preserved.
50
+ */
51
+ export async function modifyDocxContent(inputPath, outputPath, modifications) {
52
+ const inputBuf = await fs.readFile(inputPath);
53
+ const { zip, dom, body } = parseDocument(inputBuf);
54
+ for (const mod of modifications) {
55
+ switch (mod.type) {
56
+ case 'replace':
57
+ applyReplace(body, mod);
58
+ break;
59
+ case 'insert':
60
+ applyInsert(body, mod);
61
+ break;
62
+ case 'delete':
63
+ applyDelete(body, mod);
64
+ break;
65
+ case 'style':
66
+ applyStyle(body, mod);
67
+ break;
68
+ }
69
+ }
70
+ const outXml = new XMLSerializer().serializeToString(dom);
71
+ zip.file('word/document.xml', outXml);
72
+ const outBuf = zip.generate({ type: 'nodebuffer' });
73
+ await fs.writeFile(outputPath, outBuf);
74
+ }
75
+ // ═══════════════════════════════════════════════════════════════════════
76
+ // replaceBodyXml — wholesale body replacement
77
+ // ═══════════════════════════════════════════════════════════════════════
78
+ /**
79
+ * Replace the entire w:body content of a DOCX with new body XML.
80
+ * Used by the body-XML replacement mode of write_file.
81
+ */
82
+ export async function replaceBodyXml(inputPath, outputPath, newBodyXml) {
83
+ const tempDir = os.tmpdir();
84
+ const tempDocxPath = path.join(tempDir, `docx_temp_${Date.now()}_${Math.random().toString(36).substring(7)}.docx`);
85
+ const tempXmlPath = path.join(tempDir, `docx_dom_${Date.now()}_${Math.random().toString(36).substring(7)}.xml`);
86
+ try {
87
+ const inputBuf = await fs.readFile(inputPath);
88
+ await fs.writeFile(tempDocxPath, inputBuf);
89
+ const { zip, dom, body } = parseDocument(inputBuf);
90
+ await fs.writeFile(tempXmlPath, zip.file('word/document.xml').asText());
91
+ // Parse the new body XML
92
+ const newBodyDom = new DOMParser().parseFromString(`<root>${newBodyXml}</root>`, 'application/xml');
93
+ const newBodyElement = newBodyDom.documentElement.firstChild;
94
+ if (!newBodyElement || newBodyElement.nodeName !== 'w:body') {
95
+ throw new Error('Invalid body XML: must start with <w:body>');
96
+ }
97
+ // Import children from new body into original document
98
+ const doc = body.ownerDocument;
99
+ if (!doc)
100
+ throw new Error('Document owner not found');
101
+ while (body.firstChild)
102
+ body.removeChild(body.firstChild);
103
+ for (const child of nodeListToArray(newBodyElement.childNodes)) {
104
+ body.appendChild(doc.importNode(child, true));
105
+ }
106
+ const outXml = new XMLSerializer().serializeToString(dom);
107
+ zip.file('word/document.xml', outXml);
108
+ const outBuf = zip.generate({ type: 'nodebuffer' });
109
+ await fs.writeFile(outputPath, outBuf);
110
+ }
111
+ finally {
112
+ try {
113
+ await fs.unlink(tempDocxPath);
114
+ }
115
+ catch { /* ignore */ }
116
+ try {
117
+ await fs.unlink(tempXmlPath);
118
+ }
119
+ catch { /* ignore */ }
120
+ }
121
+ }
122
+ // ═══════════════════════════════════════════════════════════════════════
123
+ // writeDocx — create minimal DOCX from plain text
124
+ // ═══════════════════════════════════════════════════════════════════════
125
+ /**
126
+ * Create a brand-new minimal DOCX from a plain-text string.
127
+ * Double-newlines are treated as paragraph separators.
128
+ */
129
+ export async function writeDocx(outputPath, content) {
130
+ if (typeof content !== 'string') {
131
+ throw new Error('Modifications require an existing DOCX file. Use modifyDocxContent() instead.');
132
+ }
133
+ const zip = new PizZip();
134
+ const escaped = (s) => s.replace(/&/g, '&amp;')
135
+ .replace(/</g, '&lt;')
136
+ .replace(/>/g, '&gt;')
137
+ .replace(/"/g, '&quot;')
138
+ .replace(/'/g, '&apos;');
139
+ const docXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
140
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
141
+ <w:body>
142
+ ${content
143
+ .split('\n\n')
144
+ .map((para) => ` <w:p>
145
+ <w:r>
146
+ <w:t>${escaped(para)}</w:t>
147
+ </w:r>
148
+ </w:p>`)
149
+ .join('\n')}
150
+ </w:body>
151
+ </w:document>`;
152
+ zip.file('word/document.xml', docXml);
153
+ zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
154
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
155
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
156
+ <Default Extension="xml" ContentType="application/xml"/>
157
+ <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
158
+ </Types>`);
159
+ zip.folder('_rels')?.file('.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
160
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
161
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
162
+ </Relationships>`);
163
+ zip.folder('word')?.folder('_rels')?.file('document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
164
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
165
+ </Relationships>`);
166
+ const outBuf = zip.generate({ type: 'nodebuffer' });
167
+ await fs.writeFile(outputPath, outBuf);
168
+ }
169
+ // ═══════════════════════════════════════════════════════════════════════
170
+ // Private modification appliers (SRP: one function per modification type)
171
+ // ═══════════════════════════════════════════════════════════════════════
172
+ function applyReplace(body, mod) {
173
+ if (mod.findText === undefined)
174
+ return;
175
+ const target = mod.findText.trim();
176
+ for (const child of nodeListToArray(body.childNodes)) {
177
+ if (child.nodeType !== 1 || child.nodeName !== 'w:p')
178
+ continue;
179
+ if (getParagraphText(child).trim() !== target)
180
+ continue;
181
+ if (mod.replaceText !== undefined) {
182
+ setParagraphTextMinimal(child, mod.replaceText);
183
+ }
184
+ if (mod.style) {
185
+ if (mod.style.color)
186
+ colorParagraphRuns(child, mod.style.color);
187
+ if (mod.style.bold !== undefined || mod.style.italic !== undefined) {
188
+ styleParagraphRuns(child, mod.style);
189
+ }
190
+ }
191
+ break; // first match only
192
+ }
193
+ }
194
+ function applyInsert(body, mod) {
195
+ if (mod.paragraphIndex === undefined || mod.insertText === undefined)
196
+ return;
197
+ const doc = body.ownerDocument;
198
+ if (!doc)
199
+ return;
200
+ const newP = doc.createElement('w:p');
201
+ const newR = doc.createElement('w:r');
202
+ const newT = doc.createElement('w:t');
203
+ newT.textContent = mod.insertText;
204
+ newR.appendChild(newT);
205
+ newP.appendChild(newR);
206
+ const paragraphs = getParagraphs(body);
207
+ const idx = mod.paragraphIndex < 0
208
+ ? paragraphs.length + mod.paragraphIndex + 1
209
+ : mod.paragraphIndex;
210
+ if (idx < 0 || idx > paragraphs.length)
211
+ return;
212
+ if (idx === paragraphs.length) {
213
+ body.appendChild(newP);
214
+ }
215
+ else {
216
+ let current = 0;
217
+ for (const child of nodeListToArray(body.childNodes)) {
218
+ if (child.nodeType !== 1 || child.nodeName !== 'w:p')
219
+ continue;
220
+ if (current === idx) {
221
+ body.insertBefore(newP, child);
222
+ break;
223
+ }
224
+ current++;
225
+ }
226
+ }
227
+ }
228
+ function applyDelete(body, mod) {
229
+ if (mod.paragraphIndex === undefined)
230
+ return;
231
+ const paragraphs = getParagraphs(body);
232
+ const idx = mod.paragraphIndex < 0
233
+ ? paragraphs.length + mod.paragraphIndex
234
+ : mod.paragraphIndex;
235
+ if (idx < 0 || idx >= paragraphs.length)
236
+ return;
237
+ let current = 0;
238
+ for (const child of nodeListToArray(body.childNodes)) {
239
+ if (child.nodeType !== 1 || child.nodeName !== 'w:p')
240
+ continue;
241
+ if (current === idx) {
242
+ body.removeChild(child);
243
+ break;
244
+ }
245
+ current++;
246
+ }
247
+ }
248
+ function applyStyle(body, mod) {
249
+ if (mod.paragraphIndex === undefined || !mod.style)
250
+ return;
251
+ const paragraphs = getParagraphs(body);
252
+ const idx = mod.paragraphIndex < 0
253
+ ? paragraphs.length + mod.paragraphIndex
254
+ : mod.paragraphIndex;
255
+ if (idx < 0 || idx >= paragraphs.length)
256
+ return;
257
+ let current = 0;
258
+ for (const child of nodeListToArray(body.childNodes)) {
259
+ if (child.nodeType !== 1 || child.nodeName !== 'w:p')
260
+ continue;
261
+ if (current === idx) {
262
+ if (mod.style.color)
263
+ colorParagraphRuns(child, mod.style.color);
264
+ if (mod.style.bold !== undefined || mod.style.italic !== undefined) {
265
+ styleParagraphRuns(child, mod.style);
266
+ }
267
+ break;
268
+ }
269
+ current++;
270
+ }
271
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Op: delete_paragraph_at_body_index
3
+ *
4
+ * Remove the w:p element at the given bodyChildIndex.
5
+ * Skips if the child is not a w:p.
6
+ *
7
+ * NOTE: This is a structural op — it decreases bodyChildCount by 1.
8
+ * The orchestrator must account for this when validating invariants.
9
+ */
10
+ import type { DeleteParagraphAtBodyIndexOp, OpResult } from '../types.js';
11
+ export declare function applyDeleteParagraphAtBodyIndex(body: Element, op: DeleteParagraphAtBodyIndexOp): OpResult;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Op: delete_paragraph_at_body_index
3
+ *
4
+ * Remove the w:p element at the given bodyChildIndex.
5
+ * Skips if the child is not a w:p.
6
+ *
7
+ * NOTE: This is a structural op — it decreases bodyChildCount by 1.
8
+ * The orchestrator must account for this when validating invariants.
9
+ */
10
+ import { getBodyChildren } from '../dom.js';
11
+ export function applyDeleteParagraphAtBodyIndex(body, op) {
12
+ const children = getBodyChildren(body);
13
+ const idx = op.bodyChildIndex;
14
+ if (idx < 0 || idx >= children.length) {
15
+ return { op, status: 'skipped', matched: 0, reason: 'index_out_of_range' };
16
+ }
17
+ const child = children[idx];
18
+ if (child.nodeName !== 'w:p') {
19
+ return { op, status: 'skipped', matched: 0, reason: 'not_a_paragraph' };
20
+ }
21
+ body.removeChild(child);
22
+ return { op, status: 'applied', matched: 1 };
23
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Op: header_replace_text_exact
3
+ *
4
+ * Find ALL header XML files (word/header1.xml, header2.xml, …)
5
+ * in the ZIP, locate the first paragraph matching exact trimmed text,
6
+ * and replace its text minimally.
7
+ *
8
+ * This op modifies header XML files inside the ZIP — not document.xml body.
9
+ * It receives the PizZip instance from the orchestrator.
10
+ */
11
+ import PizZip from 'pizzip';
12
+ import type { HeaderReplaceTextExactOp, OpResult } from '../types.js';
13
+ export declare function applyHeaderReplaceTextExact(_body: Element, op: HeaderReplaceTextExactOp, zip: PizZip): OpResult;