@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.
Files changed (94) hide show
  1. package/README.md +240 -100
  2. package/dist/command-manager.js +6 -3
  3. package/dist/config-field-definitions.d.ts +41 -0
  4. package/dist/config-field-definitions.js +37 -0
  5. package/dist/config-manager.d.ts +2 -0
  6. package/dist/config-manager.js +22 -2
  7. package/dist/handlers/filesystem-handlers.js +6 -11
  8. package/dist/handlers/macos-control-handlers.d.ts +16 -0
  9. package/dist/handlers/macos-control-handlers.js +81 -0
  10. package/dist/lib.d.ts +10 -0
  11. package/dist/lib.js +10 -0
  12. package/dist/remote-device/remote-channel.d.ts +8 -3
  13. package/dist/remote-device/remote-channel.js +68 -21
  14. package/dist/search-manager.d.ts +13 -0
  15. package/dist/search-manager.js +146 -0
  16. package/dist/server.js +29 -1
  17. package/dist/test-docx.d.ts +1 -0
  18. package/dist/tools/config.d.ts +71 -0
  19. package/dist/tools/config.js +117 -2
  20. package/dist/tools/docx/builders/table.d.ts +2 -0
  21. package/dist/tools/docx/builders/table.js +60 -16
  22. package/dist/tools/docx/dom.d.ts +74 -1
  23. package/dist/tools/docx/dom.js +221 -1
  24. package/dist/tools/docx/index.d.ts +2 -2
  25. package/dist/tools/docx/ops/index.js +3 -0
  26. package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +15 -3
  27. package/dist/tools/docx/ops/replace-paragraph-text-exact.js +25 -10
  28. package/dist/tools/docx/ops/replace-table-cell-text.d.ts +25 -0
  29. package/dist/tools/docx/ops/replace-table-cell-text.js +85 -0
  30. package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +2 -1
  31. package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +9 -8
  32. package/dist/tools/docx/ops/set-color-for-style.d.ts +4 -0
  33. package/dist/tools/docx/ops/set-color-for-style.js +11 -7
  34. package/dist/tools/docx/ops/table-set-cell-text.js +8 -40
  35. package/dist/tools/docx/read.d.ts +2 -2
  36. package/dist/tools/docx/read.js +137 -17
  37. package/dist/tools/docx/types.d.ts +32 -3
  38. package/dist/tools/docx/xml-view-test.d.ts +1 -0
  39. package/dist/tools/docx/xml-view-test.js +63 -0
  40. package/dist/tools/docx/xml-view.d.ts +56 -0
  41. package/dist/tools/docx/xml-view.js +169 -0
  42. package/dist/tools/edit.js +57 -27
  43. package/dist/tools/macos-control/ax-adapter.d.ts +55 -0
  44. package/dist/tools/macos-control/ax-adapter.js +438 -0
  45. package/dist/tools/macos-control/cdp-adapter.d.ts +23 -0
  46. package/dist/tools/macos-control/cdp-adapter.js +402 -0
  47. package/dist/tools/macos-control/orchestrator.d.ts +77 -0
  48. package/dist/tools/macos-control/orchestrator.js +136 -0
  49. package/dist/tools/macos-control/role-aliases.d.ts +5 -0
  50. package/dist/tools/macos-control/role-aliases.js +34 -0
  51. package/dist/tools/macos-control/types.d.ts +129 -0
  52. package/dist/tools/macos-control/types.js +1 -0
  53. package/dist/tools/schemas.d.ts +3 -0
  54. package/dist/tools/schemas.js +2 -1
  55. package/dist/types.d.ts +0 -1
  56. package/dist/ui/config-editor/config-editor-runtime.js +14181 -0
  57. package/dist/ui/config-editor/index.html +13 -0
  58. package/dist/ui/config-editor/src/app.d.ts +43 -0
  59. package/dist/ui/config-editor/src/app.js +840 -0
  60. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  61. package/dist/ui/config-editor/src/array-modal.js +185 -0
  62. package/dist/ui/config-editor/src/main.d.ts +1 -0
  63. package/dist/ui/config-editor/src/main.js +2 -0
  64. package/dist/ui/config-editor/styles.css +586 -0
  65. package/dist/ui/file-preview/preview-runtime.js +13337 -752
  66. package/dist/ui/file-preview/shared/preview-file-types.js +3 -1
  67. package/dist/ui/file-preview/src/app.d.ts +5 -1
  68. package/dist/ui/file-preview/src/app.js +114 -200
  69. package/dist/ui/file-preview/src/components/html-renderer.d.ts +1 -5
  70. package/dist/ui/file-preview/src/components/html-renderer.js +11 -27
  71. package/dist/ui/file-preview/styles.css +117 -83
  72. package/dist/ui/resources.d.ts +7 -0
  73. package/dist/ui/resources.js +16 -2
  74. package/dist/ui/shared/compact-row.d.ts +11 -0
  75. package/dist/ui/shared/compact-row.js +18 -0
  76. package/dist/ui/shared/host-context.d.ts +15 -0
  77. package/dist/ui/shared/host-context.js +51 -0
  78. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  79. package/dist/ui/shared/tool-bridge.js +137 -0
  80. package/dist/ui/shared/tool-shell.d.ts +9 -0
  81. package/dist/ui/shared/tool-shell.js +46 -4
  82. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  83. package/dist/ui/shared/ui-event-tracker.js +27 -0
  84. package/dist/utils/capture.js +173 -11
  85. package/dist/utils/files/base.d.ts +3 -1
  86. package/dist/utils/files/docx.d.ts +28 -15
  87. package/dist/utils/files/docx.js +622 -88
  88. package/dist/utils/files/factory.d.ts +6 -5
  89. package/dist/utils/files/factory.js +18 -6
  90. package/dist/utils/system-info.js +1 -1
  91. package/dist/utils/usageTracker.js +5 -0
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/package.json +8 -3
@@ -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
- * Replace only the first w:t with `to`; clear other w:t nodes.
6
- * Does NOT remove/recreate runs or paragraph properties.
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
- * Replace only the first w:t with `to`; clear other w:t nodes.
6
- * Does NOT remove/recreate runs or paragraph properties.
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 { getBodyChildren, getParagraphText, setParagraphTextMinimal } from '../dom.js';
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
- for (const child of children) {
13
- if (child.nodeName !== 'w:p')
14
- continue;
15
- if (getParagraphText(child).trim() === target) {
16
- setParagraphTextMinimal(child, op.to);
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 { getBodyChildren, getParagraphText, ensureRunColor } from '../dom.js';
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
- for (const child of children) {
12
- if (child.nodeName !== 'w:p')
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
- if (getParagraphText(child).trim() !== target)
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 { getBodyChildren, getParagraphStyle, ensureRunColor } from '../dom.js';
12
+ import { getParagraphStyle, ensureRunColor } from '../dom.js';
9
13
  export function applySetColorForStyle(body, op) {
10
- const children = getBodyChildren(body);
14
+ // Traverse **all** paragraphs in the body.
15
+ const paragraphs = body.getElementsByTagName('w:p');
11
16
  let matched = 0;
12
- for (const child of children) {
13
- if (child.nodeName !== 'w:p')
14
- continue;
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 = child.getElementsByTagName('w:r');
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 { getBodyChildren, nodeListToArray, setParagraphTextMinimal } from '../dom.js';
8
+ import { getAllBodyTables, nodeListToArray, setCellTextPreservingStyles } from '../dom.js';
9
9
  export function applyTableSetCellText(body, op) {
10
- const children = getBodyChildren(body);
11
- // Find the n-th w:tbl
12
- let tableCount = 0;
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
- // Find first w:p inside the cell and apply minimal text replacement
48
- for (const child of nodeListToArray(cell.childNodes)) {
49
- if (child.nodeType === 1 && child.nodeName === 'w:p') {
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
- * Every paragraph gets a bodyChildIndex (among ALL w:body children)
9
- * plus a paragraphIndex (counting only w:p), style id, and text.
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. */