@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.
- package/README.md +2 -0
- package/dist/handlers/filesystem-handlers.js +58 -11
- package/dist/handlers/history-handlers.d.ts +7 -0
- package/dist/handlers/history-handlers.js +33 -1
- package/dist/server.js +30 -4
- package/dist/tools/docx/builders/image.d.ts +14 -0
- package/dist/tools/docx/builders/image.js +84 -0
- package/dist/tools/docx/builders/index.d.ts +9 -3
- package/dist/tools/docx/builders/index.js +9 -3
- package/dist/tools/docx/builders/paragraph.d.ts +12 -0
- package/dist/tools/docx/builders/paragraph.js +29 -0
- package/dist/tools/docx/builders/table.d.ts +8 -0
- package/dist/tools/docx/builders/table.js +94 -0
- package/dist/tools/docx/builders/utils.d.ts +5 -0
- package/dist/tools/docx/builders/utils.js +18 -0
- package/dist/tools/docx/constants.d.ts +28 -32
- package/dist/tools/docx/constants.js +56 -52
- package/dist/tools/docx/create.d.ts +21 -0
- package/dist/tools/docx/create.js +386 -0
- package/dist/tools/docx/dom.d.ts +66 -0
- package/dist/tools/docx/dom.js +228 -0
- package/dist/tools/docx/index.d.ts +8 -12
- package/dist/tools/docx/index.js +8 -14
- package/dist/tools/docx/modify.d.ts +28 -0
- package/dist/tools/docx/modify.js +271 -0
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +11 -0
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +23 -0
- package/dist/tools/docx/ops/header-replace-text-exact.d.ts +13 -0
- package/dist/tools/docx/ops/header-replace-text-exact.js +55 -0
- package/dist/tools/docx/ops/index.d.ts +17 -0
- package/dist/tools/docx/ops/index.js +67 -0
- package/dist/tools/docx/ops/insert-image-after-text.d.ts +24 -0
- package/dist/tools/docx/ops/insert-image-after-text.js +128 -0
- package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +12 -0
- package/dist/tools/docx/ops/insert-paragraph-after-text.js +74 -0
- package/dist/tools/docx/ops/insert-table-after-text.d.ts +19 -0
- package/dist/tools/docx/ops/insert-table-after-text.js +57 -0
- package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +12 -0
- package/dist/tools/docx/ops/replace-hyperlink-url.js +37 -0
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +9 -0
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +25 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +9 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.js +21 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +8 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +23 -0
- package/dist/tools/docx/ops/set-color-for-style.d.ts +9 -0
- package/dist/tools/docx/ops/set-color-for-style.js +27 -0
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +8 -0
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +57 -0
- package/dist/tools/docx/ops/table-set-cell-text.d.ts +9 -0
- package/dist/tools/docx/ops/table-set-cell-text.js +72 -0
- package/dist/tools/docx/read.d.ts +27 -0
- package/dist/tools/docx/read.js +188 -0
- package/dist/tools/docx/relationships.d.ts +22 -0
- package/dist/tools/docx/relationships.js +76 -0
- package/dist/tools/docx/types.d.ts +174 -104
- package/dist/tools/docx/types.js +2 -5
- package/dist/tools/docx/validate.d.ts +33 -0
- package/dist/tools/docx/validate.js +49 -0
- package/dist/tools/docx/write.d.ts +17 -0
- package/dist/tools/docx/write.js +88 -0
- package/dist/tools/docx/zip.d.ts +21 -0
- package/dist/tools/docx/zip.js +35 -0
- package/dist/tools/schemas.d.ts +13 -0
- package/dist/tools/schemas.js +5 -0
- package/dist/types.d.ts +10 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +18 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +13977 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +57 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +800 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +9 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +63 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +21 -0
- package/dist/ui/file-preview/src/components/toolbar.d.ts +6 -0
- package/dist/ui/file-preview/src/components/toolbar.js +75 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +764 -0
- package/dist/ui/resources.d.ts +21 -0
- package/dist/ui/resources.js +72 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-lifecycle.d.ts +16 -0
- package/dist/ui/shared/host-lifecycle.js +35 -0
- package/dist/ui/shared/rpc-client.d.ts +14 -0
- package/dist/ui/shared/rpc-client.js +72 -0
- package/dist/ui/shared/theme-adaptation.d.ts +10 -0
- package/dist/ui/shared/theme-adaptation.js +118 -0
- package/dist/ui/shared/tool-header.d.ts +9 -0
- package/dist/ui/shared/tool-header.js +25 -0
- package/dist/ui/shared/tool-shell.d.ts +16 -0
- package/dist/ui/shared/tool-shell.js +65 -0
- package/dist/ui/shared/widget-state.d.ts +28 -0
- package/dist/ui/shared/widget-state.js +60 -0
- package/dist/utils/capture.d.ts +1 -0
- package/dist/utils/capture.js +6 -0
- package/dist/utils/files/docx.d.ts +8 -15
- package/dist/utils/files/docx.js +76 -176
- package/dist/utils/files/text.js +9 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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 {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export type {
|
|
14
|
-
export {
|
|
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';
|
package/dist/tools/docx/index.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DOCX
|
|
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
|
-
//
|
|
11
|
-
export {
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export {
|
|
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, '&')
|
|
135
|
+
.replace(/</g, '<')
|
|
136
|
+
.replace(/>/g, '>')
|
|
137
|
+
.replace(/"/g, '"')
|
|
138
|
+
.replace(/'/g, ''');
|
|
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;
|