@wonderwhy-er/desktop-commander 0.2.36 → 0.2.38
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 +240 -100
- package/dist/command-manager.js +6 -3
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +22 -2
- package/dist/handlers/filesystem-handlers.js +6 -11
- package/dist/handlers/macos-control-handlers.d.ts +16 -0
- package/dist/handlers/macos-control-handlers.js +81 -0
- package/dist/lib.d.ts +10 -0
- package/dist/lib.js +10 -0
- package/dist/remote-device/remote-channel.d.ts +8 -3
- package/dist/remote-device/remote-channel.js +68 -21
- package/dist/search-manager.d.ts +13 -0
- package/dist/search-manager.js +146 -0
- package/dist/server.js +29 -1
- package/dist/test-docx.d.ts +1 -0
- package/dist/tools/config.d.ts +71 -0
- package/dist/tools/config.js +117 -2
- package/dist/tools/docx/builders/table.d.ts +2 -0
- package/dist/tools/docx/builders/table.js +60 -16
- package/dist/tools/docx/dom.d.ts +74 -1
- package/dist/tools/docx/dom.js +221 -1
- package/dist/tools/docx/index.d.ts +2 -2
- package/dist/tools/docx/ops/index.js +3 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +15 -3
- package/dist/tools/docx/ops/replace-paragraph-text-exact.js +25 -10
- package/dist/tools/docx/ops/replace-table-cell-text.d.ts +25 -0
- package/dist/tools/docx/ops/replace-table-cell-text.js +85 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +2 -1
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +9 -8
- package/dist/tools/docx/ops/set-color-for-style.d.ts +4 -0
- package/dist/tools/docx/ops/set-color-for-style.js +11 -7
- package/dist/tools/docx/ops/table-set-cell-text.js +8 -40
- package/dist/tools/docx/read.d.ts +2 -2
- package/dist/tools/docx/read.js +137 -17
- package/dist/tools/docx/types.d.ts +32 -3
- package/dist/tools/docx/xml-view-test.d.ts +1 -0
- package/dist/tools/docx/xml-view-test.js +63 -0
- package/dist/tools/docx/xml-view.d.ts +56 -0
- package/dist/tools/docx/xml-view.js +169 -0
- package/dist/tools/edit.js +57 -27
- package/dist/tools/macos-control/ax-adapter.d.ts +55 -0
- package/dist/tools/macos-control/ax-adapter.js +438 -0
- package/dist/tools/macos-control/cdp-adapter.d.ts +23 -0
- package/dist/tools/macos-control/cdp-adapter.js +402 -0
- package/dist/tools/macos-control/orchestrator.d.ts +77 -0
- package/dist/tools/macos-control/orchestrator.js +136 -0
- package/dist/tools/macos-control/role-aliases.d.ts +5 -0
- package/dist/tools/macos-control/role-aliases.js +34 -0
- package/dist/tools/macos-control/types.d.ts +129 -0
- package/dist/tools/macos-control/types.js +1 -0
- package/dist/tools/schemas.d.ts +3 -0
- package/dist/tools/schemas.js +2 -1
- package/dist/types.d.ts +0 -1
- package/dist/ui/config-editor/config-editor-runtime.js +14181 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +840 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +586 -0
- package/dist/ui/file-preview/preview-runtime.js +13337 -752
- package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
- package/dist/ui/file-preview/src/app.d.ts +5 -1
- package/dist/ui/file-preview/src/app.js +114 -200
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
- package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
- package/dist/ui/file-preview/styles.css +117 -83
- package/dist/ui/resources.d.ts +7 -0
- package/dist/ui/resources.js +16 -2
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +9 -0
- package/dist/ui/shared/tool-shell.js +46 -4
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/utils/capture.js +173 -11
- package/dist/utils/files/base.d.ts +3 -1
- package/dist/utils/files/docx.d.ts +28 -15
- package/dist/utils/files/docx.js +622 -88
- package/dist/utils/files/factory.d.ts +6 -5
- package/dist/utils/files/factory.js +18 -6
- package/dist/utils/system-info.js +1 -1
- package/dist/utils/usageTracker.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -3
package/dist/tools/docx/dom.js
CHANGED
|
@@ -54,6 +54,46 @@ export function getBodyChildren(body) {
|
|
|
54
54
|
}
|
|
55
55
|
return out;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Return ALL top‑level tables that are logically in the body, including those
|
|
59
|
+
* wrapped in structured document tags (w:sdt / w:sdtContent).
|
|
60
|
+
*
|
|
61
|
+
* Previous logic only saw tables that were direct children of <w:body>. That
|
|
62
|
+
* meant tables inside SDTs were invisible to table operations and readDocxOutline.
|
|
63
|
+
* This helper walks the body tree and collects any <w:tbl> that appears as a
|
|
64
|
+
* *first‑class* block (we do not recurse into tables themselves, so nested
|
|
65
|
+
* tables are not double‑counted).
|
|
66
|
+
*/
|
|
67
|
+
export function getAllBodyTables(body) {
|
|
68
|
+
const result = [];
|
|
69
|
+
function collectFromNode(node) {
|
|
70
|
+
const name = node.nodeName;
|
|
71
|
+
if (name === 'w:tbl') {
|
|
72
|
+
result.push(node);
|
|
73
|
+
return; // don't recurse into tables to avoid nested counting
|
|
74
|
+
}
|
|
75
|
+
// If this is a structured document tag, look into its content container.
|
|
76
|
+
if (name === 'w:sdt') {
|
|
77
|
+
const sdtContent = findDirectChild(node, 'w:sdtContent');
|
|
78
|
+
if (sdtContent) {
|
|
79
|
+
for (const child of nodeListToArray(sdtContent.childNodes)) {
|
|
80
|
+
if (child.nodeType === 1)
|
|
81
|
+
collectFromNode(child);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Generic container: recurse into element children.
|
|
87
|
+
for (const child of nodeListToArray(node.childNodes)) {
|
|
88
|
+
if (child.nodeType === 1)
|
|
89
|
+
collectFromNode(child);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const child of getBodyChildren(body)) {
|
|
93
|
+
collectFromNode(child);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
57
97
|
// ═══════════════════════════════════════════════════════════════════════
|
|
58
98
|
// Body signature
|
|
59
99
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -100,13 +140,108 @@ export function getParagraphStyle(p) {
|
|
|
100
140
|
return null;
|
|
101
141
|
}
|
|
102
142
|
// ═══════════════════════════════════════════════════════════════════════
|
|
143
|
+
// Table content extraction
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
145
|
+
/**
|
|
146
|
+
* Extract all text content from a table cell (w:tc).
|
|
147
|
+
* Returns the concatenated text from all paragraphs in the cell.
|
|
148
|
+
*/
|
|
149
|
+
export function getCellText(tc) {
|
|
150
|
+
const paragraphs = tc.getElementsByTagName('w:p');
|
|
151
|
+
const texts = [];
|
|
152
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
153
|
+
const p = paragraphs.item(i);
|
|
154
|
+
if (p) {
|
|
155
|
+
const text = getParagraphText(p).trim();
|
|
156
|
+
if (text)
|
|
157
|
+
texts.push(text);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return texts.join(' '); // Join multiple paragraphs in cell with space
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract all rows from a table (w:tbl).
|
|
164
|
+
* Returns an array of rows, where each row is an array of cell text strings.
|
|
165
|
+
* First row is treated as header if it exists.
|
|
166
|
+
*/
|
|
167
|
+
export function getTableContent(tbl) {
|
|
168
|
+
const rows = [];
|
|
169
|
+
for (const child of nodeListToArray(tbl.childNodes)) {
|
|
170
|
+
if (child.nodeType === 1 && child.nodeName === 'w:tr') {
|
|
171
|
+
rows.push(child);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (rows.length === 0) {
|
|
175
|
+
return { rows: [] };
|
|
176
|
+
}
|
|
177
|
+
// Extract cells from each row
|
|
178
|
+
const tableRows = [];
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
const cells = [];
|
|
181
|
+
for (const child of nodeListToArray(row.childNodes)) {
|
|
182
|
+
if (child.nodeType === 1 && child.nodeName === 'w:tc') {
|
|
183
|
+
cells.push(getCellText(child));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (cells.length > 0) {
|
|
187
|
+
tableRows.push(cells);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// First row might be header - check if it has bold formatting
|
|
191
|
+
// For simplicity, we'll treat first row as potential header
|
|
192
|
+
// User can determine this based on style or content
|
|
193
|
+
if (tableRows.length > 0) {
|
|
194
|
+
const firstRow = tableRows[0];
|
|
195
|
+
const restRows = tableRows.slice(1);
|
|
196
|
+
return {
|
|
197
|
+
headers: firstRow.length > 0 ? firstRow : undefined,
|
|
198
|
+
rows: restRows.length > 0 ? restRows : [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { rows: tableRows };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get table style from w:tblPr/w:tblStyle/@w:val, or null if absent.
|
|
205
|
+
*/
|
|
206
|
+
export function getTableStyle(tbl) {
|
|
207
|
+
const tblPr = tbl.getElementsByTagName('w:tblPr').item(0);
|
|
208
|
+
if (!tblPr)
|
|
209
|
+
return null;
|
|
210
|
+
const tblStyle = tblPr.getElementsByTagName('w:tblStyle').item(0);
|
|
211
|
+
if (!tblStyle)
|
|
212
|
+
return null;
|
|
213
|
+
return tblStyle.getAttribute('w:val');
|
|
214
|
+
}
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
216
|
+
// Image reference extraction
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
218
|
+
/**
|
|
219
|
+
* Extract image reference from a w:drawing element.
|
|
220
|
+
* Returns the relationship ID (rId) and media file path if found.
|
|
221
|
+
*/
|
|
222
|
+
export function getImageReference(drawing) {
|
|
223
|
+
// Find a:blip/@r:embed to get the relationship ID
|
|
224
|
+
const blip = drawing.getElementsByTagName('a:blip').item(0);
|
|
225
|
+
if (!blip)
|
|
226
|
+
return { rId: null, mediaPath: null };
|
|
227
|
+
const rId = blip.getAttribute('r:embed');
|
|
228
|
+
if (!rId)
|
|
229
|
+
return { rId: null, mediaPath: null };
|
|
230
|
+
// Media path will be resolved from relationships file
|
|
231
|
+
// For now, return the rId - the caller will resolve it from rels
|
|
232
|
+
return { rId, mediaPath: null };
|
|
233
|
+
}
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
103
235
|
// Minimal text replacement
|
|
104
236
|
// ═══════════════════════════════════════════════════════════════════════
|
|
105
237
|
/**
|
|
106
238
|
* Replace the text of a paragraph with minimal DOM changes.
|
|
107
239
|
* Sets the FIRST w:t to `text`, clears every subsequent w:t.
|
|
108
240
|
* Sets xml:space="preserve" so leading/trailing spaces survive.
|
|
109
|
-
* Does NOT recreate runs or remove paragraph properties.
|
|
241
|
+
* Does NOT remove/recreate runs or remove paragraph properties.
|
|
242
|
+
*
|
|
243
|
+
* WARNING: This function does NOT preserve multiple runs with different styles.
|
|
244
|
+
* Use setParagraphTextPreservingStyles() for cells with multiple styled runs.
|
|
110
245
|
*/
|
|
111
246
|
export function setParagraphTextMinimal(p, text) {
|
|
112
247
|
const tNodes = p.getElementsByTagName('w:t');
|
|
@@ -119,6 +254,91 @@ export function setParagraphTextMinimal(p, text) {
|
|
|
119
254
|
tNodes.item(i).textContent = '';
|
|
120
255
|
}
|
|
121
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Replace paragraph text while preserving all run styles.
|
|
259
|
+
*
|
|
260
|
+
* This function preserves the structure of all runs (w:r) and their
|
|
261
|
+
* properties (w:rPr), distributing the new text across existing runs.
|
|
262
|
+
*
|
|
263
|
+
* Strategy:
|
|
264
|
+
* 1. Collect all runs with their properties
|
|
265
|
+
* 2. Distribute new text across runs (preserving run count and styles)
|
|
266
|
+
* 3. If new text is longer, extend the last run
|
|
267
|
+
* 4. If new text is shorter, clear excess runs but keep their structure
|
|
268
|
+
*/
|
|
269
|
+
export function setParagraphTextPreservingStyles(p, text) {
|
|
270
|
+
const doc = p.ownerDocument;
|
|
271
|
+
if (!doc)
|
|
272
|
+
return;
|
|
273
|
+
// Work on ALL <w:t> descendants, not just direct children.
|
|
274
|
+
// This covers runs inside hyperlinks, smart tags, etc.
|
|
275
|
+
const tNodes = p.getElementsByTagName('w:t');
|
|
276
|
+
if (tNodes.length === 0) {
|
|
277
|
+
// No text nodes exist - create a minimal run + text.
|
|
278
|
+
const r = doc.createElement('w:r');
|
|
279
|
+
const t = doc.createElement('w:t');
|
|
280
|
+
t.setAttribute('xml:space', 'preserve');
|
|
281
|
+
t.textContent = text;
|
|
282
|
+
r.appendChild(t);
|
|
283
|
+
p.appendChild(r);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// First text node gets the NEW text.
|
|
287
|
+
const first = tNodes.item(0);
|
|
288
|
+
first.textContent = text;
|
|
289
|
+
first.setAttribute('xml:space', 'preserve');
|
|
290
|
+
// All other text nodes are cleared, but their runs (and w:rPr) remain,
|
|
291
|
+
// so formatting structures are preserved while old text disappears.
|
|
292
|
+
for (let i = 1; i < tNodes.length; i++) {
|
|
293
|
+
const t = tNodes.item(i);
|
|
294
|
+
if (t)
|
|
295
|
+
t.textContent = '';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Replace cell text while preserving ALL paragraphs and their styles.
|
|
300
|
+
*
|
|
301
|
+
* This function works at the cell level:
|
|
302
|
+
* - Preserves ALL paragraphs in the cell (doesn't remove any)
|
|
303
|
+
* - Updates text in the first paragraph while preserving its styles
|
|
304
|
+
* - Keeps all other paragraphs intact with their original text and styles
|
|
305
|
+
*
|
|
306
|
+
* This ensures that cells with multiple paragraphs (each with different
|
|
307
|
+
* styles, font sizes, etc.) maintain their structure after text replacement.
|
|
308
|
+
*
|
|
309
|
+
* Example: If a cell has:
|
|
310
|
+
* - Paragraph 1: "LAWN AND LANDSCAPE" (Heading1 style, large font, red color)
|
|
311
|
+
* - Paragraph 2: "Take your weekends back..." (Normal style, smaller font, black color)
|
|
312
|
+
*
|
|
313
|
+
* Replacing with "EARTH AND MOUNTAIN" will:
|
|
314
|
+
* - Update paragraph 1 to "EARTH AND MOUNTAIN" (preserving Heading1 style, large font, red color)
|
|
315
|
+
* - Keep paragraph 2 completely intact with its original text and style
|
|
316
|
+
*/
|
|
317
|
+
export function setCellTextPreservingStyles(tc, text) {
|
|
318
|
+
// Convert NodeList to array to avoid live NodeList issues
|
|
319
|
+
const paragraphs = nodeListToArray(tc.getElementsByTagName('w:p'));
|
|
320
|
+
if (paragraphs.length === 0) {
|
|
321
|
+
// Cell has no paragraphs - create one
|
|
322
|
+
const doc = tc.ownerDocument;
|
|
323
|
+
if (!doc)
|
|
324
|
+
return;
|
|
325
|
+
const p = doc.createElement('w:p');
|
|
326
|
+
const r = doc.createElement('w:r');
|
|
327
|
+
const t = doc.createElement('w:t');
|
|
328
|
+
t.setAttribute('xml:space', 'preserve');
|
|
329
|
+
t.textContent = text;
|
|
330
|
+
r.appendChild(t);
|
|
331
|
+
p.appendChild(r);
|
|
332
|
+
tc.appendChild(p);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Update first paragraph with new text, preserving all its styles
|
|
336
|
+
// This function preserves all runs and their properties (colors, bold, italic, etc.)
|
|
337
|
+
setParagraphTextPreservingStyles(paragraphs[0], text);
|
|
338
|
+
// CRITICAL: All other paragraphs remain completely untouched
|
|
339
|
+
// They keep their original text, styles, runs, and all formatting
|
|
340
|
+
// This ensures multi-paragraph cells maintain their full structure
|
|
341
|
+
}
|
|
122
342
|
// ═══════════════════════════════════════════════════════════════════════
|
|
123
343
|
// Run-level formatting helpers
|
|
124
344
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export { readDocxOutline } from './read.js';
|
|
5
5
|
export { writeDocxPatched } from './write.js';
|
|
6
6
|
export { createDocxNew } from './create.js';
|
|
7
|
-
export type { DocxContentStructure, DocxContentItem, DocxContentParagraph, DocxContentTable, DocxContentImage, } from './types.js';
|
|
7
|
+
export type { DocxContentStructure, DocxContentItem, DocxContentParagraph, DocxContentTable, DocxContentImage, DocxTableCellContent, } from './types.js';
|
|
8
8
|
export { readDocx, extractTextFromDocx, getDocxMetadata, extractBodyXml } from './read.js';
|
|
9
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';
|
|
10
|
+
export type { DocxMetadata, DocxParagraph, DocxRun, DocxModification, ParagraphOutline, TableOutline, ImageOutline, ReadDocxResult, WriteDocxStats, WriteDocxResult, BodySnapshot, DocxOp, OpResult, } from './types.js';
|
|
@@ -12,6 +12,7 @@ import { applySetParagraphStyleAtBodyIndex } from './set-paragraph-style-at-body
|
|
|
12
12
|
import { applyInsertParagraphAfterText } from './insert-paragraph-after-text.js';
|
|
13
13
|
import { applyDeleteParagraphAtBodyIndex } from './delete-paragraph-at-body-index.js';
|
|
14
14
|
import { applyTableSetCellText } from './table-set-cell-text.js';
|
|
15
|
+
import { applyReplaceTableCellText } from './replace-table-cell-text.js';
|
|
15
16
|
import { applyReplaceHyperlinkUrl } from './replace-hyperlink-url.js';
|
|
16
17
|
import { applyHeaderReplaceTextExact } from './header-replace-text-exact.js';
|
|
17
18
|
import { applyInsertTable } from './insert-table-after-text.js';
|
|
@@ -42,6 +43,8 @@ export function applyOp(body, op, zip) {
|
|
|
42
43
|
return applyDeleteParagraphAtBodyIndex(body, op);
|
|
43
44
|
case 'table_set_cell_text':
|
|
44
45
|
return applyTableSetCellText(body, op);
|
|
46
|
+
case 'replace_table_cell_text':
|
|
47
|
+
return applyReplaceTableCellText(body, op);
|
|
45
48
|
case 'replace_hyperlink_url':
|
|
46
49
|
if (!zip)
|
|
47
50
|
return { op, status: 'skipped', matched: 0, reason: 'zip_required_for_hyperlink_op' };
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op: replace_paragraph_text_exact
|
|
3
3
|
*
|
|
4
|
-
* Find FIRST paragraph whose trimmed text === `from
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Find FIRST paragraph whose trimmed text === `from` **anywhere in the body**,
|
|
5
|
+
* including paragraphs inside table cells, content controls, etc.
|
|
6
|
+
*
|
|
7
|
+
* Replacement behavior:
|
|
8
|
+
* - Replaces the matched paragraph's text while preserving all its run styles
|
|
9
|
+
* - Preserves all other paragraphs in the same cell (if the paragraph is in a table cell)
|
|
10
|
+
* - Preserves paragraph properties (w:pPr) and run properties (w:rPr)
|
|
11
|
+
*
|
|
12
|
+
* This is useful when you want to replace a specific paragraph by its exact text,
|
|
13
|
+
* especially in table cells where you want to replace one paragraph while keeping
|
|
14
|
+
* others intact. For example, replacing "LAWN AND LANDSCAPE" with "EARTH AND MOUNTAIN"
|
|
15
|
+
* in a cell that also contains a subtitle paragraph will preserve the subtitle.
|
|
16
|
+
*
|
|
17
|
+
* Note: For replacing entire cell content (matching by full cell text), use
|
|
18
|
+
* `replace_table_cell_text` instead.
|
|
7
19
|
*/
|
|
8
20
|
import type { ReplaceParagraphTextExactOp, OpResult } from '../types.js';
|
|
9
21
|
export declare function applyReplaceParagraphTextExact(body: Element, op: ReplaceParagraphTextExactOp): OpResult;
|
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op: replace_paragraph_text_exact
|
|
3
3
|
*
|
|
4
|
-
* Find FIRST paragraph whose trimmed text === `from
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Find FIRST paragraph whose trimmed text === `from` **anywhere in the body**,
|
|
5
|
+
* including paragraphs inside table cells, content controls, etc.
|
|
6
|
+
*
|
|
7
|
+
* Replacement behavior:
|
|
8
|
+
* - Replaces the matched paragraph's text while preserving all its run styles
|
|
9
|
+
* - Preserves all other paragraphs in the same cell (if the paragraph is in a table cell)
|
|
10
|
+
* - Preserves paragraph properties (w:pPr) and run properties (w:rPr)
|
|
11
|
+
*
|
|
12
|
+
* This is useful when you want to replace a specific paragraph by its exact text,
|
|
13
|
+
* especially in table cells where you want to replace one paragraph while keeping
|
|
14
|
+
* others intact. For example, replacing "LAWN AND LANDSCAPE" with "EARTH AND MOUNTAIN"
|
|
15
|
+
* in a cell that also contains a subtitle paragraph will preserve the subtitle.
|
|
16
|
+
*
|
|
17
|
+
* Note: For replacing entire cell content (matching by full cell text), use
|
|
18
|
+
* `replace_table_cell_text` instead.
|
|
7
19
|
*/
|
|
8
|
-
import {
|
|
20
|
+
import { getParagraphText, setParagraphTextPreservingStyles } from '../dom.js';
|
|
9
21
|
export function applyReplaceParagraphTextExact(body, op) {
|
|
10
|
-
const children = getBodyChildren(body);
|
|
11
22
|
const target = op.from.trim();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
// Traverse **all** paragraphs in the body, not just direct body children.
|
|
24
|
+
// This includes paragraphs inside table cells, content controls, etc.
|
|
25
|
+
const paragraphs = body.getElementsByTagName('w:p');
|
|
26
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
27
|
+
const p = paragraphs.item(i);
|
|
28
|
+
const paragraphText = getParagraphText(p).trim();
|
|
29
|
+
if (paragraphText === target) {
|
|
30
|
+
// Preserve all run styles (colors, bold, italic, etc.) when replacing
|
|
31
|
+
setParagraphTextPreservingStyles(p, op.to);
|
|
17
32
|
return { op, status: 'applied', matched: 1 };
|
|
18
33
|
}
|
|
19
34
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op: replace_table_cell_text
|
|
3
|
+
*
|
|
4
|
+
* Goal: Replace the "logical value" of a cell while preserving layout and styles.
|
|
5
|
+
*
|
|
6
|
+
* Matching strategy (tried in order):
|
|
7
|
+
* 1. Match by full cell text (all paragraphs joined with spaces)
|
|
8
|
+
* 2. Match by first paragraph text only
|
|
9
|
+
*
|
|
10
|
+
* When the caller passes FULL cell text in `from` / `to` (common for LLMs), we
|
|
11
|
+
* interpret the change like this:
|
|
12
|
+
*
|
|
13
|
+
* from: "<OLD_TITLE> <SUBTITLE ...>"
|
|
14
|
+
* to: "<NEW_TITLE> <SUBTITLE ...>"
|
|
15
|
+
*
|
|
16
|
+
* i.e. only the *title* (first paragraph) changed, the rest of the cell content
|
|
17
|
+
* stayed the same. We detect the unchanged suffix and compute NEW_TITLE by
|
|
18
|
+
* stripping that suffix from `to`. Then we only change the first paragraph text,
|
|
19
|
+
* keeping all other paragraphs (subtitle, etc.) exactly as they were.
|
|
20
|
+
*
|
|
21
|
+
* If we cannot safely detect that pattern, we fall back to treating `from`/`to`
|
|
22
|
+
* as simple first‑paragraph values.
|
|
23
|
+
*/
|
|
24
|
+
import type { ReplaceTableCellTextOp, OpResult } from '../types.js';
|
|
25
|
+
export declare function applyReplaceTableCellText(body: Element, op: ReplaceTableCellTextOp): OpResult;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op: replace_table_cell_text
|
|
3
|
+
*
|
|
4
|
+
* Goal: Replace the "logical value" of a cell while preserving layout and styles.
|
|
5
|
+
*
|
|
6
|
+
* Matching strategy (tried in order):
|
|
7
|
+
* 1. Match by full cell text (all paragraphs joined with spaces)
|
|
8
|
+
* 2. Match by first paragraph text only
|
|
9
|
+
*
|
|
10
|
+
* When the caller passes FULL cell text in `from` / `to` (common for LLMs), we
|
|
11
|
+
* interpret the change like this:
|
|
12
|
+
*
|
|
13
|
+
* from: "<OLD_TITLE> <SUBTITLE ...>"
|
|
14
|
+
* to: "<NEW_TITLE> <SUBTITLE ...>"
|
|
15
|
+
*
|
|
16
|
+
* i.e. only the *title* (first paragraph) changed, the rest of the cell content
|
|
17
|
+
* stayed the same. We detect the unchanged suffix and compute NEW_TITLE by
|
|
18
|
+
* stripping that suffix from `to`. Then we only change the first paragraph text,
|
|
19
|
+
* keeping all other paragraphs (subtitle, etc.) exactly as they were.
|
|
20
|
+
*
|
|
21
|
+
* If we cannot safely detect that pattern, we fall back to treating `from`/`to`
|
|
22
|
+
* as simple first‑paragraph values.
|
|
23
|
+
*/
|
|
24
|
+
import { getAllBodyTables, nodeListToArray, getCellText, getParagraphText, setCellTextPreservingStyles } from '../dom.js';
|
|
25
|
+
export function applyReplaceTableCellText(body, op) {
|
|
26
|
+
const target = op.from.trim();
|
|
27
|
+
// Find all logical tables in the body, including those inside SDTs.
|
|
28
|
+
const tables = getAllBodyTables(body);
|
|
29
|
+
// Search through all tables
|
|
30
|
+
for (const table of tables) {
|
|
31
|
+
// Get all rows
|
|
32
|
+
const rows = [];
|
|
33
|
+
for (const child of nodeListToArray(table.childNodes)) {
|
|
34
|
+
if (child.nodeType === 1 && child.nodeName === 'w:tr') {
|
|
35
|
+
rows.push(child);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Search through all cells in all rows
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
const cells = [];
|
|
41
|
+
for (const child of nodeListToArray(row.childNodes)) {
|
|
42
|
+
if (child.nodeType === 1 && child.nodeName === 'w:tc') {
|
|
43
|
+
cells.push(child);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const cell of cells) {
|
|
47
|
+
const cellText = getCellText(cell).trim();
|
|
48
|
+
// Strategy 1: full cell text match — try to detect a "title-only" change
|
|
49
|
+
if (cellText === target) {
|
|
50
|
+
const paragraphs = cell.getElementsByTagName('w:p');
|
|
51
|
+
if (paragraphs.length > 0) {
|
|
52
|
+
const firstP = paragraphs.item(0);
|
|
53
|
+
const firstPText = getParagraphText(firstP).trim();
|
|
54
|
+
// Cell's "suffix" is everything after the first paragraph text
|
|
55
|
+
const suffixFrom = cellText.slice(firstPText.length).trimStart();
|
|
56
|
+
const toTrimmed = op.to.trim();
|
|
57
|
+
let newFirstText = toTrimmed;
|
|
58
|
+
if (suffixFrom.length > 0 && toTrimmed.endsWith(suffixFrom)) {
|
|
59
|
+
// Common LLM pattern:
|
|
60
|
+
// from: "<OLD_TITLE> <SUFFIX>"
|
|
61
|
+
// to: "<NEW_TITLE> <SUFFIX>"
|
|
62
|
+
// Extract "<NEW_TITLE>" by removing the unchanged suffix.
|
|
63
|
+
newFirstText = toTrimmed
|
|
64
|
+
.slice(0, toTrimmed.length - suffixFrom.length)
|
|
65
|
+
.trimEnd();
|
|
66
|
+
}
|
|
67
|
+
setCellTextPreservingStyles(cell, newFirstText);
|
|
68
|
+
return { op, status: 'applied', matched: 1 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Strategy 2: match by first paragraph text only
|
|
72
|
+
const paragraphs = cell.getElementsByTagName('w:p');
|
|
73
|
+
if (paragraphs.length > 0) {
|
|
74
|
+
const firstP = paragraphs.item(0);
|
|
75
|
+
const firstParagraphText = getParagraphText(firstP).trim();
|
|
76
|
+
if (firstParagraphText === target) {
|
|
77
|
+
setCellTextPreservingStyles(cell, op.to);
|
|
78
|
+
return { op, status: 'applied', matched: 1 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { op, status: 'skipped', matched: 0, reason: 'no_match' };
|
|
85
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op: set_color_for_paragraph_exact
|
|
3
3
|
*
|
|
4
|
-
* Find FIRST paragraph whose trimmed text === `text
|
|
4
|
+
* Find FIRST paragraph whose trimmed text === `text` **anywhere in the body**,
|
|
5
|
+
* including paragraphs inside tables and other containers.
|
|
5
6
|
* Apply run-level colour to every w:r in that paragraph.
|
|
6
7
|
*/
|
|
7
8
|
import type { SetColorForParagraphExactOp, OpResult } from '../types.js';
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Op: set_color_for_paragraph_exact
|
|
3
3
|
*
|
|
4
|
-
* Find FIRST paragraph whose trimmed text === `text
|
|
4
|
+
* Find FIRST paragraph whose trimmed text === `text` **anywhere in the body**,
|
|
5
|
+
* including paragraphs inside tables and other containers.
|
|
5
6
|
* Apply run-level colour to every w:r in that paragraph.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { getParagraphText, ensureRunColor } from '../dom.js';
|
|
8
9
|
export function applySetColorForParagraphExact(body, op) {
|
|
9
|
-
const children = getBodyChildren(body);
|
|
10
10
|
const target = op.text.trim();
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Traverse **all** paragraphs in the body, not just direct children.
|
|
12
|
+
const paragraphs = body.getElementsByTagName('w:p');
|
|
13
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
14
|
+
const p = paragraphs.item(i);
|
|
15
|
+
if (getParagraphText(p).trim() !== target)
|
|
13
16
|
continue;
|
|
14
|
-
|
|
15
|
-
continue;
|
|
16
|
-
const runs = child.getElementsByTagName('w:r');
|
|
17
|
+
const runs = p.getElementsByTagName('w:r');
|
|
17
18
|
for (let i = 0; i < runs.length; i++) {
|
|
18
19
|
ensureRunColor(runs.item(i), op.color);
|
|
19
20
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* For every paragraph whose w:pPr/w:pStyle/@w:val === style,
|
|
5
5
|
* set run-level colour on every w:r in that paragraph.
|
|
6
|
+
*
|
|
7
|
+
* This now includes paragraphs inside tables and other containers,
|
|
8
|
+
* not just direct w:p children of w:body.
|
|
9
|
+
*
|
|
6
10
|
* Does NOT modify word/styles.xml — only in-document run formatting.
|
|
7
11
|
*/
|
|
8
12
|
import type { SetColorForStyleOp, OpResult } from '../types.js';
|
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* For every paragraph whose w:pPr/w:pStyle/@w:val === style,
|
|
5
5
|
* set run-level colour on every w:r in that paragraph.
|
|
6
|
+
*
|
|
7
|
+
* This now includes paragraphs inside tables and other containers,
|
|
8
|
+
* not just direct w:p children of w:body.
|
|
9
|
+
*
|
|
6
10
|
* Does NOT modify word/styles.xml — only in-document run formatting.
|
|
7
11
|
*/
|
|
8
|
-
import {
|
|
12
|
+
import { getParagraphStyle, ensureRunColor } from '../dom.js';
|
|
9
13
|
export function applySetColorForStyle(body, op) {
|
|
10
|
-
|
|
14
|
+
// Traverse **all** paragraphs in the body.
|
|
15
|
+
const paragraphs = body.getElementsByTagName('w:p');
|
|
11
16
|
let matched = 0;
|
|
12
|
-
for (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (getParagraphStyle(child) !== op.style)
|
|
17
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
18
|
+
const p = paragraphs.item(i);
|
|
19
|
+
if (getParagraphStyle(p) !== op.style)
|
|
16
20
|
continue;
|
|
17
|
-
const runs =
|
|
21
|
+
const runs = p.getElementsByTagName('w:r');
|
|
18
22
|
for (let i = 0; i < runs.length; i++) {
|
|
19
23
|
ensureRunColor(runs.item(i), op.color);
|
|
20
24
|
}
|
|
@@ -5,24 +5,14 @@
|
|
|
5
5
|
* Targets by: tableIndex (0-based among w:tbl in body), row, col.
|
|
6
6
|
* Applies minimal text replacement inside the cell's first paragraph.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { getAllBodyTables, nodeListToArray, setCellTextPreservingStyles } from '../dom.js';
|
|
9
9
|
export function applyTableSetCellText(body, op) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let table = null;
|
|
14
|
-
for (const child of children) {
|
|
15
|
-
if (child.nodeName === 'w:tbl') {
|
|
16
|
-
if (tableCount === op.tableIndex) {
|
|
17
|
-
table = child;
|
|
18
|
-
break;
|
|
19
|
-
}
|
|
20
|
-
tableCount++;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (!table) {
|
|
10
|
+
// Find the n‑th logical table in the body, including tables inside SDTs.
|
|
11
|
+
const tables = getAllBodyTables(body);
|
|
12
|
+
if (op.tableIndex < 0 || op.tableIndex >= tables.length) {
|
|
24
13
|
return { op, status: 'skipped', matched: 0, reason: 'table_not_found' };
|
|
25
14
|
}
|
|
15
|
+
const table = tables[op.tableIndex];
|
|
26
16
|
// Find the n-th w:tr
|
|
27
17
|
const rows = [];
|
|
28
18
|
for (const child of nodeListToArray(table.childNodes)) {
|
|
@@ -44,29 +34,7 @@ export function applyTableSetCellText(body, op) {
|
|
|
44
34
|
return { op, status: 'skipped', matched: 0, reason: 'col_out_of_range' };
|
|
45
35
|
}
|
|
46
36
|
const cell = cells[op.col];
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const p = child;
|
|
51
|
-
const tNodes = p.getElementsByTagName('w:t');
|
|
52
|
-
if (tNodes.length > 0) {
|
|
53
|
-
// Existing runs — use minimal replacement
|
|
54
|
-
setParagraphTextMinimal(p, op.text);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Empty cell — create a run
|
|
58
|
-
const doc = cell.ownerDocument;
|
|
59
|
-
if (!doc)
|
|
60
|
-
return { op, status: 'skipped', matched: 0, reason: 'no_owner_document' };
|
|
61
|
-
const r = doc.createElement('w:r');
|
|
62
|
-
const t = doc.createElement('w:t');
|
|
63
|
-
t.setAttribute('xml:space', 'preserve');
|
|
64
|
-
t.textContent = op.text;
|
|
65
|
-
r.appendChild(t);
|
|
66
|
-
p.appendChild(r);
|
|
67
|
-
}
|
|
68
|
-
return { op, status: 'applied', matched: 1 };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return { op, status: 'skipped', matched: 0, reason: 'no_paragraph_in_cell' };
|
|
37
|
+
// Replace cell text while preserving ALL styles (colors, bold, italic, etc.)
|
|
38
|
+
setCellTextPreservingStyles(cell, op.text);
|
|
39
|
+
return { op, status: 'applied', matched: 1 };
|
|
72
40
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { DocxMetadata, DocxParagraph, ReadDocxResult } from './types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Return a token-efficient outline of a DOCX file.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Extracts paragraphs, tables (with full cell content), and images (references only, not binary).
|
|
9
|
+
* Every element gets a bodyChildIndex (among ALL w:body children).
|
|
10
10
|
*/
|
|
11
11
|
export declare function readDocxOutline(filePath: string): Promise<ReadDocxResult>;
|
|
12
12
|
/** Extract plain text from DOCX. */
|