@beyondwork/docx-react-component 1.0.1 → 1.0.2

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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +76 -46
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +320 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1504 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,351 @@
1
+ export interface XmlElementNode {
2
+ type: "element";
3
+ name: string;
4
+ attributes: Record<string, string>;
5
+ children: XmlNode[];
6
+ start: number;
7
+ end: number;
8
+ openingTagEnd: number;
9
+ closingTagStart: number;
10
+ }
11
+
12
+ export interface XmlTextNode {
13
+ type: "text";
14
+ text: string;
15
+ start: number;
16
+ end: number;
17
+ }
18
+
19
+ export type XmlNode = XmlElementNode | XmlTextNode;
20
+
21
+ export interface RevisionParagraphBoundary {
22
+ paragraphIndex: number;
23
+ start: number;
24
+ end: number;
25
+ boundaries: Map<number, number>;
26
+ paragraphStart: number;
27
+ paragraphStartTagEnd: number;
28
+ paragraphEndTagStart: number;
29
+ paragraphEnd: number;
30
+ paragraphPropertiesStart?: number;
31
+ paragraphPropertiesEnd?: number;
32
+ paragraphRunPropertiesStart?: number;
33
+ paragraphRunPropertiesEnd?: number;
34
+ }
35
+
36
+ export function mapRevisionBoundaries(
37
+ documentXml: string,
38
+ ): RevisionParagraphBoundary[] {
39
+ const root = parseXmlWithPositions(documentXml);
40
+ const documentElement = findRequiredChildElement(root, "document");
41
+ const bodyElement = findRequiredChildElement(documentElement, "body");
42
+ const paragraphs: RevisionParagraphBoundary[] = [];
43
+ let cursor = 0;
44
+ let paragraphIndex = -1;
45
+ let previousWasParagraph = false;
46
+
47
+ for (const child of bodyElement.children) {
48
+ if (child.type !== "element") {
49
+ continue;
50
+ }
51
+
52
+ if (localName(child.name) !== "p") {
53
+ cursor += 1;
54
+ previousWasParagraph = false;
55
+ continue;
56
+ }
57
+
58
+ if (previousWasParagraph) {
59
+ cursor += 1;
60
+ }
61
+ paragraphIndex += 1;
62
+ const boundaries = new Map<number, number>();
63
+ boundaries.set(cursor, child.openingTagEnd);
64
+
65
+ const paragraphProperties = findChildElement(child, "pPr");
66
+ const paragraphRunProperties = paragraphProperties
67
+ ? findChildElement(paragraphProperties, "rPr")
68
+ : undefined;
69
+
70
+ walkStoryNodesForBoundaries(
71
+ child.children,
72
+ boundaries,
73
+ () => cursor,
74
+ (next) => {
75
+ cursor = next;
76
+ },
77
+ );
78
+
79
+ boundaries.set(cursor, child.closingTagStart);
80
+ paragraphs.push({
81
+ paragraphIndex,
82
+ start: Math.min(...boundaries.keys()),
83
+ end: Math.max(...boundaries.keys()),
84
+ boundaries,
85
+ paragraphStart: child.start,
86
+ paragraphStartTagEnd: child.openingTagEnd,
87
+ paragraphEndTagStart: child.closingTagStart,
88
+ paragraphEnd: child.end,
89
+ ...(paragraphProperties
90
+ ? {
91
+ paragraphPropertiesStart: paragraphProperties.start,
92
+ paragraphPropertiesEnd: paragraphProperties.end,
93
+ }
94
+ : {}),
95
+ ...(paragraphRunProperties
96
+ ? {
97
+ paragraphRunPropertiesStart: paragraphRunProperties.start,
98
+ paragraphRunPropertiesEnd: paragraphRunProperties.end,
99
+ }
100
+ : {}),
101
+ });
102
+ previousWasParagraph = true;
103
+ }
104
+
105
+ return paragraphs;
106
+ }
107
+
108
+ function walkStoryNodesForBoundaries(
109
+ nodes: XmlNode[],
110
+ boundaries: Map<number, number>,
111
+ getCursor: () => number,
112
+ setCursor: (next: number) => void,
113
+ ): void {
114
+ for (const node of nodes) {
115
+ walkStoryNodeForBoundaries(node, boundaries, getCursor, setCursor);
116
+ }
117
+ }
118
+
119
+ function walkStoryNodeForBoundaries(
120
+ node: XmlNode,
121
+ boundaries: Map<number, number>,
122
+ getCursor: () => number,
123
+ setCursor: (next: number) => void,
124
+ ): void {
125
+ if (node.type !== "element") {
126
+ return;
127
+ }
128
+
129
+ switch (localName(node.name)) {
130
+ case "pPr":
131
+ return;
132
+ case "r":
133
+ boundaries.set(getCursor(), node.start);
134
+ for (const child of node.children) {
135
+ walkStoryNodeForBoundaries(child, boundaries, getCursor, setCursor);
136
+ }
137
+ boundaries.set(getCursor(), node.end);
138
+ return;
139
+ case "ins":
140
+ case "del":
141
+ case "moveFrom":
142
+ case "moveTo":
143
+ case "hyperlink":
144
+ case "smartTag":
145
+ case "sdtContent":
146
+ case "customXml":
147
+ for (const child of node.children) {
148
+ walkStoryNodeForBoundaries(child, boundaries, getCursor, setCursor);
149
+ }
150
+ return;
151
+ case "t":
152
+ case "delText":
153
+ case "instrText":
154
+ case "delInstrText": {
155
+ const text = node.children
156
+ .filter((child): child is XmlTextNode => child.type === "text")
157
+ .map((child) => child.text)
158
+ .join("");
159
+ if (text.length === 0) {
160
+ return;
161
+ }
162
+
163
+ if (!boundaries.has(getCursor())) {
164
+ boundaries.set(getCursor(), node.start);
165
+ }
166
+ setCursor(getCursor() + text.length);
167
+ boundaries.set(getCursor(), node.end);
168
+ return;
169
+ }
170
+ case "tab":
171
+ case "br":
172
+ case "cr":
173
+ if (!boundaries.has(getCursor())) {
174
+ boundaries.set(getCursor(), node.start);
175
+ }
176
+ setCursor(getCursor() + 1);
177
+ boundaries.set(getCursor(), node.end);
178
+ return;
179
+ default:
180
+ for (const child of node.children) {
181
+ walkStoryNodeForBoundaries(child, boundaries, getCursor, setCursor);
182
+ }
183
+ }
184
+ }
185
+
186
+ export function parseXmlWithPositions(xml: string): XmlElementNode {
187
+ const root: XmlElementNode = {
188
+ type: "element",
189
+ name: "__root__",
190
+ attributes: {},
191
+ children: [],
192
+ start: 0,
193
+ end: xml.length,
194
+ openingTagEnd: 0,
195
+ closingTagStart: xml.length,
196
+ };
197
+ const stack: XmlElementNode[] = [root];
198
+ let cursor = 0;
199
+
200
+ while (cursor < xml.length) {
201
+ if (xml.startsWith("<!--", cursor)) {
202
+ const end = xml.indexOf("-->", cursor);
203
+ cursor = end >= 0 ? end + 3 : xml.length;
204
+ continue;
205
+ }
206
+
207
+ if (xml.startsWith("<?", cursor)) {
208
+ const end = xml.indexOf("?>", cursor);
209
+ cursor = end >= 0 ? end + 2 : xml.length;
210
+ continue;
211
+ }
212
+
213
+ if (xml.startsWith("<![CDATA[", cursor)) {
214
+ const end = xml.indexOf("]]>", cursor);
215
+ const textEnd = end >= 0 ? end : xml.length;
216
+ stack[stack.length - 1]?.children.push({
217
+ type: "text",
218
+ text: xml.slice(cursor + 9, textEnd),
219
+ start: cursor,
220
+ end: textEnd,
221
+ });
222
+ cursor = end >= 0 ? end + 3 : xml.length;
223
+ continue;
224
+ }
225
+
226
+ if (xml[cursor] !== "<") {
227
+ const nextTag = xml.indexOf("<", cursor);
228
+ const end = nextTag >= 0 ? nextTag : xml.length;
229
+ const text = decodeXmlEntities(xml.slice(cursor, end));
230
+ if (text.length > 0) {
231
+ stack[stack.length - 1]?.children.push({
232
+ type: "text",
233
+ text,
234
+ start: cursor,
235
+ end,
236
+ });
237
+ }
238
+ cursor = end;
239
+ continue;
240
+ }
241
+
242
+ if (xml[cursor + 1] === "/") {
243
+ const tagEnd = xml.indexOf(">", cursor);
244
+ if (tagEnd < 0) {
245
+ throw new Error("Malformed XML: unterminated closing tag.");
246
+ }
247
+
248
+ const node = stack.pop();
249
+ if (!node || stack.length === 0) {
250
+ throw new Error("Malformed XML: unexpected closing tag.");
251
+ }
252
+
253
+ node.closingTagStart = cursor;
254
+ node.end = tagEnd + 1;
255
+ cursor = tagEnd + 1;
256
+ continue;
257
+ }
258
+
259
+ const tagEnd = xml.indexOf(">", cursor);
260
+ if (tagEnd < 0) {
261
+ throw new Error("Malformed XML: unterminated opening tag.");
262
+ }
263
+
264
+ const rawTag = xml.slice(cursor + 1, tagEnd);
265
+ const selfClosing = /\/\s*$/.test(rawTag);
266
+ const normalizedTag = selfClosing ? rawTag.replace(/\/\s*$/, "") : rawTag;
267
+ const parts = normalizedTag.trim().split(/\s+/, 1);
268
+ const name = parts[0];
269
+ if (!name) {
270
+ throw new Error("Malformed XML: empty tag name.");
271
+ }
272
+
273
+ const attributes = parseAttributes(normalizedTag.slice(name.length));
274
+ const node: XmlElementNode = {
275
+ type: "element",
276
+ name,
277
+ attributes,
278
+ children: [],
279
+ start: cursor,
280
+ end: selfClosing ? tagEnd + 1 : xml.length,
281
+ openingTagEnd: tagEnd + 1,
282
+ closingTagStart: tagEnd + 1,
283
+ };
284
+
285
+ stack[stack.length - 1]?.children.push(node);
286
+ cursor = tagEnd + 1;
287
+
288
+ if (!selfClosing) {
289
+ stack.push(node);
290
+ }
291
+ }
292
+
293
+ if (stack.length !== 1) {
294
+ throw new Error("Malformed XML: unclosed element.");
295
+ }
296
+
297
+ return root;
298
+ }
299
+
300
+ export function findRequiredChildElement(
301
+ node: XmlElementNode,
302
+ childLocalName: string,
303
+ ): XmlElementNode {
304
+ const child = findChildElement(node, childLocalName);
305
+ if (!child) {
306
+ throw new Error(`Expected <${childLocalName}> element.`);
307
+ }
308
+
309
+ return child;
310
+ }
311
+
312
+ export function findChildElement(
313
+ node: XmlElementNode,
314
+ childLocalName: string,
315
+ ): XmlElementNode | undefined {
316
+ return node.children.find(
317
+ (entry): entry is XmlElementNode =>
318
+ entry.type === "element" && localName(entry.name) === childLocalName,
319
+ );
320
+ }
321
+
322
+ export function localName(name: string): string {
323
+ const separatorIndex = name.indexOf(":");
324
+ return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
325
+ }
326
+
327
+ function parseAttributes(source: string): Record<string, string> {
328
+ const attributes: Record<string, string> = {};
329
+ const attributePattern = /([A-Za-z_][A-Za-z0-9:._-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
330
+
331
+ for (const match of source.matchAll(attributePattern)) {
332
+ const key = match[1];
333
+ if (!key) {
334
+ continue;
335
+ }
336
+
337
+ const rawValue = match[3] ?? match[4] ?? "";
338
+ attributes[key] = decodeXmlEntities(rawValue);
339
+ }
340
+
341
+ return attributes;
342
+ }
343
+
344
+ function decodeXmlEntities(value: string): string {
345
+ return value
346
+ .replace(/&lt;/g, "<")
347
+ .replace(/&gt;/g, ">")
348
+ .replace(/&quot;/g, "\"")
349
+ .replace(/&apos;/g, "'")
350
+ .replace(/&amp;/g, "&");
351
+ }
@@ -0,0 +1,3 @@
1
+ # IO OPC
2
+
3
+ Open Packaging Conventions package reading, part enumeration, and relationship handling belong here.
@@ -0,0 +1,166 @@
1
+ import type { EditorError } from "../../api/public-types.ts";
2
+
3
+ export const CORRUPT_PACKAGE_REASONS = [
4
+ "zip-structure",
5
+ "content-types-missing",
6
+ "main-document-missing",
7
+ "broken-relationship-graph",
8
+ "malformed-xml",
9
+ "unsupported-compression",
10
+ "unreadable-package",
11
+ ] as const;
12
+
13
+ export type CorruptPackageReason = (typeof CORRUPT_PACKAGE_REASONS)[number];
14
+
15
+ export interface CorruptPackageIssue {
16
+ reason: CorruptPackageReason;
17
+ message: string;
18
+ partPath?: string;
19
+ relationshipSourcePath?: string;
20
+ relationshipId?: string;
21
+ targetPartPath?: string;
22
+ details?: Record<string, unknown>;
23
+ }
24
+
25
+ export interface CorruptPackageErrorDetails extends Record<string, unknown> {
26
+ stage: "package";
27
+ reason: CorruptPackageReason;
28
+ diagnosticsToken: string;
29
+ partPath?: string;
30
+ relationshipSourcePath?: string;
31
+ relationshipId?: string;
32
+ targetPartPath?: string;
33
+ }
34
+
35
+ export function createCorruptPackageError(
36
+ issue: CorruptPackageIssue,
37
+ ): EditorError {
38
+ return {
39
+ errorId: `error:package_corrupt:${createCorruptPackageDiagnosticsToken(issue)}`,
40
+ code: "package_corrupt",
41
+ message: issue.message,
42
+ isFatal: true,
43
+ source: "import",
44
+ details: {
45
+ stage: "package",
46
+ reason: issue.reason,
47
+ diagnosticsToken: createCorruptPackageDiagnosticsToken(issue),
48
+ ...(issue.partPath ? { partPath: issue.partPath } : {}),
49
+ ...(issue.relationshipSourcePath
50
+ ? { relationshipSourcePath: issue.relationshipSourcePath }
51
+ : {}),
52
+ ...(issue.relationshipId ? { relationshipId: issue.relationshipId } : {}),
53
+ ...(issue.targetPartPath ? { targetPartPath: issue.targetPartPath } : {}),
54
+ ...(issue.details ?? {}),
55
+ } satisfies CorruptPackageErrorDetails,
56
+ };
57
+ }
58
+
59
+ export function createMissingPartIssue(partPath: string): CorruptPackageIssue {
60
+ return {
61
+ reason:
62
+ partPath === "/[Content_Types].xml"
63
+ ? "content-types-missing"
64
+ : partPath === "/word/document.xml"
65
+ ? "main-document-missing"
66
+ : "unreadable-package",
67
+ message: `DOCX package is missing required part ${partPath}.`,
68
+ partPath,
69
+ };
70
+ }
71
+
72
+ export function createBrokenRelationshipIssue(input: {
73
+ relationshipSourcePath: string | null;
74
+ relationshipId: string;
75
+ targetPartPath: string;
76
+ }): CorruptPackageIssue {
77
+ return {
78
+ reason: "broken-relationship-graph",
79
+ message:
80
+ input.relationshipSourcePath === null
81
+ ? `DOCX package relationship ${input.relationshipId} points to missing part ${input.targetPartPath}.`
82
+ : `DOCX part ${input.relationshipSourcePath} has unresolved relationship ${input.relationshipId} targeting ${input.targetPartPath}.`,
83
+ relationshipSourcePath: input.relationshipSourcePath ?? undefined,
84
+ relationshipId: input.relationshipId,
85
+ targetPartPath: input.targetPartPath,
86
+ };
87
+ }
88
+
89
+ export function classifyCorruptPackageError(error: unknown): CorruptPackageIssue {
90
+ const message =
91
+ error instanceof Error
92
+ ? error.message
93
+ : typeof error === "string"
94
+ ? error
95
+ : "DOCX package could not be read safely.";
96
+ const normalized = message.toLowerCase();
97
+
98
+ if (
99
+ normalized.includes("end of central directory") ||
100
+ normalized.includes("zip archive") ||
101
+ normalized.includes("central directory signature") ||
102
+ normalized.includes("local file header signature")
103
+ ) {
104
+ return {
105
+ reason: "zip-structure",
106
+ message,
107
+ };
108
+ }
109
+
110
+ if (normalized.includes("compression")) {
111
+ return {
112
+ reason: "unsupported-compression",
113
+ message,
114
+ };
115
+ }
116
+
117
+ if (normalized.includes("relationship")) {
118
+ return {
119
+ reason: "broken-relationship-graph",
120
+ message,
121
+ };
122
+ }
123
+
124
+ if (normalized.includes("/[content_types].xml")) {
125
+ return createMissingPartIssue("/[Content_Types].xml");
126
+ }
127
+
128
+ if (normalized.includes("/word/document.xml")) {
129
+ return createMissingPartIssue("/word/document.xml");
130
+ }
131
+
132
+ if (normalized.includes("xml")) {
133
+ return {
134
+ reason: "malformed-xml",
135
+ message,
136
+ };
137
+ }
138
+
139
+ return {
140
+ reason: "unreadable-package",
141
+ message,
142
+ };
143
+ }
144
+
145
+ export function createCorruptPackageDiagnosticsToken(
146
+ issue: CorruptPackageIssue,
147
+ ): string {
148
+ const fields = [
149
+ issue.reason,
150
+ issue.message,
151
+ issue.partPath ?? "",
152
+ issue.relationshipSourcePath ?? "",
153
+ issue.relationshipId ?? "",
154
+ issue.targetPartPath ?? "",
155
+ ];
156
+
157
+ let hash = 2166136261;
158
+ for (const field of fields) {
159
+ for (let index = 0; index < field.length; index += 1) {
160
+ hash ^= field.charCodeAt(index);
161
+ hash = Math.imul(hash, 16777619) >>> 0;
162
+ }
163
+ }
164
+
165
+ return `pkg-${hash.toString(16).padStart(8, "0")}`;
166
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ getRelationshipsPartPath,
3
+ type OpcPackageManifest,
4
+ type OpcPackagePart,
5
+ } from "../ooxml/part-manifest.ts";
6
+ import { writeOpcPackage } from "./package-writer.ts";
7
+
8
+ export const DOCX_MIME_TYPE =
9
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
10
+ export const DOCX_DOCUMENT_CONTENT_TYPE =
11
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
12
+ export const DOCX_DOCUMENT_RELATIONSHIP_TYPE =
13
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
14
+
15
+ interface WriteDocxPackageInput {
16
+ documentXml: string;
17
+ documentContentType?: string;
18
+ packageRelationshipType?: string;
19
+ }
20
+
21
+ export function writeDocxPackage(input: WriteDocxPackageInput): Uint8Array {
22
+ const documentPath = "/word/document.xml";
23
+ const contentType = input.documentContentType ?? DOCX_DOCUMENT_CONTENT_TYPE;
24
+ const relationshipType =
25
+ input.packageRelationshipType ?? DOCX_DOCUMENT_RELATIONSHIP_TYPE;
26
+ const documentBytes = new TextEncoder().encode(input.documentXml);
27
+
28
+ const documentPart: OpcPackagePart = {
29
+ path: documentPath,
30
+ surfaceKind: "content",
31
+ contentType,
32
+ relationshipsPartPath: getRelationshipsPartPath(documentPath),
33
+ relationships: [],
34
+ compression: "deflate",
35
+ bytes: documentBytes,
36
+ crc32: 0,
37
+ };
38
+
39
+ const manifest: OpcPackageManifest = {
40
+ contentTypes: {
41
+ defaults: {
42
+ rels: "application/vnd.openxmlformats-package.relationships+xml",
43
+ xml: "application/xml",
44
+ },
45
+ overrides: {
46
+ [documentPath]: contentType,
47
+ },
48
+ },
49
+ packageRelationships: [
50
+ {
51
+ id: "rId1",
52
+ type: relationshipType,
53
+ target: "word/document.xml",
54
+ targetMode: "internal",
55
+ },
56
+ ],
57
+ parts: [
58
+ {
59
+ path: documentPart.path,
60
+ surfaceKind: documentPart.surfaceKind,
61
+ contentType: documentPart.contentType,
62
+ relationshipsPartPath: documentPart.relationshipsPartPath,
63
+ relationships: documentPart.relationships,
64
+ compression: documentPart.compression,
65
+ byteLength: documentPart.bytes.byteLength,
66
+ },
67
+ ],
68
+ };
69
+
70
+ return writeOpcPackage({
71
+ manifest,
72
+ parts: new Map([[documentPart.path, documentPart]]),
73
+ });
74
+ }