@wonderwhy-er/desktop-commander 0.2.35 → 0.2.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/handlers/filesystem-handlers.js +58 -11
- package/dist/handlers/history-handlers.d.ts +7 -0
- package/dist/handlers/history-handlers.js +33 -1
- package/dist/server.js +30 -4
- package/dist/tools/docx/builders/image.d.ts +14 -0
- package/dist/tools/docx/builders/image.js +84 -0
- package/dist/tools/docx/builders/index.d.ts +9 -3
- package/dist/tools/docx/builders/index.js +9 -3
- package/dist/tools/docx/builders/paragraph.d.ts +12 -0
- package/dist/tools/docx/builders/paragraph.js +29 -0
- package/dist/tools/docx/builders/table.d.ts +8 -0
- package/dist/tools/docx/builders/table.js +94 -0
- package/dist/tools/docx/builders/utils.d.ts +5 -0
- package/dist/tools/docx/builders/utils.js +18 -0
- package/dist/tools/docx/constants.d.ts +28 -32
- package/dist/tools/docx/constants.js +56 -52
- package/dist/tools/docx/create.d.ts +21 -0
- package/dist/tools/docx/create.js +386 -0
- package/dist/tools/docx/dom.d.ts +66 -0
- package/dist/tools/docx/dom.js +228 -0
- package/dist/tools/docx/index.d.ts +8 -12
- package/dist/tools/docx/index.js +8 -14
- package/dist/tools/docx/modify.d.ts +28 -0
- package/dist/tools/docx/modify.js +271 -0
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +11 -0
- package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +23 -0
- package/dist/tools/docx/ops/header-replace-text-exact.d.ts +13 -0
- package/dist/tools/docx/ops/header-replace-text-exact.js +55 -0
- package/dist/tools/docx/ops/index.d.ts +17 -0
- package/dist/tools/docx/ops/index.js +67 -0
- package/dist/tools/docx/ops/insert-image-after-text.d.ts +24 -0
- package/dist/tools/docx/ops/insert-image-after-text.js +128 -0
- package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +12 -0
- package/dist/tools/docx/ops/insert-paragraph-after-text.js +74 -0
- package/dist/tools/docx/ops/insert-table-after-text.d.ts +19 -0
- package/dist/tools/docx/ops/insert-table-after-text.js +57 -0
- package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +12 -0
- package/dist/tools/docx/ops/replace-hyperlink-url.js +37 -0
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +9 -0
- package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +25 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +9 -0
- package/dist/tools/docx/ops/replace-paragraph-text-exact.js +21 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +8 -0
- package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +23 -0
- package/dist/tools/docx/ops/set-color-for-style.d.ts +9 -0
- package/dist/tools/docx/ops/set-color-for-style.js +27 -0
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +8 -0
- package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +57 -0
- package/dist/tools/docx/ops/table-set-cell-text.d.ts +9 -0
- package/dist/tools/docx/ops/table-set-cell-text.js +72 -0
- package/dist/tools/docx/read.d.ts +27 -0
- package/dist/tools/docx/read.js +188 -0
- package/dist/tools/docx/relationships.d.ts +22 -0
- package/dist/tools/docx/relationships.js +76 -0
- package/dist/tools/docx/types.d.ts +174 -104
- package/dist/tools/docx/types.js +2 -5
- package/dist/tools/docx/validate.d.ts +33 -0
- package/dist/tools/docx/validate.js +49 -0
- package/dist/tools/docx/write.d.ts +17 -0
- package/dist/tools/docx/write.js +88 -0
- package/dist/tools/docx/zip.d.ts +21 -0
- package/dist/tools/docx/zip.js +35 -0
- package/dist/tools/schemas.d.ts +13 -0
- package/dist/tools/schemas.js +5 -0
- package/dist/types.d.ts +10 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +18 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +13977 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +57 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +800 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +9 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +63 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +21 -0
- package/dist/ui/file-preview/src/components/toolbar.d.ts +6 -0
- package/dist/ui/file-preview/src/components/toolbar.js +75 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +764 -0
- package/dist/ui/resources.d.ts +21 -0
- package/dist/ui/resources.js +72 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-lifecycle.d.ts +16 -0
- package/dist/ui/shared/host-lifecycle.js +35 -0
- package/dist/ui/shared/rpc-client.d.ts +14 -0
- package/dist/ui/shared/rpc-client.js +72 -0
- package/dist/ui/shared/theme-adaptation.d.ts +10 -0
- package/dist/ui/shared/theme-adaptation.js +118 -0
- package/dist/ui/shared/tool-header.d.ts +9 -0
- package/dist/ui/shared/tool-header.js +25 -0
- package/dist/ui/shared/tool-shell.d.ts +16 -0
- package/dist/ui/shared/tool-shell.js +65 -0
- package/dist/ui/shared/widget-state.d.ts +28 -0
- package/dist/ui/shared/widget-state.js +60 -0
- package/dist/utils/capture.d.ts +1 -0
- package/dist/utils/capture.js +6 -0
- package/dist/utils/files/docx.d.ts +8 -15
- package/dist/utils/files/docx.js +76 -176
- package/dist/utils/files/text.js +9 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Work with code and text, run processes, and automate tasks, going far beyond oth
|
|
|
16
16
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Desktop Commander MCP" />
|
|
17
17
|
</a>
|
|
18
18
|
|
|
19
|
+
## 👋 We’re hiring — come build with us: https://desktopcommander.app/careers/
|
|
20
|
+
|
|
19
21
|
## Table of Contents
|
|
20
22
|
- [Features](#features)
|
|
21
23
|
- [How to install](#how-to-install)
|
|
@@ -3,11 +3,34 @@ import { withTimeout } from '../utils/withTimeout.js';
|
|
|
3
3
|
import { createErrorResponse } from '../error-handlers.js';
|
|
4
4
|
import { configManager } from '../config-manager.js';
|
|
5
5
|
import { ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, WritePdfArgsSchema } from '../tools/schemas.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { buildUiToolMeta, FILE_PREVIEW_RESOURCE_URI } from '../ui/contracts.js';
|
|
9
|
+
import { resolvePreviewFileType } from '../ui/file-preview/shared/preview-file-types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Expand home directory (~) in a file path
|
|
12
|
+
*/
|
|
13
|
+
function expandHome(filePath) {
|
|
14
|
+
if (filePath === '~' || filePath.startsWith('~/') || filePath.startsWith(`~${path.sep}`)) {
|
|
15
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
16
|
+
}
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a file path to an absolute path for use in structured content.
|
|
21
|
+
* This ensures "Open in folder" always has a valid absolute path.
|
|
22
|
+
*/
|
|
23
|
+
function resolveAbsolutePath(filePath) {
|
|
24
|
+
const expanded = expandHome(filePath);
|
|
25
|
+
return path.isAbsolute(expanded)
|
|
26
|
+
? path.resolve(expanded)
|
|
27
|
+
: path.resolve(process.cwd(), expanded);
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* Helper function to check if path contains an error
|
|
8
31
|
*/
|
|
9
|
-
function isErrorPath(
|
|
10
|
-
return
|
|
32
|
+
function isErrorPath(filePath) {
|
|
33
|
+
return filePath.startsWith('__ERROR__:');
|
|
11
34
|
}
|
|
12
35
|
/**
|
|
13
36
|
* Extract error message from error path
|
|
@@ -44,6 +67,10 @@ export async function handleReadFile(args) {
|
|
|
44
67
|
sheet: sheetParam,
|
|
45
68
|
range: parsed.range
|
|
46
69
|
};
|
|
70
|
+
// Resolve to absolute path for local files (not URLs) so "Open in folder" works
|
|
71
|
+
const resolvedFilePath = parsed.isUrl
|
|
72
|
+
? parsed.path
|
|
73
|
+
: resolveAbsolutePath(parsed.path);
|
|
47
74
|
const fileResult = await readFile(parsed.path, options);
|
|
48
75
|
// Handle PDF files
|
|
49
76
|
if (fileResult.metadata?.isPdf) {
|
|
@@ -68,28 +95,40 @@ export async function handleReadFile(args) {
|
|
|
68
95
|
text: `PDF file: ${parsed.path}${author}${title} (${meta?.totalPages} pages) \n`
|
|
69
96
|
},
|
|
70
97
|
...pdfContent
|
|
71
|
-
]
|
|
98
|
+
],
|
|
99
|
+
structuredContent: {
|
|
100
|
+
fileName: path.basename(resolvedFilePath),
|
|
101
|
+
filePath: resolvedFilePath,
|
|
102
|
+
fileType: 'unsupported',
|
|
103
|
+
content: ''
|
|
104
|
+
},
|
|
105
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
|
|
72
106
|
};
|
|
73
107
|
}
|
|
74
108
|
// Handle image files
|
|
75
109
|
if (fileResult.metadata?.isImage) {
|
|
76
|
-
// For image files,
|
|
77
|
-
//
|
|
110
|
+
// For image files, keep content payload text-only for broad host compatibility.
|
|
111
|
+
// The preview widget reads image bytes from structuredContent.
|
|
78
112
|
const imageData = typeof fileResult.content === 'string'
|
|
79
113
|
? fileResult.content
|
|
80
114
|
: fileResult.content.toString('base64');
|
|
115
|
+
const imageSummary = `Image file: ${parsed.path} (${fileResult.mimeType})\n`;
|
|
81
116
|
return {
|
|
82
117
|
content: [
|
|
83
118
|
{
|
|
84
119
|
type: "text",
|
|
85
|
-
text:
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
type: "image",
|
|
89
|
-
data: imageData,
|
|
90
|
-
mimeType: fileResult.mimeType
|
|
120
|
+
text: imageSummary
|
|
91
121
|
}
|
|
92
122
|
],
|
|
123
|
+
structuredContent: {
|
|
124
|
+
fileName: path.basename(resolvedFilePath),
|
|
125
|
+
filePath: resolvedFilePath,
|
|
126
|
+
fileType: 'image',
|
|
127
|
+
content: imageSummary,
|
|
128
|
+
imageData,
|
|
129
|
+
mimeType: fileResult.mimeType
|
|
130
|
+
},
|
|
131
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
|
|
93
132
|
};
|
|
94
133
|
}
|
|
95
134
|
else {
|
|
@@ -97,8 +136,16 @@ export async function handleReadFile(args) {
|
|
|
97
136
|
const textContent = typeof fileResult.content === 'string'
|
|
98
137
|
? fileResult.content
|
|
99
138
|
: fileResult.content.toString('utf8');
|
|
139
|
+
const previewFileType = resolvePreviewFileType(resolvedFilePath);
|
|
100
140
|
return {
|
|
101
141
|
content: [{ type: "text", text: textContent }],
|
|
142
|
+
structuredContent: {
|
|
143
|
+
fileName: path.basename(resolvedFilePath),
|
|
144
|
+
filePath: resolvedFilePath,
|
|
145
|
+
fileType: previewFileType,
|
|
146
|
+
content: textContent
|
|
147
|
+
},
|
|
148
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true)
|
|
102
149
|
};
|
|
103
150
|
}
|
|
104
151
|
};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ServerResult } from '../types.js';
|
|
2
|
+
type TrackUiEventParams = Record<string, string | number | boolean | null>;
|
|
3
|
+
export declare function buildTrackUiEventCapturePayload(event: string, component: string, params: TrackUiEventParams): Record<string, string | number | boolean | null>;
|
|
2
4
|
/**
|
|
3
5
|
* Handle get_recent_tool_calls command
|
|
4
6
|
*/
|
|
5
7
|
export declare function handleGetRecentToolCalls(args: unknown): Promise<ServerResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Handle track_ui_event command
|
|
10
|
+
*/
|
|
11
|
+
export declare function handleTrackUiEvent(args: unknown): Promise<ServerResult>;
|
|
12
|
+
export {};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { toolHistory } from '../utils/toolHistory.js';
|
|
2
|
-
import { GetRecentToolCallsArgsSchema } from '../tools/schemas.js';
|
|
2
|
+
import { GetRecentToolCallsArgsSchema, TrackUiEventArgsSchema } from '../tools/schemas.js';
|
|
3
|
+
import { capture_ui_event } from '../utils/capture.js';
|
|
4
|
+
export function buildTrackUiEventCapturePayload(event, component, params) {
|
|
5
|
+
return {
|
|
6
|
+
...params,
|
|
7
|
+
component,
|
|
8
|
+
event
|
|
9
|
+
};
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Handle get_recent_tool_calls command
|
|
5
13
|
*/
|
|
@@ -33,3 +41,27 @@ export async function handleGetRecentToolCalls(args) {
|
|
|
33
41
|
};
|
|
34
42
|
}
|
|
35
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Handle track_ui_event command
|
|
46
|
+
*/
|
|
47
|
+
export async function handleTrackUiEvent(args) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = TrackUiEventArgsSchema.parse(args);
|
|
50
|
+
await capture_ui_event('mcp_ui_event', buildTrackUiEventCapturePayload(parsed.event, parsed.component, parsed.params));
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `Tracked UI event: ${parsed.event}`
|
|
55
|
+
}]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `Error tracking UI event: ${error instanceof Error ? error.message : String(error)}`
|
|
63
|
+
}],
|
|
64
|
+
isError: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, InitializeRequestSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
4
|
import { getSystemInfo, getOSSpecificGuidance, getPathGuidance, getDevelopmentToolGuidance } from './utils/system-info.js';
|
|
5
5
|
// Get system information once at startup
|
|
@@ -21,6 +21,8 @@ import { handleWelcomePageOnboarding } from './utils/welcome-onboarding.js';
|
|
|
21
21
|
import { VERSION } from './version.js';
|
|
22
22
|
import { capture, capture_call_tool } from "./utils/capture.js";
|
|
23
23
|
import { logToStderr, logger } from './utils/logger.js';
|
|
24
|
+
import { buildUiToolMeta, FILE_PREVIEW_RESOURCE_URI } from './ui/contracts.js';
|
|
25
|
+
import { listUiResources, readUiResource } from './ui/resources.js';
|
|
24
26
|
// Store startup messages to send after initialization
|
|
25
27
|
const deferredMessages = [];
|
|
26
28
|
function deferLog(level, message) {
|
|
@@ -47,11 +49,18 @@ export const server = new Server({
|
|
|
47
49
|
});
|
|
48
50
|
// Add handler for resources/list method
|
|
49
51
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
50
|
-
// Return an empty list of resources
|
|
51
52
|
return {
|
|
52
|
-
resources:
|
|
53
|
+
resources: listUiResources(),
|
|
53
54
|
};
|
|
54
55
|
});
|
|
56
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
57
|
+
const { uri } = request.params;
|
|
58
|
+
const response = await readUiResource(uri);
|
|
59
|
+
if (response) {
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
63
|
+
});
|
|
55
64
|
// Add handler for prompts/list method
|
|
56
65
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
57
66
|
// Return an empty list of prompts
|
|
@@ -237,6 +246,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
237
246
|
${PATH_GUIDANCE}
|
|
238
247
|
${CMD_PREFIX_DESCRIPTION}`,
|
|
239
248
|
inputSchema: zodToJsonSchema(ReadFileArgsSchema),
|
|
249
|
+
_meta: buildUiToolMeta(FILE_PREVIEW_RESOURCE_URI, true),
|
|
240
250
|
annotations: {
|
|
241
251
|
title: "Read File or URL",
|
|
242
252
|
readOnlyHint: true,
|
|
@@ -1179,6 +1189,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1179
1189
|
};
|
|
1180
1190
|
}
|
|
1181
1191
|
break;
|
|
1192
|
+
case "track_ui_event":
|
|
1193
|
+
try {
|
|
1194
|
+
result = await handlers.handleTrackUiEvent(args);
|
|
1195
|
+
}
|
|
1196
|
+
catch (error) {
|
|
1197
|
+
capture('server_request_error', { message: `Error in track_ui_event handler: ${error}` });
|
|
1198
|
+
result = {
|
|
1199
|
+
content: [{ type: "text", text: `Error: Failed to track UI event` }],
|
|
1200
|
+
isError: true,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1182
1204
|
case "give_feedback_to_desktop_commander":
|
|
1183
1205
|
try {
|
|
1184
1206
|
result = await giveFeedbackToDesktopCommander(args);
|
|
@@ -1265,12 +1287,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1265
1287
|
// Add tool call to history (exclude only get_recent_tool_calls to prevent recursion)
|
|
1266
1288
|
const duration = Date.now() - startTime;
|
|
1267
1289
|
const EXCLUDED_TOOLS = [
|
|
1268
|
-
'get_recent_tool_calls'
|
|
1290
|
+
'get_recent_tool_calls',
|
|
1291
|
+
'track_ui_event'
|
|
1269
1292
|
];
|
|
1270
1293
|
if (!EXCLUDED_TOOLS.includes(name)) {
|
|
1271
1294
|
toolHistory.addCall(name, args, result, duration);
|
|
1272
1295
|
}
|
|
1273
1296
|
// Track success or failure based on result
|
|
1297
|
+
if (name === 'track_ui_event') {
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1274
1300
|
if (result.isError) {
|
|
1275
1301
|
await usageTracker.trackFailure(name);
|
|
1276
1302
|
console.log(`[FEEDBACK DEBUG] Tool ${name} failed, not checking feedback`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image builder — creates w:drawing elements and manages image relationships.
|
|
3
|
+
*/
|
|
4
|
+
import PizZip from 'pizzip';
|
|
5
|
+
import type { DocxContentImage, InsertImageOp } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Build an image element and add it to the ZIP archive.
|
|
8
|
+
*
|
|
9
|
+
* @param doc The XML document
|
|
10
|
+
* @param zip The DOCX ZIP archive
|
|
11
|
+
* @param spec The image specification (from content or operation)
|
|
12
|
+
* @returns A w:p element containing the image drawing
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildImageElement(doc: Document, zip: PizZip, spec: DocxContentImage | InsertImageOp): Promise<Element>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image builder — creates w:drawing elements and manages image relationships.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
7
|
+
import { addImageRelationship, ensureContentType } from '../relationships.js';
|
|
8
|
+
import { escapeXmlAttr } from './utils.js';
|
|
9
|
+
import { pixelsToEmu, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, NAMESPACES } from '../constants.js';
|
|
10
|
+
/**
|
|
11
|
+
* Build an image element and add it to the ZIP archive.
|
|
12
|
+
*
|
|
13
|
+
* @param doc The XML document
|
|
14
|
+
* @param zip The DOCX ZIP archive
|
|
15
|
+
* @param spec The image specification (from content or operation)
|
|
16
|
+
* @returns A w:p element containing the image drawing
|
|
17
|
+
*/
|
|
18
|
+
export async function buildImageElement(doc, zip, spec) {
|
|
19
|
+
// Validate image exists
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(spec.imagePath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`Image file not found: ${spec.imagePath}`);
|
|
25
|
+
}
|
|
26
|
+
// Read image
|
|
27
|
+
const imgBuffer = await fs.readFile(spec.imagePath);
|
|
28
|
+
const ext = path.extname(spec.imagePath).toLowerCase();
|
|
29
|
+
const baseName = path.basename(spec.imagePath);
|
|
30
|
+
// Find next available media filename
|
|
31
|
+
let mediaIndex = 1;
|
|
32
|
+
while (zip.file(`word/media/image${mediaIndex}${ext}`)) {
|
|
33
|
+
mediaIndex++;
|
|
34
|
+
}
|
|
35
|
+
const mediaFileName = `image${mediaIndex}${ext}`;
|
|
36
|
+
// Add image to ZIP
|
|
37
|
+
zip.file(`word/media/${mediaFileName}`, imgBuffer);
|
|
38
|
+
// Add relationship
|
|
39
|
+
const rId = addImageRelationship(zip, mediaFileName);
|
|
40
|
+
// Ensure Content_Types entry
|
|
41
|
+
ensureContentType(zip, ext);
|
|
42
|
+
// Compute dimensions (EMU)
|
|
43
|
+
const widthPx = spec.width ?? DEFAULT_IMAGE_WIDTH;
|
|
44
|
+
const heightPx = spec.height ?? DEFAULT_IMAGE_HEIGHT;
|
|
45
|
+
const widthEmu = pixelsToEmu(widthPx);
|
|
46
|
+
const heightEmu = pixelsToEmu(heightPx);
|
|
47
|
+
// Build drawing XML
|
|
48
|
+
const altText = spec.altText ?? baseName;
|
|
49
|
+
const drawingXmlStr = buildDrawingXml(rId, widthEmu, heightEmu, altText, mediaFileName);
|
|
50
|
+
// Parse drawing XML into a paragraph
|
|
51
|
+
const drawingFragment = new DOMParser().parseFromString(`<w:p xmlns:w="${NAMESPACES.W}">` +
|
|
52
|
+
`<w:r>${drawingXmlStr}</w:r></w:p>`, 'application/xml');
|
|
53
|
+
return doc.importNode(drawingFragment.documentElement, true);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the inline w:drawing XML for an image reference.
|
|
57
|
+
*/
|
|
58
|
+
function buildDrawingXml(rId, widthEmu, heightEmu, altText, fileName) {
|
|
59
|
+
return (`<w:drawing xmlns:w="${NAMESPACES.W}">` +
|
|
60
|
+
`<wp:inline distT="0" distB="0" distL="0" distR="0" ` +
|
|
61
|
+
`xmlns:wp="${NAMESPACES.WP}">` +
|
|
62
|
+
`<wp:extent cx="${widthEmu}" cy="${heightEmu}"/>` +
|
|
63
|
+
`<wp:docPr id="1" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
|
|
64
|
+
`<a:graphic xmlns:a="${NAMESPACES.A}">` +
|
|
65
|
+
`<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">` +
|
|
66
|
+
`<pic:pic xmlns:pic="${NAMESPACES.PIC}">` +
|
|
67
|
+
`<pic:nvPicPr>` +
|
|
68
|
+
`<pic:cNvPr id="0" name="${fileName}" descr="${escapeXmlAttr(altText)}"/>` +
|
|
69
|
+
`<pic:cNvPicPr/>` +
|
|
70
|
+
`</pic:nvPicPr>` +
|
|
71
|
+
`<pic:blipFill>` +
|
|
72
|
+
`<a:blip r:embed="${rId}" xmlns:r="${NAMESPACES.R}"/>` +
|
|
73
|
+
`<a:stretch><a:fillRect/></a:stretch>` +
|
|
74
|
+
`</pic:blipFill>` +
|
|
75
|
+
`<pic:spPr>` +
|
|
76
|
+
`<a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm>` +
|
|
77
|
+
`<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` +
|
|
78
|
+
`</pic:spPr>` +
|
|
79
|
+
`</pic:pic>` +
|
|
80
|
+
`</a:graphicData>` +
|
|
81
|
+
`</a:graphic>` +
|
|
82
|
+
`</wp:inline>` +
|
|
83
|
+
`</w:drawing>`);
|
|
84
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DOCX
|
|
3
|
-
*
|
|
2
|
+
* DOCX element builders — Single Responsibility: build XML elements
|
|
3
|
+
* for paragraphs, tables, and images.
|
|
4
|
+
*
|
|
5
|
+
* These builders are shared between create.ts and ops/ modules to
|
|
6
|
+
* eliminate code duplication and ensure consistency.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
8
|
+
export { buildParagraph } from './paragraph.js';
|
|
9
|
+
export { buildTable } from './table.js';
|
|
10
|
+
export { buildImageElement } from './image.js';
|
|
11
|
+
export { escapeXml, escapeXmlAttr } from './utils.js';
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DOCX
|
|
3
|
-
*
|
|
2
|
+
* DOCX element builders — Single Responsibility: build XML elements
|
|
3
|
+
* for paragraphs, tables, and images.
|
|
4
|
+
*
|
|
5
|
+
* These builders are shared between create.ts and ops/ modules to
|
|
6
|
+
* eliminate code duplication and ensure consistency.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
8
|
+
export { buildParagraph } from './paragraph.js';
|
|
9
|
+
export { buildTable } from './table.js';
|
|
10
|
+
export { buildImageElement } from './image.js';
|
|
11
|
+
export { escapeXml, escapeXmlAttr } from './utils.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paragraph builder — creates w:p elements with optional styles.
|
|
3
|
+
*/
|
|
4
|
+
import type { DocxContentParagraph } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build a paragraph element from content structure.
|
|
7
|
+
*
|
|
8
|
+
* @param doc The XML document
|
|
9
|
+
* @param item The paragraph content item
|
|
10
|
+
* @returns A w:p element
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildParagraph(doc: Document, item: DocxContentParagraph): Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paragraph builder — creates w:p elements with optional styles.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Build a paragraph element from content structure.
|
|
6
|
+
*
|
|
7
|
+
* @param doc The XML document
|
|
8
|
+
* @param item The paragraph content item
|
|
9
|
+
* @returns A w:p element
|
|
10
|
+
*/
|
|
11
|
+
export function buildParagraph(doc, item) {
|
|
12
|
+
const p = doc.createElement('w:p');
|
|
13
|
+
// Set style if provided
|
|
14
|
+
if (item.style) {
|
|
15
|
+
const pPr = doc.createElement('w:pPr');
|
|
16
|
+
const pStyle = doc.createElement('w:pStyle');
|
|
17
|
+
pStyle.setAttribute('w:val', item.style);
|
|
18
|
+
pPr.appendChild(pStyle);
|
|
19
|
+
p.appendChild(pPr);
|
|
20
|
+
}
|
|
21
|
+
// Add text run
|
|
22
|
+
const r = doc.createElement('w:r');
|
|
23
|
+
const t = doc.createElement('w:t');
|
|
24
|
+
t.setAttribute('xml:space', 'preserve');
|
|
25
|
+
t.textContent = item.text;
|
|
26
|
+
r.appendChild(t);
|
|
27
|
+
p.appendChild(r);
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table builder — creates w:tbl elements with headers, rows, and styling.
|
|
3
|
+
*/
|
|
4
|
+
import type { DocxContentTable, InsertTableOp } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build a table element from content structure or operation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildTable(doc: Document, spec: DocxContentTable | InsertTableOp): Element;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table builder — creates w:tbl elements with headers, rows, and styling.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Build a table element from content structure or operation.
|
|
6
|
+
*/
|
|
7
|
+
export function buildTable(doc, spec) {
|
|
8
|
+
const tbl = doc.createElement('w:tbl');
|
|
9
|
+
// Table properties
|
|
10
|
+
const tblPr = doc.createElement('w:tblPr');
|
|
11
|
+
if (spec.style) {
|
|
12
|
+
const tblStyle = doc.createElement('w:tblStyle');
|
|
13
|
+
tblStyle.setAttribute('w:val', spec.style);
|
|
14
|
+
tblPr.appendChild(tblStyle);
|
|
15
|
+
}
|
|
16
|
+
const tblW = doc.createElement('w:tblW');
|
|
17
|
+
tblW.setAttribute('w:w', '0');
|
|
18
|
+
tblW.setAttribute('w:type', 'auto');
|
|
19
|
+
tblPr.appendChild(tblW);
|
|
20
|
+
// Table borders
|
|
21
|
+
const tblBorders = doc.createElement('w:tblBorders');
|
|
22
|
+
for (const side of ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']) {
|
|
23
|
+
const border = doc.createElement(`w:${side}`);
|
|
24
|
+
border.setAttribute('w:val', 'single');
|
|
25
|
+
border.setAttribute('w:sz', '4');
|
|
26
|
+
border.setAttribute('w:space', '0');
|
|
27
|
+
border.setAttribute('w:color', '000000');
|
|
28
|
+
tblBorders.appendChild(border);
|
|
29
|
+
}
|
|
30
|
+
tblPr.appendChild(tblBorders);
|
|
31
|
+
tbl.appendChild(tblPr);
|
|
32
|
+
// Table grid
|
|
33
|
+
const colCount = spec.headers
|
|
34
|
+
? spec.headers.length
|
|
35
|
+
: spec.rows.length > 0
|
|
36
|
+
? spec.rows[0].length
|
|
37
|
+
: 0;
|
|
38
|
+
if (colCount > 0) {
|
|
39
|
+
const tblGrid = doc.createElement('w:tblGrid');
|
|
40
|
+
for (let c = 0; c < colCount; c++) {
|
|
41
|
+
const gridCol = doc.createElement('w:gridCol');
|
|
42
|
+
const w = spec.colWidths?.[c] ?? Math.floor(9000 / colCount);
|
|
43
|
+
gridCol.setAttribute('w:w', String(w));
|
|
44
|
+
tblGrid.appendChild(gridCol);
|
|
45
|
+
}
|
|
46
|
+
tbl.appendChild(tblGrid);
|
|
47
|
+
}
|
|
48
|
+
// Helper to build a cell
|
|
49
|
+
const buildCell = (text, isHeader, widthTwips) => {
|
|
50
|
+
const tc = doc.createElement('w:tc');
|
|
51
|
+
if (widthTwips) {
|
|
52
|
+
const tcPr = doc.createElement('w:tcPr');
|
|
53
|
+
const tcW = doc.createElement('w:tcW');
|
|
54
|
+
tcW.setAttribute('w:w', String(widthTwips));
|
|
55
|
+
tcW.setAttribute('w:type', 'dxa');
|
|
56
|
+
tcPr.appendChild(tcW);
|
|
57
|
+
tc.appendChild(tcPr);
|
|
58
|
+
}
|
|
59
|
+
const p = doc.createElement('w:p');
|
|
60
|
+
const r = doc.createElement('w:r');
|
|
61
|
+
if (isHeader) {
|
|
62
|
+
const rPr = doc.createElement('w:rPr');
|
|
63
|
+
const b = doc.createElement('w:b');
|
|
64
|
+
rPr.appendChild(b);
|
|
65
|
+
r.appendChild(rPr);
|
|
66
|
+
}
|
|
67
|
+
const t = doc.createElement('w:t');
|
|
68
|
+
t.setAttribute('xml:space', 'preserve');
|
|
69
|
+
t.textContent = text;
|
|
70
|
+
r.appendChild(t);
|
|
71
|
+
p.appendChild(r);
|
|
72
|
+
tc.appendChild(p);
|
|
73
|
+
return tc;
|
|
74
|
+
};
|
|
75
|
+
// Header row
|
|
76
|
+
if (spec.headers && spec.headers.length > 0) {
|
|
77
|
+
const tr = doc.createElement('w:tr');
|
|
78
|
+
for (let i = 0; i < spec.headers.length; i++) {
|
|
79
|
+
const width = spec.colWidths?.[i];
|
|
80
|
+
tr.appendChild(buildCell(spec.headers[i], true, width));
|
|
81
|
+
}
|
|
82
|
+
tbl.appendChild(tr);
|
|
83
|
+
}
|
|
84
|
+
// Data rows
|
|
85
|
+
for (const row of spec.rows) {
|
|
86
|
+
const tr = doc.createElement('w:tr');
|
|
87
|
+
for (let i = 0; i < row.length; i++) {
|
|
88
|
+
const width = spec.colWidths?.[i];
|
|
89
|
+
tr.appendChild(buildCell(row[i], false, width));
|
|
90
|
+
}
|
|
91
|
+
tbl.appendChild(tr);
|
|
92
|
+
}
|
|
93
|
+
return tbl;
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML escaping utilities.
|
|
3
|
+
*/
|
|
4
|
+
export function escapeXml(s) {
|
|
5
|
+
return s
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
}
|
|
12
|
+
export function escapeXmlAttr(s) {
|
|
13
|
+
return s
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/"/g, '"')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>');
|
|
18
|
+
}
|
|
@@ -1,36 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DOCX
|
|
3
|
-
*
|
|
4
|
-
* Centralised constants shared across the DOCX module.
|
|
5
|
-
*
|
|
6
|
-
* @module docx/constants
|
|
2
|
+
* DOCX constants — shared values used across the module.
|
|
7
3
|
*/
|
|
8
|
-
export declare const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly footer: 720;
|
|
24
|
-
readonly gutter: 0;
|
|
25
|
-
};
|
|
26
|
-
readonly footer: true;
|
|
27
|
-
readonly pageNumber: false;
|
|
4
|
+
export declare const IMAGE_MIME_TYPES: Record<string, string>;
|
|
5
|
+
export declare function getMimeType(ext: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Convert pixels to EMU (English Metric Units).
|
|
8
|
+
* 1 inch = 914400 EMU, 1 px ≈ 9525 EMU (at 96 DPI)
|
|
9
|
+
*/
|
|
10
|
+
export declare const PX_TO_EMU = 9525;
|
|
11
|
+
export declare function pixelsToEmu(px: number): number;
|
|
12
|
+
export declare const NAMESPACES: {
|
|
13
|
+
readonly W: "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14
|
+
readonly WP: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
|
15
|
+
readonly A: "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
16
|
+
readonly PIC: "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
|
17
|
+
readonly R: "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
18
|
+
readonly RELS: "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
28
19
|
};
|
|
29
|
-
export declare const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readonly
|
|
20
|
+
export declare const DEFAULT_IMAGE_WIDTH = 300;
|
|
21
|
+
export declare const DEFAULT_IMAGE_HEIGHT = 200;
|
|
22
|
+
export declare const DOCX_PATHS: {
|
|
23
|
+
readonly CONTENT_TYPES: "[Content_Types].xml";
|
|
24
|
+
readonly DOCUMENT_XML: "word/document.xml";
|
|
25
|
+
readonly DOCUMENT_RELS: "word/_rels/document.xml.rels";
|
|
26
|
+
readonly ROOT_RELS: "_rels/.rels";
|
|
27
|
+
readonly STYLES_XML: "word/styles.xml";
|
|
28
|
+
readonly SETTINGS_XML: "word/settings.xml";
|
|
29
|
+
readonly WEB_SETTINGS_XML: "word/webSettings.xml";
|
|
30
|
+
readonly FONT_TABLE_XML: "word/fontTable.xml";
|
|
31
|
+
readonly MEDIA_FOLDER: "word/media";
|
|
33
32
|
};
|
|
34
|
-
export declare const CORE_PROPERTIES_PATH = "docProps/core.xml";
|
|
35
|
-
export declare const IMAGE_MIME_TYPES: Readonly<Record<string, string>>;
|
|
36
|
-
export declare const HTML_WRAPPER_TEMPLATE = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n</head>\n<body>\n{content}\n</body>\n</html>";
|