@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/utils/files/docx.js
CHANGED
|
@@ -1,135 +1,669 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOCX File Handler
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Approach: expose DOCX as filtered/raw XML through existing read_file + edit_block.
|
|
5
|
+
*
|
|
6
|
+
* READ (default): Returns a text-bearing outline — skips shapes, drawings, SVG noise.
|
|
7
|
+
* Shows paragraphs with text, tables with cell content, style info, and image refs.
|
|
8
|
+
* Each element shows its raw XML tag context so Claude can target it for editing.
|
|
9
|
+
*
|
|
10
|
+
* READ (with offset/length): Returns raw pretty-printed XML with line pagination,
|
|
11
|
+
* so Claude can drill into specific sections when the outline isn't enough.
|
|
12
|
+
*
|
|
13
|
+
* EDIT (old_string/new_string): Find/replace on the pretty-printed XML, then
|
|
14
|
+
* compact and repack into valid DOCX. Works exactly like text file editing.
|
|
15
|
+
*
|
|
16
|
+
* Round-trip: DOCX → unzip → pretty-print → [outline or raw] → edit → compact → repack
|
|
5
17
|
*/
|
|
6
18
|
import fs from 'fs/promises';
|
|
7
|
-
import
|
|
19
|
+
import PizZip from 'pizzip';
|
|
20
|
+
// ════════════════════════════════════════════════════════════════
|
|
21
|
+
// XML Pretty-Print / Compact
|
|
22
|
+
// ════════════════════════════════════════════════════════════════
|
|
8
23
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
24
|
+
* Pretty-print XML: split tags onto separate lines with indentation.
|
|
25
|
+
* Preserves text node content exactly. compact→pretty→compact is lossless.
|
|
11
26
|
*/
|
|
27
|
+
function prettyPrintXml(xml) {
|
|
28
|
+
const parts = xml.split(/(?<=>)(?=<)/);
|
|
29
|
+
const lines = [];
|
|
30
|
+
let depth = 0;
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
const trimmed = part.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
continue;
|
|
35
|
+
const isClosing = trimmed.startsWith('</');
|
|
36
|
+
const isSelfClosing = trimmed.endsWith('/>');
|
|
37
|
+
const isProcessingInstruction = trimmed.startsWith('<?');
|
|
38
|
+
const isInline = !isClosing && !isSelfClosing && trimmed.includes('</');
|
|
39
|
+
if (isClosing)
|
|
40
|
+
depth = Math.max(0, depth - 1);
|
|
41
|
+
lines.push(' '.repeat(depth) + trimmed);
|
|
42
|
+
if (!isClosing && !isSelfClosing && !isInline && !isProcessingInstruction)
|
|
43
|
+
depth++;
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compact pretty-printed XML back — strip leading indentation, join lines.
|
|
49
|
+
* Does NOT touch whitespace inside <w:t> text nodes.
|
|
50
|
+
*/
|
|
51
|
+
function compactXml(prettyXml) {
|
|
52
|
+
return prettyXml.split('\n').map(l => l.trimStart()).join('');
|
|
53
|
+
}
|
|
54
|
+
function loadDocxZip(buf) {
|
|
55
|
+
const zip = new PizZip(buf);
|
|
56
|
+
const docFile = zip.file('word/document.xml');
|
|
57
|
+
if (!docFile)
|
|
58
|
+
throw new Error('Invalid DOCX: missing word/document.xml');
|
|
59
|
+
const xmlParts = new Map();
|
|
60
|
+
// Collect all XML parts for potential editing
|
|
61
|
+
const zipFiles = zip.files;
|
|
62
|
+
for (const relativePath of Object.keys(zipFiles)) {
|
|
63
|
+
if (relativePath.endsWith('.xml') || relativePath.endsWith('.rels')) {
|
|
64
|
+
try {
|
|
65
|
+
xmlParts.set(relativePath, zipFiles[relativePath].asText());
|
|
66
|
+
}
|
|
67
|
+
catch { /* skip binary entries */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
zip,
|
|
72
|
+
documentXml: docFile.asText(),
|
|
73
|
+
xmlParts,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract a text-bearing outline from document.xml.
|
|
78
|
+
*
|
|
79
|
+
* Walks direct children of <w:body> and for each:
|
|
80
|
+
* - w:p (paragraph): extracts text from <w:t> elements, shows style
|
|
81
|
+
* - w:tbl (table): extracts cell text for each row
|
|
82
|
+
* - mc:AlternateContent / shapes / drawings: shows size, skips content
|
|
83
|
+
* - w:sdt: looks inside for text/tables
|
|
84
|
+
*
|
|
85
|
+
* Returns a human-readable outline with enough context for editing.
|
|
86
|
+
*/
|
|
87
|
+
function extractOutline(xml) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
// Parse body children using regex — faster and more reliable than DOM for outline
|
|
90
|
+
// Find <w:body>...</w:body>
|
|
91
|
+
const bodyMatch = xml.match(/<w:body[^>]*>([\s\S]*)<\/w:body>/);
|
|
92
|
+
if (!bodyMatch)
|
|
93
|
+
return '[No w:body found in document.xml]';
|
|
94
|
+
const bodyContent = bodyMatch[1];
|
|
95
|
+
// Split into top-level children of w:body
|
|
96
|
+
// We need to find top-level elements, respecting nesting
|
|
97
|
+
const children = splitTopLevelElements(bodyContent);
|
|
98
|
+
let paragraphCount = 0;
|
|
99
|
+
let tableCount = 0;
|
|
100
|
+
let imageCount = 0;
|
|
101
|
+
for (let i = 0; i < children.length; i++) {
|
|
102
|
+
const child = children[i];
|
|
103
|
+
const tagMatch = child.match(/^<(\S+?)[\s>\/]/);
|
|
104
|
+
if (!tagMatch)
|
|
105
|
+
continue;
|
|
106
|
+
const tag = tagMatch[1];
|
|
107
|
+
if (tag === 'w:p') {
|
|
108
|
+
const text = extractAllText(child);
|
|
109
|
+
const textFragments = extractTextFragments(child);
|
|
110
|
+
const style = extractParagraphStyle(child);
|
|
111
|
+
const hasDrawing = child.includes('<w:drawing') || child.includes('<mc:AlternateContent');
|
|
112
|
+
let line = `[${i}] w:p`;
|
|
113
|
+
if (style)
|
|
114
|
+
line += ` style="${style}"`;
|
|
115
|
+
if (text && hasDrawing) {
|
|
116
|
+
line += ` (+ drawing/image)`;
|
|
117
|
+
imageCount++;
|
|
118
|
+
}
|
|
119
|
+
else if (hasDrawing) {
|
|
120
|
+
line += ` [drawing/image, ${(child.length / 1024).toFixed(1)}KB]`;
|
|
121
|
+
imageCount++;
|
|
122
|
+
if (!text) {
|
|
123
|
+
lines.push(line);
|
|
124
|
+
paragraphCount++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (textFragments.length > 0) {
|
|
129
|
+
const joined = textFragments.join('');
|
|
130
|
+
if (joined.length > 500) {
|
|
131
|
+
line += `\n ${textFragments.slice(0, 8).join('')}...`;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
line += `\n ${joined}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (!hasDrawing) {
|
|
138
|
+
line += ' (empty)';
|
|
139
|
+
}
|
|
140
|
+
lines.push(line);
|
|
141
|
+
paragraphCount++;
|
|
142
|
+
}
|
|
143
|
+
else if (tag === 'w:tbl') {
|
|
144
|
+
const rows = extractTableRows(child);
|
|
145
|
+
let line = `[${i}] w:tbl (${rows.length} rows)`;
|
|
146
|
+
const style = extractTableStyle(child);
|
|
147
|
+
if (style)
|
|
148
|
+
line += ` style="${style}"`;
|
|
149
|
+
// Show all rows (tables usually contain the real content)
|
|
150
|
+
for (let r = 0; r < rows.length; r++) {
|
|
151
|
+
const cells = rows[r].map(c => {
|
|
152
|
+
if (!c || c.trim() === '')
|
|
153
|
+
return '';
|
|
154
|
+
return c.length > 60 ? c.substring(0, 60) + '…' : c;
|
|
155
|
+
});
|
|
156
|
+
line += `\n row${r}: [${cells.join(' | ')}]`;
|
|
157
|
+
}
|
|
158
|
+
lines.push(line);
|
|
159
|
+
tableCount++;
|
|
160
|
+
}
|
|
161
|
+
else if (tag === 'w:sdt') {
|
|
162
|
+
// Structured document tag — look inside for content
|
|
163
|
+
const sdtFragments = extractTextFragments(child);
|
|
164
|
+
const innerTables = (child.match(/<w:tbl[\s>]/g) || []).length;
|
|
165
|
+
let line = `[${i}] w:sdt`;
|
|
166
|
+
if (innerTables > 0) {
|
|
167
|
+
line += ` (contains ${innerTables} table${innerTables > 1 ? 's' : ''})`;
|
|
168
|
+
tableCount += innerTables;
|
|
169
|
+
}
|
|
170
|
+
if (sdtFragments.length > 0) {
|
|
171
|
+
const joined = sdtFragments.join('');
|
|
172
|
+
if (joined.length > 150) {
|
|
173
|
+
line += `\n ${sdtFragments.slice(0, 3).join('')}...`;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
line += `\n ${joined}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
lines.push(line);
|
|
180
|
+
}
|
|
181
|
+
else if (tag === 'w:sectPr') {
|
|
182
|
+
lines.push(`[${i}] w:sectPr (section properties)`);
|
|
183
|
+
}
|
|
184
|
+
else if (tag === 'mc:AlternateContent') {
|
|
185
|
+
lines.push(`[${i}] mc:AlternateContent [drawing, ${(child.length / 1024).toFixed(1)}KB — skipped]`);
|
|
186
|
+
imageCount++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(`[${i}] ${tag} (${(child.length / 1024).toFixed(1)}KB)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Summary header
|
|
193
|
+
const header = `DOCX Outline: ${children.length} body children, ${paragraphCount} paragraphs, ${tableCount} tables, ${imageCount} images\n` +
|
|
194
|
+
`Edit with: edit_block(file, old_string="<w:t>old text</w:t>", new_string="<w:t>new text</w:t>")\n` +
|
|
195
|
+
`Raw XML: use read_file with offset=1 to see pretty-printed XML for advanced edits.\n` +
|
|
196
|
+
'─'.repeat(70);
|
|
197
|
+
return header + '\n' + lines.join('\n');
|
|
198
|
+
}
|
|
199
|
+
// ════════════════════════════════════════════════════════════════
|
|
200
|
+
// XML text extraction helpers (regex-based, no DOM needed)
|
|
201
|
+
// ════════════════════════════════════════════════════════════════
|
|
202
|
+
/** Extract all <w:t>...</w:t> text content from an XML fragment */
|
|
203
|
+
function extractAllText(xml) {
|
|
204
|
+
const texts = [];
|
|
205
|
+
// Match <w:t> or <w:t xml:space="preserve"> but NOT <w:tbl>, <w:tc>, <w:tr>, etc.
|
|
206
|
+
const re = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
|
|
207
|
+
let m;
|
|
208
|
+
while ((m = re.exec(xml)) !== null) {
|
|
209
|
+
if (m[1])
|
|
210
|
+
texts.push(m[1]);
|
|
211
|
+
}
|
|
212
|
+
return texts.join('').trim();
|
|
213
|
+
}
|
|
214
|
+
/** Extract <w:t>...</w:t> elements as XML fragments for use in edit_block */
|
|
215
|
+
function extractTextFragments(xml) {
|
|
216
|
+
const fragments = [];
|
|
217
|
+
const re = /<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>/g;
|
|
218
|
+
let m;
|
|
219
|
+
while ((m = re.exec(xml)) !== null) {
|
|
220
|
+
if (m[1] && m[1].trim())
|
|
221
|
+
fragments.push(m[0]);
|
|
222
|
+
}
|
|
223
|
+
return fragments;
|
|
224
|
+
}
|
|
225
|
+
/** Extract paragraph style id from w:pPr/w:pStyle */
|
|
226
|
+
function extractParagraphStyle(xml) {
|
|
227
|
+
const m = xml.match(/<w:pStyle\s+w:val="([^"]+)"/);
|
|
228
|
+
return m ? m[1] : null;
|
|
229
|
+
}
|
|
230
|
+
/** Extract table style from w:tblPr/w:tblStyle */
|
|
231
|
+
function extractTableStyle(xml) {
|
|
232
|
+
const m = xml.match(/<w:tblStyle\s+w:val="([^"]+)"/);
|
|
233
|
+
return m ? m[1] : null;
|
|
234
|
+
}
|
|
235
|
+
/** Extract table rows as arrays of cell text */
|
|
236
|
+
function extractTableRows(tableXml) {
|
|
237
|
+
const rows = [];
|
|
238
|
+
// Find each <w:tr>...</w:tr> using nesting-aware extraction
|
|
239
|
+
const rowElements = extractNestedElements(tableXml, 'w:tr');
|
|
240
|
+
for (const rowXml of rowElements) {
|
|
241
|
+
const cells = [];
|
|
242
|
+
const cellElements = extractNestedElements(rowXml, 'w:tc');
|
|
243
|
+
for (const cellXml of cellElements) {
|
|
244
|
+
cells.push(extractAllText(cellXml));
|
|
245
|
+
}
|
|
246
|
+
if (cells.length > 0)
|
|
247
|
+
rows.push(cells);
|
|
248
|
+
}
|
|
249
|
+
return rows;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Extract all occurrences of a named element from XML, respecting nesting.
|
|
253
|
+
* Returns array of full element strings including open/close tags.
|
|
254
|
+
*/
|
|
255
|
+
function extractNestedElements(xml, tagName) {
|
|
256
|
+
const results = [];
|
|
257
|
+
const openTag = `<${tagName}`;
|
|
258
|
+
const closeTag = `</${tagName}>`;
|
|
259
|
+
let searchFrom = 0;
|
|
260
|
+
while (searchFrom < xml.length) {
|
|
261
|
+
const openPos = xml.indexOf(openTag, searchFrom);
|
|
262
|
+
if (openPos === -1)
|
|
263
|
+
break;
|
|
264
|
+
// Check it's actually a tag start (followed by space, >, or /)
|
|
265
|
+
const charAfter = xml[openPos + openTag.length];
|
|
266
|
+
if (charAfter !== ' ' && charAfter !== '>' && charAfter !== '/') {
|
|
267
|
+
searchFrom = openPos + 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Check for self-closing
|
|
271
|
+
const nextClose = xml.indexOf('>', openPos);
|
|
272
|
+
if (nextClose !== -1 && xml[nextClose - 1] === '/') {
|
|
273
|
+
results.push(xml.substring(openPos, nextClose + 1));
|
|
274
|
+
searchFrom = nextClose + 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// Find matching close tag respecting nesting
|
|
278
|
+
let depth = 1;
|
|
279
|
+
let pos = nextClose + 1;
|
|
280
|
+
while (depth > 0 && pos < xml.length) {
|
|
281
|
+
const nextOpen = xml.indexOf(openTag, pos);
|
|
282
|
+
const nextCloseTag = xml.indexOf(closeTag, pos);
|
|
283
|
+
if (nextCloseTag === -1)
|
|
284
|
+
break; // malformed XML
|
|
285
|
+
if (nextOpen !== -1 && nextOpen < nextCloseTag) {
|
|
286
|
+
// Check it's actually an open tag
|
|
287
|
+
const ca = xml[nextOpen + openTag.length];
|
|
288
|
+
if (ca === ' ' || ca === '>' || ca === '/') {
|
|
289
|
+
depth++;
|
|
290
|
+
}
|
|
291
|
+
pos = nextOpen + openTag.length;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
depth--;
|
|
295
|
+
if (depth === 0) {
|
|
296
|
+
results.push(xml.substring(openPos, nextCloseTag + closeTag.length));
|
|
297
|
+
}
|
|
298
|
+
pos = nextCloseTag + closeTag.length;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
searchFrom = pos;
|
|
302
|
+
}
|
|
303
|
+
return results;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Split XML into top-level elements respecting nesting depth.
|
|
307
|
+
* E.g. for body content, returns each direct child element as a string.
|
|
308
|
+
*/
|
|
309
|
+
function splitTopLevelElements(xml) {
|
|
310
|
+
const elements = [];
|
|
311
|
+
let depth = 0;
|
|
312
|
+
let currentStart = -1;
|
|
313
|
+
// Simple state machine: track < > and nesting
|
|
314
|
+
let i = 0;
|
|
315
|
+
while (i < xml.length) {
|
|
316
|
+
if (xml[i] === '<') {
|
|
317
|
+
// Check what kind of tag
|
|
318
|
+
if (xml[i + 1] === '/') {
|
|
319
|
+
// Closing tag
|
|
320
|
+
depth--;
|
|
321
|
+
if (depth === 0) {
|
|
322
|
+
// Find end of this closing tag
|
|
323
|
+
const closeEnd = xml.indexOf('>', i);
|
|
324
|
+
if (closeEnd === -1)
|
|
325
|
+
break; // malformed XML — bail out
|
|
326
|
+
if (currentStart !== -1) {
|
|
327
|
+
elements.push(xml.substring(currentStart, closeEnd + 1).trim());
|
|
328
|
+
currentStart = -1;
|
|
329
|
+
}
|
|
330
|
+
i = closeEnd + 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (xml[i + 1] === '?' || xml[i + 1] === '!') {
|
|
335
|
+
// Processing instruction or comment — skip
|
|
336
|
+
const end = xml.indexOf('>', i);
|
|
337
|
+
if (end === -1)
|
|
338
|
+
break; // malformed XML — bail out
|
|
339
|
+
i = end + 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Opening tag
|
|
344
|
+
if (depth === 0)
|
|
345
|
+
currentStart = i;
|
|
346
|
+
// Check for self-closing
|
|
347
|
+
const tagEnd = xml.indexOf('>', i);
|
|
348
|
+
if (tagEnd === -1)
|
|
349
|
+
break; // malformed XML — bail out
|
|
350
|
+
if (xml[tagEnd - 1] === '/') {
|
|
351
|
+
// Self-closing
|
|
352
|
+
if (depth === 0) {
|
|
353
|
+
elements.push(xml.substring(currentStart, tagEnd + 1).trim());
|
|
354
|
+
currentStart = -1;
|
|
355
|
+
i = tagEnd + 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
depth++;
|
|
361
|
+
}
|
|
362
|
+
i = tagEnd + 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
return elements.filter(e => e.length > 0);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Extract outline info for headers and footers from the DOCX zip.
|
|
372
|
+
*/
|
|
373
|
+
function extractHeaderFooterOutline(zip) {
|
|
374
|
+
const parts = [];
|
|
375
|
+
const zipFiles = zip.files;
|
|
376
|
+
for (const relativePath of Object.keys(zipFiles)) {
|
|
377
|
+
if ((relativePath.startsWith('word/header') || relativePath.startsWith('word/footer'))
|
|
378
|
+
&& relativePath.endsWith('.xml')) {
|
|
379
|
+
try {
|
|
380
|
+
const xml = zipFiles[relativePath].asText();
|
|
381
|
+
const text = extractAllText(xml);
|
|
382
|
+
const name = relativePath.replace('word/', '');
|
|
383
|
+
if (text) {
|
|
384
|
+
parts.push(`${name}: "${text.length > 100 ? text.substring(0, 100) + '...' : text}"`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
parts.push(`${name}: (no text content)`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch { /* skip */ }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return parts.length > 0 ? '\n\nHeaders/Footers:\n' + parts.join('\n') : '';
|
|
394
|
+
}
|
|
395
|
+
// ════════════════════════════════════════════════════════════════
|
|
396
|
+
// DOCX creation helpers
|
|
397
|
+
// ════════════════════════════════════════════════════════════════
|
|
398
|
+
function escapeXml(text) {
|
|
399
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
400
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
401
|
+
}
|
|
402
|
+
function createMinimalDocxZip(documentXml) {
|
|
403
|
+
const zip = new PizZip();
|
|
404
|
+
zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
405
|
+
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
406
|
+
`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
|
|
407
|
+
`<Default Extension="xml" ContentType="application/xml"/>` +
|
|
408
|
+
`<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>` +
|
|
409
|
+
`<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>` +
|
|
410
|
+
`</Types>`);
|
|
411
|
+
zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
412
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
413
|
+
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>` +
|
|
414
|
+
`</Relationships>`);
|
|
415
|
+
zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
416
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
417
|
+
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>` +
|
|
418
|
+
`</Relationships>`);
|
|
419
|
+
zip.file('word/styles.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
420
|
+
`<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
421
|
+
`<w:docDefaults><w:rPrDefault><w:rPr>` +
|
|
422
|
+
`<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>` +
|
|
423
|
+
`<w:sz w:val="22"/><w:szCs w:val="22"/>` +
|
|
424
|
+
`</w:rPr></w:rPrDefault></w:docDefaults>` +
|
|
425
|
+
`<w:style w:type="paragraph" w:styleId="Normal" w:default="1"><w:name w:val="Normal"/></w:style>` +
|
|
426
|
+
`<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:pPr><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style>` +
|
|
427
|
+
`<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:pPr><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style>` +
|
|
428
|
+
`<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:pPr><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:b/><w:sz w:val="24"/></w:rPr></w:style>` +
|
|
429
|
+
`</w:styles>`);
|
|
430
|
+
zip.file('word/document.xml', documentXml);
|
|
431
|
+
return zip;
|
|
432
|
+
}
|
|
433
|
+
// ════════════════════════════════════════════════════════════════
|
|
434
|
+
// Count occurrences helper
|
|
435
|
+
// ════════════════════════════════════════════════════════════════
|
|
436
|
+
function countOccurrences(haystack, needle) {
|
|
437
|
+
let count = 0;
|
|
438
|
+
let pos = haystack.indexOf(needle);
|
|
439
|
+
while (pos !== -1) {
|
|
440
|
+
count++;
|
|
441
|
+
pos = haystack.indexOf(needle, pos + 1);
|
|
442
|
+
}
|
|
443
|
+
return count;
|
|
444
|
+
}
|
|
445
|
+
// ════════════════════════════════════════════════════════════════
|
|
446
|
+
// DocxFileHandler — implements FileHandler
|
|
447
|
+
// ════════════════════════════════════════════════════════════════
|
|
12
448
|
export class DocxFileHandler {
|
|
13
449
|
constructor() {
|
|
14
450
|
this.extensions = ['.docx'];
|
|
15
451
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Check if this handler can handle the given file
|
|
18
|
-
*/
|
|
19
452
|
canHandle(path) {
|
|
20
|
-
|
|
21
|
-
return this.extensions.some(e => ext.endsWith(e));
|
|
453
|
+
return this.extensions.some(e => path.toLowerCase().endsWith(e));
|
|
22
454
|
}
|
|
23
455
|
/**
|
|
24
|
-
* Read DOCX content
|
|
456
|
+
* Read DOCX content.
|
|
457
|
+
*
|
|
458
|
+
* Default (offset=0, no explicit length or default length): returns outline
|
|
459
|
+
* With offset/length: returns raw pretty-printed XML with line pagination
|
|
25
460
|
*/
|
|
26
461
|
async read(path, options) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
462
|
+
const buf = await fs.readFile(path);
|
|
463
|
+
const { zip, documentXml } = loadDocxZip(buf);
|
|
464
|
+
const pretty = prettyPrintXml(documentXml);
|
|
465
|
+
const allLines = pretty.split('\n');
|
|
466
|
+
const totalLines = allLines.length;
|
|
467
|
+
const offset = options?.offset ?? 0;
|
|
468
|
+
const length = options?.length;
|
|
469
|
+
// If user explicitly requests non-zero offset, give raw XML with pagination
|
|
470
|
+
const wantsRaw = offset !== 0;
|
|
471
|
+
if (wantsRaw) {
|
|
472
|
+
let startLine;
|
|
473
|
+
let sliceLength;
|
|
474
|
+
if (offset < 0) {
|
|
475
|
+
startLine = Math.max(0, totalLines + offset);
|
|
476
|
+
sliceLength = totalLines - startLine;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
startLine = offset;
|
|
480
|
+
sliceLength = length ?? totalLines;
|
|
481
|
+
}
|
|
482
|
+
const sliced = allLines.slice(startLine, startLine + sliceLength);
|
|
483
|
+
const remaining = totalLines - (startLine + sliced.length);
|
|
484
|
+
const status = `[DOCX XML: lines ${startLine}-${startLine + sliced.length - 1} of ${totalLines} (${remaining} remaining)]`;
|
|
34
485
|
return {
|
|
35
|
-
content:
|
|
486
|
+
content: status + '\n' + sliced.join('\n'),
|
|
36
487
|
mimeType: 'application/xml',
|
|
37
|
-
metadata: {
|
|
38
|
-
isDocx: true,
|
|
39
|
-
author: result.metadata.author,
|
|
40
|
-
title: result.metadata.title,
|
|
41
|
-
subject: result.metadata.subject,
|
|
42
|
-
creator: result.metadata.creator,
|
|
43
|
-
paragraphCount: result.metadata.paragraphCount,
|
|
44
|
-
wordCount: result.metadata.wordCount,
|
|
45
|
-
paragraphs: result.paragraphs,
|
|
46
|
-
// Include extracted text for reference
|
|
47
|
-
extractedText: result.text
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
53
|
-
return {
|
|
54
|
-
content: `Error reading DOCX: ${errorMessage}`,
|
|
55
|
-
mimeType: 'text/plain',
|
|
56
|
-
metadata: {
|
|
57
|
-
error: true,
|
|
58
|
-
errorMessage
|
|
59
|
-
}
|
|
488
|
+
metadata: { isDocx: true, lineCount: totalLines },
|
|
60
489
|
};
|
|
61
490
|
}
|
|
491
|
+
// Default: return outline
|
|
492
|
+
const outline = extractOutline(documentXml);
|
|
493
|
+
const headerFooterInfo = extractHeaderFooterOutline(zip);
|
|
494
|
+
const rawSizeKB = (documentXml.length / 1024).toFixed(1);
|
|
495
|
+
return {
|
|
496
|
+
content: outline + headerFooterInfo +
|
|
497
|
+
`\n\nRaw XML: ${totalLines} lines, ${rawSizeKB}KB.` +
|
|
498
|
+
`\nFor bulk changes (translation, mass find/replace): use start_process with a Python script using zipfile to edit <w:t> elements.`,
|
|
499
|
+
mimeType: 'text/plain',
|
|
500
|
+
metadata: { isDocx: true, lineCount: totalLines },
|
|
501
|
+
};
|
|
62
502
|
}
|
|
63
503
|
/**
|
|
64
|
-
* Write DOCX
|
|
65
|
-
*
|
|
504
|
+
* Write/create a DOCX file.
|
|
505
|
+
* Content is plain text — each line becomes a paragraph.
|
|
506
|
+
* Lines starting with # become headings (# = Heading1, ## = Heading2, etc.)
|
|
66
507
|
*/
|
|
67
508
|
async write(path, content, mode) {
|
|
68
|
-
|
|
69
|
-
'
|
|
509
|
+
if (mode === 'append') {
|
|
510
|
+
throw new Error('DOCX append not supported. Use edit_block to modify existing DOCX files.');
|
|
511
|
+
}
|
|
512
|
+
const text = typeof content === 'string' ? content : String(content);
|
|
513
|
+
const lines = text.split('\n');
|
|
514
|
+
// Build paragraph XML from lines
|
|
515
|
+
const paragraphs = [];
|
|
516
|
+
for (const line of lines) {
|
|
517
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
518
|
+
if (headingMatch) {
|
|
519
|
+
const level = headingMatch[1].length;
|
|
520
|
+
const headingText = headingMatch[2];
|
|
521
|
+
paragraphs.push(`<w:p><w:pPr><w:pStyle w:val="Heading${level}"/></w:pPr>` +
|
|
522
|
+
`<w:r><w:t>${escapeXml(headingText)}</w:t></w:r></w:p>`);
|
|
523
|
+
}
|
|
524
|
+
else if (line.trim() === '') {
|
|
525
|
+
paragraphs.push(`<w:p/>`);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
paragraphs.push(`<w:p><w:r><w:t xml:space="preserve">${escapeXml(line)}</w:t></w:r></w:p>`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const docXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
532
|
+
`<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
533
|
+
`<w:body>${paragraphs.join('')}` +
|
|
534
|
+
`<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr>` +
|
|
535
|
+
`</w:body></w:document>`;
|
|
536
|
+
const zip = createMinimalDocxZip(docXml);
|
|
537
|
+
const buf = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } });
|
|
538
|
+
await fs.writeFile(path, buf);
|
|
70
539
|
}
|
|
71
540
|
/**
|
|
72
|
-
* Edit DOCX
|
|
541
|
+
* Edit DOCX via find/replace on pretty-printed XML.
|
|
542
|
+
*
|
|
543
|
+
* Works on the same representation that read() returns when using offset/length,
|
|
544
|
+
* so XML fragments copied from read output work as search strings.
|
|
545
|
+
* After editing, XML is compacted and repacked into the DOCX.
|
|
73
546
|
*/
|
|
74
|
-
async editRange(path,
|
|
547
|
+
async editRange(path, _range, content, options) {
|
|
75
548
|
try {
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
549
|
+
let oldStr;
|
|
550
|
+
let newStr;
|
|
551
|
+
let expectedReplacements = 1;
|
|
552
|
+
if (typeof content === 'object' && content !== null) {
|
|
553
|
+
oldStr = content.oldStr || content.old_string || content.search || '';
|
|
554
|
+
newStr = content.newStr || content.new_string || content.replace || '';
|
|
555
|
+
expectedReplacements = content.expectedReplacements || content.expected_replacements || 1;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
return {
|
|
559
|
+
success: false, editsApplied: 0,
|
|
560
|
+
errors: [{ location: 'docx', error: 'DOCX editing requires old_string and new_string' }],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (!oldStr) {
|
|
564
|
+
return {
|
|
565
|
+
success: false, editsApplied: 0,
|
|
566
|
+
errors: [{ location: 'docx', error: 'old_string cannot be empty' }],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
// Load and pretty-print
|
|
570
|
+
const buf = await fs.readFile(path);
|
|
571
|
+
const zip = new PizZip(buf);
|
|
572
|
+
const docFile = zip.file('word/document.xml');
|
|
573
|
+
if (!docFile)
|
|
574
|
+
throw new Error('Invalid DOCX: missing word/document.xml');
|
|
575
|
+
const rawXml = docFile.asText();
|
|
576
|
+
const pretty = prettyPrintXml(rawXml);
|
|
577
|
+
// Also check headers/footers for the search string
|
|
578
|
+
let targetPretty = pretty;
|
|
579
|
+
let targetFile = 'word/document.xml';
|
|
580
|
+
let matchCount = countOccurrences(pretty, oldStr);
|
|
581
|
+
// If not found in document.xml, search through headers/footers
|
|
582
|
+
if (matchCount === 0) {
|
|
583
|
+
const xmlFiles = ['word/header1.xml', 'word/header2.xml', 'word/header3.xml',
|
|
584
|
+
'word/footer1.xml', 'word/footer2.xml', 'word/footer3.xml'];
|
|
585
|
+
for (const xmlPath of xmlFiles) {
|
|
586
|
+
const f = zip.file(xmlPath);
|
|
587
|
+
if (!f)
|
|
588
|
+
continue;
|
|
589
|
+
const partPretty = prettyPrintXml(f.asText());
|
|
590
|
+
const c = countOccurrences(partPretty, oldStr);
|
|
591
|
+
if (c > 0) {
|
|
592
|
+
targetPretty = partPretty;
|
|
593
|
+
targetFile = xmlPath;
|
|
594
|
+
matchCount = c;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
93
597
|
}
|
|
94
598
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
599
|
+
if (matchCount === 0) {
|
|
600
|
+
return {
|
|
601
|
+
success: false, editsApplied: 0,
|
|
602
|
+
errors: [{ location: targetFile, error: `Search string not found in DOCX` }],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (matchCount !== expectedReplacements) {
|
|
606
|
+
return {
|
|
607
|
+
success: false, editsApplied: 0,
|
|
608
|
+
errors: [{
|
|
609
|
+
location: targetFile,
|
|
610
|
+
error: `Expected ${expectedReplacements} occurrence(s) but found ${matchCount}. ` +
|
|
611
|
+
`Set expected_replacements to ${matchCount} to replace all, ` +
|
|
612
|
+
`or add more context to make the search unique.`,
|
|
613
|
+
}],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
// Apply replacement
|
|
617
|
+
let edited = targetPretty;
|
|
618
|
+
if (expectedReplacements === 1) {
|
|
619
|
+
const idx = edited.indexOf(oldStr);
|
|
620
|
+
edited = edited.substring(0, idx) + newStr + edited.substring(idx + oldStr.length);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
edited = edited.split(oldStr).join(newStr);
|
|
624
|
+
}
|
|
625
|
+
// Compact and repack
|
|
626
|
+
const compacted = compactXml(edited);
|
|
627
|
+
zip.file(targetFile, compacted);
|
|
628
|
+
const outBuf = zip.generate({
|
|
629
|
+
type: 'nodebuffer',
|
|
630
|
+
compression: 'DEFLATE',
|
|
631
|
+
compressionOptions: { level: 6 },
|
|
632
|
+
});
|
|
633
|
+
await fs.writeFile(path, outBuf);
|
|
634
|
+
return { success: true, editsApplied: matchCount };
|
|
101
635
|
}
|
|
102
636
|
catch (error) {
|
|
103
637
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
104
638
|
return {
|
|
105
|
-
success: false,
|
|
106
|
-
|
|
107
|
-
errors: [{ location: range, error: errorMessage }]
|
|
639
|
+
success: false, editsApplied: 0,
|
|
640
|
+
errors: [{ location: 'docx', error: errorMessage }],
|
|
108
641
|
};
|
|
109
642
|
}
|
|
110
643
|
}
|
|
111
644
|
/**
|
|
112
|
-
* Get DOCX file
|
|
645
|
+
* Get DOCX file info
|
|
113
646
|
*/
|
|
114
647
|
async getInfo(path) {
|
|
115
648
|
const stats = await fs.stat(path);
|
|
116
|
-
// Get DOCX metadata
|
|
117
649
|
let metadata = { isDocx: true };
|
|
118
650
|
try {
|
|
119
|
-
const
|
|
651
|
+
const buf = await fs.readFile(path);
|
|
652
|
+
const { documentXml } = loadDocxZip(buf);
|
|
653
|
+
const text = extractAllText(documentXml);
|
|
654
|
+
const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
|
|
655
|
+
const paragraphCount = (documentXml.match(/<w:p[\s>]/g) || []).length;
|
|
656
|
+
const tableCount = (documentXml.match(/<w:tbl[\s>]/g) || []).length;
|
|
657
|
+
const imageCount = (documentXml.match(/<w:drawing[\s>]/g) || []).length;
|
|
120
658
|
metadata = {
|
|
121
659
|
isDocx: true,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
paragraphCount: docxMetadata.paragraphCount,
|
|
127
|
-
wordCount: docxMetadata.wordCount
|
|
660
|
+
paragraphCount,
|
|
661
|
+
tableCount,
|
|
662
|
+
imageCount,
|
|
663
|
+
wordCount,
|
|
128
664
|
};
|
|
129
665
|
}
|
|
130
|
-
catch {
|
|
131
|
-
// If we can't parse, just return basic info
|
|
132
|
-
}
|
|
666
|
+
catch { /* return basic info */ }
|
|
133
667
|
return {
|
|
134
668
|
size: stats.size,
|
|
135
669
|
created: stats.birthtime,
|
|
@@ -138,8 +672,8 @@ export class DocxFileHandler {
|
|
|
138
672
|
isDirectory: false,
|
|
139
673
|
isFile: true,
|
|
140
674
|
permissions: (stats.mode & 0o777).toString(8),
|
|
141
|
-
fileType: '
|
|
142
|
-
metadata
|
|
675
|
+
fileType: 'docx',
|
|
676
|
+
metadata,
|
|
143
677
|
};
|
|
144
678
|
}
|
|
145
679
|
}
|