@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.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/dist/handlers/filesystem-handlers.js +58 -11
  3. package/dist/handlers/history-handlers.d.ts +7 -0
  4. package/dist/handlers/history-handlers.js +33 -1
  5. package/dist/server.js +30 -4
  6. package/dist/tools/docx/builders/image.d.ts +14 -0
  7. package/dist/tools/docx/builders/image.js +84 -0
  8. package/dist/tools/docx/builders/index.d.ts +9 -3
  9. package/dist/tools/docx/builders/index.js +9 -3
  10. package/dist/tools/docx/builders/paragraph.d.ts +12 -0
  11. package/dist/tools/docx/builders/paragraph.js +29 -0
  12. package/dist/tools/docx/builders/table.d.ts +8 -0
  13. package/dist/tools/docx/builders/table.js +94 -0
  14. package/dist/tools/docx/builders/utils.d.ts +5 -0
  15. package/dist/tools/docx/builders/utils.js +18 -0
  16. package/dist/tools/docx/constants.d.ts +28 -32
  17. package/dist/tools/docx/constants.js +56 -52
  18. package/dist/tools/docx/create.d.ts +21 -0
  19. package/dist/tools/docx/create.js +386 -0
  20. package/dist/tools/docx/dom.d.ts +66 -0
  21. package/dist/tools/docx/dom.js +228 -0
  22. package/dist/tools/docx/index.d.ts +8 -12
  23. package/dist/tools/docx/index.js +8 -14
  24. package/dist/tools/docx/modify.d.ts +28 -0
  25. package/dist/tools/docx/modify.js +271 -0
  26. package/dist/tools/docx/ops/delete-paragraph-at-body-index.d.ts +11 -0
  27. package/dist/tools/docx/ops/delete-paragraph-at-body-index.js +23 -0
  28. package/dist/tools/docx/ops/header-replace-text-exact.d.ts +13 -0
  29. package/dist/tools/docx/ops/header-replace-text-exact.js +55 -0
  30. package/dist/tools/docx/ops/index.d.ts +17 -0
  31. package/dist/tools/docx/ops/index.js +67 -0
  32. package/dist/tools/docx/ops/insert-image-after-text.d.ts +24 -0
  33. package/dist/tools/docx/ops/insert-image-after-text.js +128 -0
  34. package/dist/tools/docx/ops/insert-paragraph-after-text.d.ts +12 -0
  35. package/dist/tools/docx/ops/insert-paragraph-after-text.js +74 -0
  36. package/dist/tools/docx/ops/insert-table-after-text.d.ts +19 -0
  37. package/dist/tools/docx/ops/insert-table-after-text.js +57 -0
  38. package/dist/tools/docx/ops/replace-hyperlink-url.d.ts +12 -0
  39. package/dist/tools/docx/ops/replace-hyperlink-url.js +37 -0
  40. package/dist/tools/docx/ops/replace-paragraph-at-body-index.d.ts +9 -0
  41. package/dist/tools/docx/ops/replace-paragraph-at-body-index.js +25 -0
  42. package/dist/tools/docx/ops/replace-paragraph-text-exact.d.ts +9 -0
  43. package/dist/tools/docx/ops/replace-paragraph-text-exact.js +21 -0
  44. package/dist/tools/docx/ops/set-color-for-paragraph-exact.d.ts +8 -0
  45. package/dist/tools/docx/ops/set-color-for-paragraph-exact.js +23 -0
  46. package/dist/tools/docx/ops/set-color-for-style.d.ts +9 -0
  47. package/dist/tools/docx/ops/set-color-for-style.js +27 -0
  48. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.d.ts +8 -0
  49. package/dist/tools/docx/ops/set-paragraph-style-at-body-index.js +57 -0
  50. package/dist/tools/docx/ops/table-set-cell-text.d.ts +9 -0
  51. package/dist/tools/docx/ops/table-set-cell-text.js +72 -0
  52. package/dist/tools/docx/read.d.ts +27 -0
  53. package/dist/tools/docx/read.js +188 -0
  54. package/dist/tools/docx/relationships.d.ts +22 -0
  55. package/dist/tools/docx/relationships.js +76 -0
  56. package/dist/tools/docx/types.d.ts +174 -104
  57. package/dist/tools/docx/types.js +2 -5
  58. package/dist/tools/docx/validate.d.ts +33 -0
  59. package/dist/tools/docx/validate.js +49 -0
  60. package/dist/tools/docx/write.d.ts +17 -0
  61. package/dist/tools/docx/write.js +88 -0
  62. package/dist/tools/docx/zip.d.ts +21 -0
  63. package/dist/tools/docx/zip.js +35 -0
  64. package/dist/tools/schemas.d.ts +13 -0
  65. package/dist/tools/schemas.js +5 -0
  66. package/dist/types.d.ts +10 -0
  67. package/dist/ui/contracts.d.ts +14 -0
  68. package/dist/ui/contracts.js +18 -0
  69. package/dist/ui/file-preview/index.html +16 -0
  70. package/dist/ui/file-preview/preview-runtime.js +13977 -0
  71. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  72. package/dist/ui/file-preview/shared/preview-file-types.js +57 -0
  73. package/dist/ui/file-preview/src/app.d.ts +4 -0
  74. package/dist/ui/file-preview/src/app.js +800 -0
  75. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  76. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  77. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  78. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  79. package/dist/ui/file-preview/src/components/html-renderer.d.ts +9 -0
  80. package/dist/ui/file-preview/src/components/html-renderer.js +63 -0
  81. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  82. package/dist/ui/file-preview/src/components/markdown-renderer.js +21 -0
  83. package/dist/ui/file-preview/src/components/toolbar.d.ts +6 -0
  84. package/dist/ui/file-preview/src/components/toolbar.js +75 -0
  85. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  86. package/dist/ui/file-preview/src/image-preview.js +21 -0
  87. package/dist/ui/file-preview/src/main.d.ts +1 -0
  88. package/dist/ui/file-preview/src/main.js +5 -0
  89. package/dist/ui/file-preview/src/types.d.ts +1 -0
  90. package/dist/ui/file-preview/src/types.js +1 -0
  91. package/dist/ui/file-preview/styles.css +764 -0
  92. package/dist/ui/resources.d.ts +21 -0
  93. package/dist/ui/resources.js +72 -0
  94. package/dist/ui/shared/escape-html.d.ts +4 -0
  95. package/dist/ui/shared/escape-html.js +11 -0
  96. package/dist/ui/shared/host-lifecycle.d.ts +16 -0
  97. package/dist/ui/shared/host-lifecycle.js +35 -0
  98. package/dist/ui/shared/rpc-client.d.ts +14 -0
  99. package/dist/ui/shared/rpc-client.js +72 -0
  100. package/dist/ui/shared/theme-adaptation.d.ts +10 -0
  101. package/dist/ui/shared/theme-adaptation.js +118 -0
  102. package/dist/ui/shared/tool-header.d.ts +9 -0
  103. package/dist/ui/shared/tool-header.js +25 -0
  104. package/dist/ui/shared/tool-shell.d.ts +16 -0
  105. package/dist/ui/shared/tool-shell.js +65 -0
  106. package/dist/ui/shared/widget-state.d.ts +28 -0
  107. package/dist/ui/shared/widget-state.js +60 -0
  108. package/dist/utils/capture.d.ts +1 -0
  109. package/dist/utils/capture.js +6 -0
  110. package/dist/utils/files/docx.d.ts +8 -15
  111. package/dist/utils/files/docx.js +76 -176
  112. package/dist/utils/files/text.js +9 -1
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.js +1 -1
  115. package/package.json +5 -2
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Op: set_paragraph_style_at_body_index
3
+ *
4
+ * Set (or replace) the paragraph style (w:pPr/w:pStyle) at a given
5
+ * bodyChildIndex. Skips if the child is not a w:p.
6
+ */
7
+ import type { SetParagraphStyleAtBodyIndexOp, OpResult } from '../types.js';
8
+ export declare function applySetParagraphStyleAtBodyIndex(body: Element, op: SetParagraphStyleAtBodyIndexOp): OpResult;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Op: set_paragraph_style_at_body_index
3
+ *
4
+ * Set (or replace) the paragraph style (w:pPr/w:pStyle) at a given
5
+ * bodyChildIndex. Skips if the child is not a w:p.
6
+ */
7
+ import { getBodyChildren, nodeListToArray } from '../dom.js';
8
+ export function applySetParagraphStyleAtBodyIndex(body, op) {
9
+ const children = getBodyChildren(body);
10
+ const idx = op.bodyChildIndex;
11
+ if (idx < 0 || idx >= children.length) {
12
+ return { op, status: 'skipped', matched: 0, reason: 'index_out_of_range' };
13
+ }
14
+ const child = children[idx];
15
+ if (child.nodeName !== 'w:p') {
16
+ return { op, status: 'skipped', matched: 0, reason: 'not_a_paragraph' };
17
+ }
18
+ const doc = child.ownerDocument;
19
+ if (!doc)
20
+ return { op, status: 'skipped', matched: 0, reason: 'no_owner_document' };
21
+ // Find or create w:pPr
22
+ let pPr = null;
23
+ for (const n of nodeListToArray(child.childNodes)) {
24
+ if (n.nodeType === 1 && n.nodeName === 'w:pPr') {
25
+ pPr = n;
26
+ break;
27
+ }
28
+ }
29
+ if (!pPr) {
30
+ pPr = doc.createElement('w:pPr');
31
+ if (child.firstChild) {
32
+ child.insertBefore(pPr, child.firstChild);
33
+ }
34
+ else {
35
+ child.appendChild(pPr);
36
+ }
37
+ }
38
+ // Find or create w:pStyle inside pPr
39
+ let pStyle = null;
40
+ for (const n of nodeListToArray(pPr.childNodes)) {
41
+ if (n.nodeType === 1 && n.nodeName === 'w:pStyle') {
42
+ pStyle = n;
43
+ break;
44
+ }
45
+ }
46
+ if (!pStyle) {
47
+ pStyle = doc.createElement('w:pStyle');
48
+ if (pPr.firstChild) {
49
+ pPr.insertBefore(pStyle, pPr.firstChild);
50
+ }
51
+ else {
52
+ pPr.appendChild(pStyle);
53
+ }
54
+ }
55
+ pStyle.setAttribute('w:val', op.style);
56
+ return { op, status: 'applied', matched: 1 };
57
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Op: table_set_cell_text
3
+ *
4
+ * Set the text content of a specific table cell.
5
+ * Targets by: tableIndex (0-based among w:tbl in body), row, col.
6
+ * Applies minimal text replacement inside the cell's first paragraph.
7
+ */
8
+ import type { TableSetCellTextOp, OpResult } from '../types.js';
9
+ export declare function applyTableSetCellText(body: Element, op: TableSetCellTextOp): OpResult;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Op: table_set_cell_text
3
+ *
4
+ * Set the text content of a specific table cell.
5
+ * Targets by: tableIndex (0-based among w:tbl in body), row, col.
6
+ * Applies minimal text replacement inside the cell's first paragraph.
7
+ */
8
+ import { getBodyChildren, nodeListToArray, setParagraphTextMinimal } from '../dom.js';
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) {
24
+ return { op, status: 'skipped', matched: 0, reason: 'table_not_found' };
25
+ }
26
+ // Find the n-th w:tr
27
+ const rows = [];
28
+ for (const child of nodeListToArray(table.childNodes)) {
29
+ if (child.nodeType === 1 && child.nodeName === 'w:tr') {
30
+ rows.push(child);
31
+ }
32
+ }
33
+ if (op.row < 0 || op.row >= rows.length) {
34
+ return { op, status: 'skipped', matched: 0, reason: 'row_out_of_range' };
35
+ }
36
+ // Find the n-th w:tc in the row
37
+ const cells = [];
38
+ for (const child of nodeListToArray(rows[op.row].childNodes)) {
39
+ if (child.nodeType === 1 && child.nodeName === 'w:tc') {
40
+ cells.push(child);
41
+ }
42
+ }
43
+ if (op.col < 0 || op.col >= cells.length) {
44
+ return { op, status: 'skipped', matched: 0, reason: 'col_out_of_range' };
45
+ }
46
+ 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' };
72
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * DOCX reading utilities
3
+ * Extracts text, metadata, and compact outlines from DOCX files.
4
+ */
5
+ import type { DocxMetadata, DocxParagraph, ReadDocxResult } from './types.js';
6
+ /**
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.
10
+ */
11
+ export declare function readDocxOutline(filePath: string): Promise<ReadDocxResult>;
12
+ /** Extract plain text from DOCX. */
13
+ export declare function extractTextFromDocx(path: string): Promise<string>;
14
+ /** Get comprehensive metadata. */
15
+ export declare function getDocxMetadata(path: string): Promise<DocxMetadata>;
16
+ /** Extract body XML. */
17
+ export declare function extractBodyXml(path: string): Promise<string>;
18
+ /** Read DOCX file with optional pagination. */
19
+ export declare function readDocx(path: string, options?: {
20
+ offset?: number;
21
+ length?: number;
22
+ }): Promise<{
23
+ text: string;
24
+ paragraphs: DocxParagraph[];
25
+ metadata: DocxMetadata;
26
+ bodyXml: string;
27
+ }>;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * DOCX reading utilities
3
+ * Extracts text, metadata, and compact outlines from DOCX files.
4
+ */
5
+ import fs from 'fs/promises';
6
+ import PizZip from 'pizzip';
7
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
8
+ import { nodeListToArray, getParagraphText, getParagraphStyle, getBody, getBodyChildren, countTables, countImages, } from './dom.js';
9
+ // ═══════════════════════════════════════════════════════════════════════
10
+ // Internal helpers
11
+ // ═══════════════════════════════════════════════════════════════════════
12
+ async function loadDocx(path) {
13
+ const inputBuf = await fs.readFile(path);
14
+ return new PizZip(inputBuf);
15
+ }
16
+ // ═══════════════════════════════════════════════════════════════════════
17
+ // readDocxOutline — compact JSON outline (used by read_docx tool)
18
+ // ═══════════════════════════════════════════════════════════════════════
19
+ /**
20
+ * Return a token-efficient outline of a DOCX file.
21
+ * Every paragraph gets a bodyChildIndex (among ALL w:body children)
22
+ * plus a paragraphIndex (counting only w:p), style id, and text.
23
+ */
24
+ export async function readDocxOutline(filePath) {
25
+ const zip = await loadDocx(filePath);
26
+ const docFile = zip.file('word/document.xml');
27
+ if (!docFile)
28
+ throw new Error('Invalid DOCX: missing word/document.xml');
29
+ const xmlStr = docFile.asText();
30
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
31
+ const body = getBody(dom);
32
+ const children = getBodyChildren(body);
33
+ const paragraphs = [];
34
+ const stylesSet = new Set();
35
+ let paragraphIndex = 0;
36
+ for (let i = 0; i < children.length; i++) {
37
+ const child = children[i];
38
+ if (child.nodeName !== 'w:p')
39
+ continue;
40
+ const text = getParagraphText(child).trim();
41
+ const style = getParagraphStyle(child);
42
+ if (style)
43
+ stylesSet.add(style);
44
+ paragraphs.push({
45
+ bodyChildIndex: i,
46
+ paragraphIndex,
47
+ style,
48
+ text,
49
+ });
50
+ paragraphIndex++;
51
+ }
52
+ return {
53
+ path: filePath,
54
+ paragraphs,
55
+ stylesSeen: [...stylesSet].sort(),
56
+ counts: {
57
+ tables: countTables(children),
58
+ images: countImages(body),
59
+ bodyChildren: children.length,
60
+ },
61
+ };
62
+ }
63
+ // ═══════════════════════════════════════════════════════════════════════
64
+ // Legacy read functions (used by read_file handler / file handler)
65
+ // ═══════════════════════════════════════════════════════════════════════
66
+ /** Extract plain text from DOCX. */
67
+ export async function extractTextFromDocx(path) {
68
+ const zip = await loadDocx(path);
69
+ const docFile = zip.file('word/document.xml');
70
+ if (!docFile)
71
+ throw new Error('Invalid DOCX: missing word/document.xml');
72
+ const xmlStr = docFile.asText();
73
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
74
+ const body = dom.getElementsByTagName('w:body').item(0);
75
+ if (!body)
76
+ throw new Error('Invalid DOCX: missing w:body');
77
+ const paragraphs = [];
78
+ for (const child of nodeListToArray(body.childNodes)) {
79
+ if (child.nodeType !== 1)
80
+ continue;
81
+ if (child.nodeName !== 'w:p')
82
+ continue;
83
+ const text = getParagraphText(child).trim();
84
+ if (text)
85
+ paragraphs.push(text);
86
+ }
87
+ return paragraphs.join('\n\n');
88
+ }
89
+ /** Extract paragraphs from DOCX. */
90
+ async function extractParagraphs(path) {
91
+ const zip = await loadDocx(path);
92
+ const docFile = zip.file('word/document.xml');
93
+ if (!docFile)
94
+ throw new Error('Invalid DOCX: missing word/document.xml');
95
+ const xmlStr = docFile.asText();
96
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
97
+ const body = dom.getElementsByTagName('w:body').item(0);
98
+ if (!body)
99
+ throw new Error('Invalid DOCX: missing w:body');
100
+ const paragraphs = [];
101
+ let index = 0;
102
+ for (const child of nodeListToArray(body.childNodes)) {
103
+ if (child.nodeType !== 1)
104
+ continue;
105
+ if (child.nodeName !== 'w:p')
106
+ continue;
107
+ const text = getParagraphText(child).trim();
108
+ paragraphs.push({ index, text, hasText: text.length > 0 });
109
+ index++;
110
+ }
111
+ return paragraphs;
112
+ }
113
+ /** Get core properties from DOCX. */
114
+ async function getCoreProperties(zip) {
115
+ const corePropsFile = zip.file('docProps/core.xml');
116
+ if (!corePropsFile)
117
+ return {};
118
+ const xmlStr = corePropsFile.asText();
119
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
120
+ const getProperty = (name) => {
121
+ const elements = dom.getElementsByTagName(name);
122
+ if (elements.length > 0)
123
+ return elements[0].textContent || undefined;
124
+ return undefined;
125
+ };
126
+ return {
127
+ title: getProperty('dc:title'),
128
+ author: getProperty('dc:creator'),
129
+ subject: getProperty('dc:subject'),
130
+ creator: getProperty('cp:creator'),
131
+ };
132
+ }
133
+ /** Get comprehensive metadata. */
134
+ export async function getDocxMetadata(path) {
135
+ const zip = await loadDocx(path);
136
+ const paragraphs = await extractParagraphs(path);
137
+ const coreProps = await getCoreProperties(zip);
138
+ const fullText = paragraphs.map((p) => p.text).join(' ');
139
+ const wordCount = fullText.split(/\s+/).filter((w) => w.length > 0).length;
140
+ return { ...coreProps, paragraphCount: paragraphs.length, wordCount };
141
+ }
142
+ /** Extract body XML. */
143
+ export async function extractBodyXml(path) {
144
+ const inputBuf = await fs.readFile(path);
145
+ const zip = new PizZip(inputBuf);
146
+ const docFile = zip.file('word/document.xml');
147
+ if (!docFile)
148
+ throw new Error('Invalid DOCX: missing word/document.xml');
149
+ const xmlStr = docFile.asText();
150
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
151
+ const body = dom.getElementsByTagName('w:body').item(0);
152
+ if (!body)
153
+ throw new Error('Invalid DOCX: missing w:body');
154
+ return new XMLSerializer().serializeToString(body);
155
+ }
156
+ /** Read DOCX file with optional pagination. */
157
+ export async function readDocx(path, options) {
158
+ const zip = await loadDocx(path);
159
+ const docFile = zip.file('word/document.xml');
160
+ if (!docFile)
161
+ throw new Error('Invalid DOCX: missing word/document.xml');
162
+ const xmlStr = docFile.asText();
163
+ const dom = new DOMParser().parseFromString(xmlStr, 'application/xml');
164
+ const body = dom.getElementsByTagName('w:body').item(0);
165
+ if (!body)
166
+ throw new Error('Invalid DOCX: missing w:body');
167
+ const allParagraphs = [];
168
+ let index = 0;
169
+ for (const child of nodeListToArray(body.childNodes)) {
170
+ if (child.nodeType !== 1)
171
+ continue;
172
+ if (child.nodeName !== 'w:p')
173
+ continue;
174
+ const text = getParagraphText(child).trim();
175
+ allParagraphs.push({ index, text, hasText: text.length > 0 });
176
+ index++;
177
+ }
178
+ let paragraphs = allParagraphs;
179
+ if (options?.offset !== undefined || options?.length !== undefined) {
180
+ const offset = options.offset || 0;
181
+ const length = options.length !== undefined ? options.length : allParagraphs.length;
182
+ paragraphs = allParagraphs.slice(offset, offset + length);
183
+ }
184
+ const metadata = await getDocxMetadata(path);
185
+ const text = paragraphs.map((p) => p.text).join('\n\n');
186
+ const bodyXml = new XMLSerializer().serializeToString(body);
187
+ return { text, paragraphs, metadata, bodyXml };
188
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * DOCX relationship management — Single Responsibility: manage relationships
3
+ * in word/_rels/document.xml.rels and Content_Types.xml.
4
+ *
5
+ * Used for adding images, hyperlinks, and other external resources.
6
+ */
7
+ import PizZip from 'pizzip';
8
+ /**
9
+ * Add an image relationship to word/_rels/document.xml.rels and return the rId.
10
+ *
11
+ * @param zip The DOCX ZIP archive
12
+ * @param mediaFileName The filename in word/media/ (e.g., "image1.png")
13
+ * @returns The relationship ID (e.g., "rId1")
14
+ */
15
+ export declare function addImageRelationship(zip: PizZip, mediaFileName: string): string;
16
+ /**
17
+ * Ensure the Content_Types.xml has a Default entry for the given file extension.
18
+ *
19
+ * @param zip The DOCX ZIP archive
20
+ * @param ext The file extension (e.g., ".png")
21
+ */
22
+ export declare function ensureContentType(zip: PizZip, ext: string): void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DOCX relationship management — Single Responsibility: manage relationships
3
+ * in word/_rels/document.xml.rels and Content_Types.xml.
4
+ *
5
+ * Used for adding images, hyperlinks, and other external resources.
6
+ */
7
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
8
+ import { nodeListToArray } from './dom.js';
9
+ import { getMimeType } from './constants.js';
10
+ import { DOCX_PATHS, NAMESPACES } from './constants.js';
11
+ /**
12
+ * Add an image relationship to word/_rels/document.xml.rels and return the rId.
13
+ *
14
+ * @param zip The DOCX ZIP archive
15
+ * @param mediaFileName The filename in word/media/ (e.g., "image1.png")
16
+ * @returns The relationship ID (e.g., "rId1")
17
+ */
18
+ export function addImageRelationship(zip, mediaFileName) {
19
+ const relsPath = DOCX_PATHS.DOCUMENT_RELS;
20
+ let relsEntry = zip.file(relsPath);
21
+ // Create .rels file if it doesn't exist
22
+ if (!relsEntry) {
23
+ const emptyRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
24
+ `<Relationships xmlns="${NAMESPACES.RELS}"></Relationships>`;
25
+ zip.file(relsPath, emptyRels);
26
+ relsEntry = zip.file(relsPath);
27
+ }
28
+ const relsXml = relsEntry.asText();
29
+ const relsDom = new DOMParser().parseFromString(relsXml, 'application/xml');
30
+ const relationships = relsDom.getElementsByTagName('Relationship');
31
+ // Find max existing rId
32
+ let maxId = 0;
33
+ for (const rel of nodeListToArray(relationships)) {
34
+ const id = rel.getAttribute('Id') || '';
35
+ const match = id.match(/^rId(\d+)$/);
36
+ if (match) {
37
+ maxId = Math.max(maxId, parseInt(match[1], 10));
38
+ }
39
+ }
40
+ const newRId = `rId${maxId + 1}`;
41
+ // Create new Relationship element
42
+ const newRel = relsDom.createElement('Relationship');
43
+ newRel.setAttribute('Id', newRId);
44
+ newRel.setAttribute('Type', `${NAMESPACES.R}/image`);
45
+ newRel.setAttribute('Target', `media/${mediaFileName}`);
46
+ relsDom.documentElement.appendChild(newRel);
47
+ // Write back
48
+ const newRelsXml = new XMLSerializer().serializeToString(relsDom);
49
+ zip.file(relsPath, newRelsXml);
50
+ return newRId;
51
+ }
52
+ /**
53
+ * Ensure the Content_Types.xml has a Default entry for the given file extension.
54
+ *
55
+ * @param zip The DOCX ZIP archive
56
+ * @param ext The file extension (e.g., ".png")
57
+ */
58
+ export function ensureContentType(zip, ext) {
59
+ const ctPath = DOCX_PATHS.CONTENT_TYPES;
60
+ const ctEntry = zip.file(ctPath);
61
+ if (!ctEntry)
62
+ return;
63
+ const ctXml = ctEntry.asText();
64
+ const extNoDot = ext.replace(/^\./, '');
65
+ // Check if already present
66
+ if (ctXml.includes(`Extension="${extNoDot}"`))
67
+ return;
68
+ const ctDom = new DOMParser().parseFromString(ctXml, 'application/xml');
69
+ const types = ctDom.documentElement;
70
+ const defaultEl = ctDom.createElement('Default');
71
+ defaultEl.setAttribute('Extension', extNoDot);
72
+ defaultEl.setAttribute('ContentType', getMimeType(ext));
73
+ types.appendChild(defaultEl);
74
+ const newCtXml = new XMLSerializer().serializeToString(ctDom);
75
+ zip.file(ctPath, newCtXml);
76
+ }