@opengeni/artifact-tool 0.2.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 (203) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +104 -0
  3. package/dist/bounded-zip.d.ts +28 -0
  4. package/dist/chunk-4BSENMAE.js +39 -0
  5. package/dist/chunk-4BSENMAE.js.map +1 -0
  6. package/dist/chunk-4FRFNNBM.js +276 -0
  7. package/dist/chunk-4FRFNNBM.js.map +1 -0
  8. package/dist/chunk-5XIA6KHZ.js +49 -0
  9. package/dist/chunk-5XIA6KHZ.js.map +1 -0
  10. package/dist/chunk-66CT2ZRP.js +2229 -0
  11. package/dist/chunk-66CT2ZRP.js.map +1 -0
  12. package/dist/chunk-72SYDU4O.js +319 -0
  13. package/dist/chunk-72SYDU4O.js.map +1 -0
  14. package/dist/chunk-B2ZJRZDA.js +279 -0
  15. package/dist/chunk-B2ZJRZDA.js.map +1 -0
  16. package/dist/chunk-DCTLTNPW.js +2458 -0
  17. package/dist/chunk-DCTLTNPW.js.map +1 -0
  18. package/dist/chunk-FGZM4FQ6.js +473 -0
  19. package/dist/chunk-FGZM4FQ6.js.map +1 -0
  20. package/dist/chunk-HTJBLBDE.js +591 -0
  21. package/dist/chunk-HTJBLBDE.js.map +1 -0
  22. package/dist/chunk-IYFXZQ34.js +5306 -0
  23. package/dist/chunk-IYFXZQ34.js.map +1 -0
  24. package/dist/chunk-J6N7IR44.js +172 -0
  25. package/dist/chunk-J6N7IR44.js.map +1 -0
  26. package/dist/chunk-N2IHNWBS.js +1002 -0
  27. package/dist/chunk-N2IHNWBS.js.map +1 -0
  28. package/dist/chunk-NAG44K67.js +549 -0
  29. package/dist/chunk-NAG44K67.js.map +1 -0
  30. package/dist/chunk-O24KTEGP.js +33 -0
  31. package/dist/chunk-O24KTEGP.js.map +1 -0
  32. package/dist/chunk-PI2AZVGK.js +2074 -0
  33. package/dist/chunk-PI2AZVGK.js.map +1 -0
  34. package/dist/chunk-Q2FFHEY7.js +40 -0
  35. package/dist/chunk-Q2FFHEY7.js.map +1 -0
  36. package/dist/chunk-RBGQADXA.js +57 -0
  37. package/dist/chunk-RBGQADXA.js.map +1 -0
  38. package/dist/chunk-U5G2BKM2.js +613 -0
  39. package/dist/chunk-U5G2BKM2.js.map +1 -0
  40. package/dist/chunk-VOJHMSM3.js +39 -0
  41. package/dist/chunk-VOJHMSM3.js.map +1 -0
  42. package/dist/chunk-VUNV25KB.js +16 -0
  43. package/dist/chunk-VUNV25KB.js.map +1 -0
  44. package/dist/chunk-XIFUNVBO.js +101 -0
  45. package/dist/chunk-XIFUNVBO.js.map +1 -0
  46. package/dist/chunk-ZCJYRJGB.js +489 -0
  47. package/dist/chunk-ZCJYRJGB.js.map +1 -0
  48. package/dist/chunk-ZZXPEE4S.js +2260 -0
  49. package/dist/chunk-ZZXPEE4S.js.map +1 -0
  50. package/dist/document-docx-api.d.ts +49 -0
  51. package/dist/document-docx-codec.d.ts +9 -0
  52. package/dist/document-docx-codec.js +4765 -0
  53. package/dist/document-docx-codec.js.map +1 -0
  54. package/dist/document-docx-import.d.ts +261 -0
  55. package/dist/document-docx-packer.d.ts +18 -0
  56. package/dist/document-docx-state.d.ts +12 -0
  57. package/dist/document-render.d.ts +3 -0
  58. package/dist/document-render.js +492 -0
  59. package/dist/document-render.js.map +1 -0
  60. package/dist/document.d.ts +432 -0
  61. package/dist/document.js +37 -0
  62. package/dist/document.js.map +1 -0
  63. package/dist/errors.d.ts +23 -0
  64. package/dist/file-blob.d.ts +11 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +186 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspection.d.ts +12 -0
  69. package/dist/kernel.d.ts +101 -0
  70. package/dist/lib-GQLPACFK.js +9746 -0
  71. package/dist/lib-GQLPACFK.js.map +1 -0
  72. package/dist/materializer-cli-entry.d.ts +2 -0
  73. package/dist/materializer-cli-entry.js +29 -0
  74. package/dist/materializer-cli-entry.js.map +1 -0
  75. package/dist/materializer-cli.d.ts +24 -0
  76. package/dist/materializer-cli.js +24 -0
  77. package/dist/materializer-cli.js.map +1 -0
  78. package/dist/native-raster.d.ts +10 -0
  79. package/dist/native.d.ts +129 -0
  80. package/dist/native.js +31 -0
  81. package/dist/native.js.map +1 -0
  82. package/dist/office-import.d.ts +48 -0
  83. package/dist/office-import.js +154 -0
  84. package/dist/office-import.js.map +1 -0
  85. package/dist/presentation-pptx-api.d.ts +60 -0
  86. package/dist/presentation-pptx-import.d.ts +5 -0
  87. package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
  88. package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
  89. package/dist/presentation-pptx-state-digest.d.ts +3 -0
  90. package/dist/presentation-pptx-state.d.ts +4 -0
  91. package/dist/presentation-pptx-xml.d.ts +29 -0
  92. package/dist/presentation-pptx.d.ts +5 -0
  93. package/dist/presentation-pptx.js +2365 -0
  94. package/dist/presentation-pptx.js.map +1 -0
  95. package/dist/presentation-render.d.ts +29 -0
  96. package/dist/presentation-render.js +375 -0
  97. package/dist/presentation-render.js.map +1 -0
  98. package/dist/presentation.d.ts +565 -0
  99. package/dist/presentation.js +71 -0
  100. package/dist/presentation.js.map +1 -0
  101. package/dist/production-composite.d.ts +68 -0
  102. package/dist/production-document.d.ts +2 -0
  103. package/dist/production-document.js +176 -0
  104. package/dist/production-document.js.map +1 -0
  105. package/dist/production-native-codecs.d.ts +94 -0
  106. package/dist/production-presentation.d.ts +2 -0
  107. package/dist/production-presentation.js +176 -0
  108. package/dist/production-presentation.js.map +1 -0
  109. package/dist/production-sha256.d.ts +2 -0
  110. package/dist/production-spreadsheet-native.d.ts +12 -0
  111. package/dist/production-spreadsheet.d.ts +2 -0
  112. package/dist/production-spreadsheet.js +176 -0
  113. package/dist/production-spreadsheet.js.map +1 -0
  114. package/dist/production.d.ts +96 -0
  115. package/dist/raster-image.d.ts +34 -0
  116. package/dist/reference.d.ts +17 -0
  117. package/dist/reference.js +574 -0
  118. package/dist/reference.js.map +1 -0
  119. package/dist/runtime-cli-entry.d.ts +2 -0
  120. package/dist/runtime-cli-entry.js +21 -0
  121. package/dist/runtime-cli-entry.js.map +1 -0
  122. package/dist/runtime-cli.d.ts +76 -0
  123. package/dist/runtime-cli.js +26 -0
  124. package/dist/runtime-cli.js.map +1 -0
  125. package/dist/runtime-development.d.ts +73 -0
  126. package/dist/runtime-development.js +30 -0
  127. package/dist/runtime-development.js.map +1 -0
  128. package/dist/runtime-receipt.d.ts +19 -0
  129. package/dist/runtime.d.ts +355 -0
  130. package/dist/runtime.js +42 -0
  131. package/dist/runtime.js.map +1 -0
  132. package/dist/snapshot.d.ts +26 -0
  133. package/dist/snapshot.js +61 -0
  134. package/dist/snapshot.js.map +1 -0
  135. package/dist/spreadsheet-address.d.ts +18 -0
  136. package/dist/spreadsheet-file.d.ts +17 -0
  137. package/dist/spreadsheet-formula.d.ts +67 -0
  138. package/dist/spreadsheet-image.d.ts +17 -0
  139. package/dist/spreadsheet-render.d.ts +5 -0
  140. package/dist/spreadsheet-render.js +752 -0
  141. package/dist/spreadsheet-render.js.map +1 -0
  142. package/dist/spreadsheet-types.d.ts +155 -0
  143. package/dist/spreadsheet-xlsx-api.d.ts +88 -0
  144. package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
  145. package/dist/spreadsheet-xlsx-codec.js +16 -0
  146. package/dist/spreadsheet-xlsx-codec.js.map +1 -0
  147. package/dist/spreadsheet-xlsx-state.d.ts +16 -0
  148. package/dist/spreadsheet.d.ts +724 -0
  149. package/dist/spreadsheet.js +67 -0
  150. package/dist/spreadsheet.js.map +1 -0
  151. package/package.json +129 -0
  152. package/src/bounded-zip.ts +450 -0
  153. package/src/document-docx-api.ts +64 -0
  154. package/src/document-docx-codec.ts +2253 -0
  155. package/src/document-docx-import.ts +3814 -0
  156. package/src/document-docx-packer.ts +72 -0
  157. package/src/document-docx-state.ts +59 -0
  158. package/src/document-render.ts +615 -0
  159. package/src/document.ts +2800 -0
  160. package/src/errors.ts +45 -0
  161. package/src/file-blob.ts +43 -0
  162. package/src/index.ts +4 -0
  163. package/src/inspection.ts +50 -0
  164. package/src/kernel.ts +525 -0
  165. package/src/materializer-cli-entry.ts +11 -0
  166. package/src/materializer-cli.ts +743 -0
  167. package/src/native-raster.ts +141 -0
  168. package/src/native.ts +390 -0
  169. package/src/office-import.ts +243 -0
  170. package/src/presentation-pptx-api.ts +107 -0
  171. package/src/presentation-pptx-import.ts +2065 -0
  172. package/src/presentation-pptx-state-digest.ts +46 -0
  173. package/src/presentation-pptx-state.ts +18 -0
  174. package/src/presentation-pptx-xml.ts +473 -0
  175. package/src/presentation-pptx.ts +447 -0
  176. package/src/presentation-render.ts +532 -0
  177. package/src/presentation.ts +2599 -0
  178. package/src/production-composite.ts +805 -0
  179. package/src/production-document.ts +2 -0
  180. package/src/production-native-codecs.ts +1141 -0
  181. package/src/production-presentation.ts +2 -0
  182. package/src/production-sha256.ts +150 -0
  183. package/src/production-spreadsheet-native.ts +686 -0
  184. package/src/production-spreadsheet.ts +2 -0
  185. package/src/production.ts +1022 -0
  186. package/src/raster-image.ts +586 -0
  187. package/src/reference.ts +17 -0
  188. package/src/runtime-cli-entry.ts +13 -0
  189. package/src/runtime-cli.ts +670 -0
  190. package/src/runtime-development.ts +689 -0
  191. package/src/runtime-receipt.ts +169 -0
  192. package/src/runtime.ts +1463 -0
  193. package/src/snapshot.ts +88 -0
  194. package/src/spreadsheet-address.ts +90 -0
  195. package/src/spreadsheet-file.ts +71 -0
  196. package/src/spreadsheet-formula.ts +1212 -0
  197. package/src/spreadsheet-image.ts +242 -0
  198. package/src/spreadsheet-render.ts +985 -0
  199. package/src/spreadsheet-types.ts +171 -0
  200. package/src/spreadsheet-xlsx-api.ts +123 -0
  201. package/src/spreadsheet-xlsx-codec.ts +3063 -0
  202. package/src/spreadsheet-xlsx-state.ts +191 -0
  203. package/src/spreadsheet.ts +5195 -0
@@ -0,0 +1,2253 @@
1
+ import { UnsupportedArtifactFeatureError } from "./errors";
2
+ import { FileBlob } from "./file-blob";
3
+ import { packDocxWithBoundedCompression } from "./document-docx-packer";
4
+ import {
5
+ DOCX_MEDIA_TYPE,
6
+ DocumentFidelityError,
7
+ type DocumentDocxExportOptions,
8
+ type DocumentDocxImportOptions,
9
+ type DocumentFidelityIssue,
10
+ type DocumentLossPreservationEnvelope,
11
+ type DocumentOpaqueContent,
12
+ } from "./document-docx-api";
13
+ import {
14
+ DOCUMENT_LOSS_PRESERVATION,
15
+ documentFidelityReport,
16
+ documentLossPreservationEnvelope,
17
+ } from "./document-docx-state";
18
+ import {
19
+ DocxImportError,
20
+ importDocx as importDocxDto,
21
+ type ImportedBlock,
22
+ type ImportedComment,
23
+ type ImportedDocument,
24
+ type ImportedHeaderFooter,
25
+ type ImportedBorder,
26
+ type ImportedBoxMeasures,
27
+ type ImportedParagraph,
28
+ type ImportedParagraphStyle,
29
+ type ImportedRunStyle,
30
+ type ImportedSection,
31
+ type ImportedStyle,
32
+ type ImportedTable,
33
+ } from "./document-docx-import";
34
+ import {
35
+ Document,
36
+ DocumentCommentThread,
37
+ DocumentPageBreak,
38
+ DocumentParagraph,
39
+ DocumentTable,
40
+ DocumentTextRun,
41
+ TrackedChange,
42
+ type DocumentBlock,
43
+ type DocumentPageGeometry,
44
+ type DocumentParagraphStyle,
45
+ type DocumentSection,
46
+ type DocumentSectionStories,
47
+ type DocumentStory,
48
+ type DocumentStoryBlock,
49
+ type DocumentTableStyle,
50
+ type DocumentTextStyle,
51
+ } from "./document";
52
+
53
+ export {
54
+ DOCX_MEDIA_TYPE,
55
+ DocumentFidelityError,
56
+ type DocumentDocxExportOptions,
57
+ type DocumentDocxImportOptions,
58
+ type DocumentFidelityIssue,
59
+ type DocumentLossPreservationEnvelope,
60
+ } from "./document-docx-api";
61
+ type ParagraphProjection = { source: ImportedParagraph; target: DocumentParagraph };
62
+ type ParagraphProjectionMap = Map<string, ParagraphProjection[]>;
63
+ type ResolvedImportedStyle = { paragraph: ImportedParagraphStyle; run: ImportedRunStyle };
64
+ const importedStyleCache = new WeakMap<object, Map<string, ResolvedImportedStyle>>();
65
+
66
+ export async function exportDocx(
67
+ document: Document,
68
+ options: DocumentDocxExportOptions = {},
69
+ ): Promise<FileBlob> {
70
+ validateExportOptions(options);
71
+ document.toJSON();
72
+ const envelope = DOCUMENT_LOSS_PRESERVATION.get(document);
73
+ if (envelope && (await documentModelDigest(document)) === envelope.modelDigest) {
74
+ return FileBlob.fromBytes(envelope.sourceBytes.slice(), {
75
+ type: DOCX_MEDIA_TYPE,
76
+ ...(options.fileName ? { name: options.fileName } : {}),
77
+ });
78
+ }
79
+ const blockingIssues = documentFidelityReport(document).filter(
80
+ (issue) => issue.severity === "error",
81
+ );
82
+ if (blockingIssues.length > 0 && options.unsupportedContent !== "discard") {
83
+ throw new DocumentFidelityError(
84
+ 'DOCX export would discard opaque OOXML; export again with unsupportedContent: "discard" only after explicit user acknowledgement',
85
+ blockingIssues,
86
+ );
87
+ }
88
+ const docx = await import("docx");
89
+ const context = buildDocxContext(document, docx);
90
+ const evenAndOddHeaderAndFooters = document.evenAndOddHeaders;
91
+ const sections = document.sections.items.map((section, index) => {
92
+ const end = document.sections.items[index + 1]?.startBlockIndex ?? document.blocks.items.length;
93
+ const blocks = document.blocks.items.slice(section.startBlockIndex, end);
94
+ const landscape = section.page.widthPt > section.page.heightPt;
95
+ return {
96
+ properties: {
97
+ page: {
98
+ size: {
99
+ width: ptToTwip(landscape ? section.page.heightPt : section.page.widthPt),
100
+ height: ptToTwip(landscape ? section.page.widthPt : section.page.heightPt),
101
+ orientation: landscape ? docx.PageOrientation.LANDSCAPE : docx.PageOrientation.PORTRAIT,
102
+ },
103
+ margin: {
104
+ top: ptToTwip(section.page.marginTopPt),
105
+ right: ptToTwip(section.page.marginRightPt),
106
+ bottom: ptToTwip(section.page.marginBottomPt),
107
+ left: ptToTwip(section.page.marginLeftPt),
108
+ header: ptToTwip(section.page.headerPt ?? 36),
109
+ footer: ptToTwip(section.page.footerPt ?? 36),
110
+ gutter: ptToTwip(section.page.gutterPt ?? 0),
111
+ },
112
+ },
113
+ titlePage: section.titlePage,
114
+ },
115
+ headers: buildDocxStories(section.headers, section.page, docx, context),
116
+ footers: buildDocxStories(section.footers, section.page, docx, context),
117
+ children: blocks.map((block) => toDocxBlock(block, section.page, docx, context)),
118
+ };
119
+ });
120
+ const file = new docx.Document({
121
+ sections,
122
+ numbering: { config: numberingConfig(docx) },
123
+ comments: { children: context.commentDefinitions },
124
+ features: { trackRevisions: document.trackRevisions },
125
+ evenAndOddHeaderAndFooters,
126
+ creator: "OpenGeni",
127
+ lastModifiedBy: "OpenGeni",
128
+ revision: document.revision,
129
+ });
130
+ const bytes = await packDocxWithBoundedCompression(file, docx.Packer, [
131
+ {
132
+ path: "docProps/core.xml",
133
+ data: deterministicCoreProperties(document.revision),
134
+ },
135
+ ]);
136
+ return FileBlob.fromBytes(bytes, {
137
+ type: DOCX_MEDIA_TYPE,
138
+ ...(options.fileName ? { name: options.fileName } : {}),
139
+ });
140
+ }
141
+
142
+ export async function importDocx(
143
+ input: FileBlob | Blob | ArrayBuffer,
144
+ options: DocumentDocxImportOptions = {},
145
+ ): Promise<Document> {
146
+ validateImportOptions(options);
147
+ const sourceBytes = await ownedDocxBytes(input);
148
+ const imported = await importDocxDto(ownedArrayBuffer(sourceBytes), options.limits);
149
+ if (hasOpaqueContent(imported.opaqueContent) && options.unsupportedContent === "error") {
150
+ throw new DocumentFidelityError(
151
+ "The DOCX contains bounded inert OOXML outside the editable document model",
152
+ [opaqueContentIssue(imported.opaqueContent, "error")],
153
+ );
154
+ }
155
+ const document = documentFromImportedDocx(imported, await semanticNamespace(imported));
156
+ if (hasOpaqueContent(imported.opaqueContent)) {
157
+ DOCUMENT_LOSS_PRESERVATION.set(document, {
158
+ sourceBytes,
159
+ sourceDigest: await sha256Hex(sourceBytes),
160
+ importedRevision: document.revision,
161
+ opaqueContent: cloneOpaqueContent(imported.opaqueContent),
162
+ modelDigest: await documentModelDigest(document),
163
+ });
164
+ }
165
+ return document;
166
+ }
167
+
168
+ export function fidelityReport(document: Document): readonly DocumentFidelityIssue[] {
169
+ document.toJSON();
170
+ return documentFidelityReport(document);
171
+ }
172
+
173
+ export function lossPreservationEnvelope(
174
+ document: Document,
175
+ ): DocumentLossPreservationEnvelope | null {
176
+ document.toJSON();
177
+ return documentLossPreservationEnvelope(document);
178
+ }
179
+
180
+ export async function attachLossPreservationEnvelope(
181
+ document: Document,
182
+ envelope: DocumentLossPreservationEnvelope,
183
+ options: Pick<DocumentDocxImportOptions, "limits"> = {},
184
+ ): Promise<void> {
185
+ document.toJSON();
186
+ validateAttachOptions(options);
187
+ const normalized = normalizeLossPreservationEnvelope(envelope);
188
+ if (normalized.sourceDigest !== (await sha256Hex(normalized.sourceBytes))) {
189
+ throw new DocxImportError(
190
+ "invalid_package",
191
+ "DOCX loss-preservation source digest does not match its bytes",
192
+ );
193
+ }
194
+ const imported = await importDocxDto(ownedArrayBuffer(normalized.sourceBytes), options.limits);
195
+ if (!sameOpaqueContent(imported.opaqueContent, normalized.opaqueContent)) {
196
+ throw new DocxImportError(
197
+ "invalid_package",
198
+ "DOCX loss-preservation metadata does not match its source package",
199
+ );
200
+ }
201
+ const projected = documentFromImportedDocx(imported, await semanticNamespace(imported));
202
+ const projectedDigest = await documentModelDigest(projected);
203
+ const documentDigest = await documentModelDigest(document);
204
+ if (projectedDigest !== normalized.modelDigest || documentDigest !== normalized.modelDigest) {
205
+ throw new DocxImportError(
206
+ "invalid_package",
207
+ "DOCX loss-preservation envelope belongs to a different document snapshot",
208
+ );
209
+ }
210
+ DOCUMENT_LOSS_PRESERVATION.set(document, {
211
+ sourceBytes: normalized.sourceBytes,
212
+ sourceDigest: normalized.sourceDigest,
213
+ importedRevision: document.revision,
214
+ opaqueContent: normalized.opaqueContent,
215
+ modelDigest: normalized.modelDigest,
216
+ });
217
+ }
218
+
219
+ function documentFromImportedDocx(imported: ImportedDocument, idNamespace: string): Document {
220
+ if (imported.schemaVersion !== 1 || imported.format !== "docx") {
221
+ throw new UnsupportedArtifactFeatureError(
222
+ "document",
223
+ "unknown imported DOCX schema",
224
+ "DOCX import",
225
+ );
226
+ }
227
+ const importedSections = [...imported.sections].sort(
228
+ (left, right) => left.startBlockIndex - right.startBlockIndex,
229
+ );
230
+ validateImportedSections(imported, importedSections);
231
+ const firstSection = importedSections[0];
232
+ if (!firstSection || firstSection.startBlockIndex !== 0)
233
+ throw new Error("Imported DOCX has no section starting at body block 0");
234
+ const document = Document.create({
235
+ page: importedPage(firstSection),
236
+ idNamespace,
237
+ titlePage: firstSection.titlePage,
238
+ evenAndOddHeaders: imported.evenAndOddHeaders,
239
+ trackRevisions: imported.trackRevisions,
240
+ });
241
+ const paragraphMap: ParagraphProjectionMap = new Map();
242
+ const styleMap = new Map(imported.styles.map((style) => [style.styleId, style]));
243
+ const listKinds = importedListKinds(imported.lists);
244
+ let sectionIndex = 1;
245
+ imported.blocks.forEach((block, blockIndex) => {
246
+ while (importedSections[sectionIndex]?.startBlockIndex === blockIndex) {
247
+ document.sections.add({
248
+ page: importedPage(importedSections[sectionIndex]!),
249
+ titlePage: importedSections[sectionIndex]!.titlePage,
250
+ });
251
+ sectionIndex += 1;
252
+ }
253
+ appendImportedBodyBlock(document, block, styleMap, listKinds, paragraphMap);
254
+ });
255
+ while (sectionIndex < importedSections.length) {
256
+ const section = importedSections[sectionIndex]!;
257
+ if (section.startBlockIndex !== imported.blocks.length)
258
+ throw new Error("Imported DOCX section boundary does not align with body blocks");
259
+ if (document.sections.items.at(-1)?.startBlockIndex === imported.blocks.length) {
260
+ throw new UnsupportedArtifactFeatureError(
261
+ "document",
262
+ "multiple empty sections at the same boundary",
263
+ "DOCX import",
264
+ );
265
+ }
266
+ document.sections.add({ page: importedPage(section), titlePage: section.titlePage });
267
+ sectionIndex += 1;
268
+ }
269
+ const headers = new Map(imported.headers.map((story) => [story.partName, story]));
270
+ const footers = new Map(imported.footers.map((story) => [story.partName, story]));
271
+ importedSections.forEach((source, index) => {
272
+ const target = document.sections.items[index];
273
+ if (!target) throw new Error("Imported DOCX section count changed during conversion");
274
+ importSectionStories(
275
+ document,
276
+ source,
277
+ target,
278
+ headers,
279
+ footers,
280
+ styleMap,
281
+ listKinds,
282
+ paragraphMap,
283
+ );
284
+ });
285
+ importComments(document, imported.comments, paragraphMap, styleMap);
286
+ importTrackedChanges(document, imported, paragraphMap);
287
+ validateImportedReviewProjection(document);
288
+ document.toJSON();
289
+ return document;
290
+ }
291
+
292
+ function appendImportedBodyBlock(
293
+ document: Document,
294
+ block: ImportedBlock,
295
+ styles: ReadonlyMap<string, ImportedStyle>,
296
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
297
+ paragraphs: ParagraphProjectionMap,
298
+ ): void {
299
+ if (block.kind === "pageBreak") {
300
+ document.blocks.addPageBreak();
301
+ return;
302
+ }
303
+ if (block.kind === "paragraph") {
304
+ if (isOnlyPageBreak(block, styles)) {
305
+ document.blocks.addPageBreak();
306
+ return;
307
+ }
308
+ const projected = importedParagraph(block, styles, listKinds);
309
+ const paragraph = document.blocks.addParagraph(projected.runs, projected.style);
310
+ appendMap(paragraphs, block.id, { source: block, target: paragraph });
311
+ return;
312
+ }
313
+ const projected = importedTable(
314
+ block,
315
+ styles,
316
+ listKinds,
317
+ document.sections.items.at(-1)?.page ?? document.page,
318
+ );
319
+ document.blocks.addTable(projected.rows, projected.style);
320
+ }
321
+
322
+ function appendImportedStoryBlock(
323
+ story: DocumentStory,
324
+ block: ImportedBlock,
325
+ document: Document,
326
+ styles: ReadonlyMap<string, ImportedStyle>,
327
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
328
+ paragraphs: ParagraphProjectionMap,
329
+ page: DocumentPageGeometry,
330
+ ): void {
331
+ if (
332
+ block.kind === "pageBreak" ||
333
+ (block.kind === "paragraph" && block.inlines.some((inline) => inline.kind === "pageBreak"))
334
+ ) {
335
+ throw new UnsupportedArtifactFeatureError(
336
+ "document",
337
+ "page breaks in headers or footers",
338
+ "DOCX import",
339
+ );
340
+ }
341
+ if (block.kind === "paragraph") {
342
+ const projected = importedParagraph(block, styles, listKinds);
343
+ const paragraph = story.addParagraph(projected.runs, projected.style);
344
+ appendMap(paragraphs, block.id, { source: block, target: paragraph });
345
+ } else {
346
+ const projected = importedTable(block, styles, listKinds, page);
347
+ story.addTable(projected.rows, projected.style);
348
+ }
349
+ }
350
+
351
+ function importedParagraph(
352
+ source: ImportedParagraph,
353
+ styles: ReadonlyMap<string, ImportedStyle>,
354
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
355
+ ): { runs: readonly DocumentTextRun[] | string; style: DocumentParagraphStyle } {
356
+ if (source.inlines.some((inline) => inline.kind === "pageBreak")) {
357
+ throw new UnsupportedArtifactFeatureError(
358
+ "document",
359
+ "inline page breaks mixed with paragraph text",
360
+ "DOCX import",
361
+ );
362
+ }
363
+ const inherited = resolveImportedStyle(source.styleId, styles, "paragraph");
364
+ const paragraphStyle = mapImportedParagraphStyle(
365
+ { ...inherited.paragraph, ...source.style },
366
+ source.styleId,
367
+ listKinds,
368
+ );
369
+ const runs = source.inlines.map((inline) => {
370
+ if (inline.kind !== "run") throw new Error("Unexpected non-run in imported paragraph");
371
+ const inheritedRun = resolveImportedStyle(inline.styleId, styles, "character").run;
372
+ return new DocumentTextRun(
373
+ inline.text,
374
+ mapImportedRunStyle({ ...inherited.run, ...inheritedRun, ...inline.style }),
375
+ );
376
+ });
377
+ return { runs: runs.length > 0 ? runs : "", style: paragraphStyle };
378
+ }
379
+
380
+ function importedTable(
381
+ source: ImportedTable,
382
+ styles: ReadonlyMap<string, ImportedStyle>,
383
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
384
+ page: DocumentPageGeometry,
385
+ ): {
386
+ rows: ReadonlyArray<ReadonlyArray<readonly DocumentTextRun[]>>;
387
+ style: DocumentTableStyle;
388
+ } {
389
+ if (source.rows.length === 0 || source.rows.some((row) => row.cells.length === 0))
390
+ throw new Error("Imported DOCX table is empty");
391
+ if (source.styleId) {
392
+ throw new UnsupportedArtifactFeatureError(
393
+ "document",
394
+ `table style ${source.styleId}`,
395
+ "DOCX import",
396
+ );
397
+ }
398
+ const columnCount = source.rows[0]!.cells.length;
399
+ if (source.rows.some((row) => row.cells.length !== columnCount)) {
400
+ throw new UnsupportedArtifactFeatureError("document", "ragged DOCX tables", "DOCX import");
401
+ }
402
+ const headerRows = source.rows.findIndex((row) => !row.header);
403
+ const normalizedHeaderRows = headerRows < 0 ? source.rows.length : headerRows;
404
+ if (source.rows.slice(normalizedHeaderRows).some((row) => row.header)) {
405
+ throw new UnsupportedArtifactFeatureError(
406
+ "document",
407
+ "non-contiguous repeating table headers",
408
+ "DOCX import",
409
+ );
410
+ }
411
+ const cannotSplit = source.rows[0]!.cannotSplit;
412
+ if (source.rows.some((row) => row.cannotSplit !== cannotSplit)) {
413
+ throw new UnsupportedArtifactFeatureError(
414
+ "document",
415
+ "mixed table row-splitting policies",
416
+ "DOCX import",
417
+ );
418
+ }
419
+ if (source.rows.some((row) => row.heightPt !== undefined || row.heightRule !== undefined)) {
420
+ throw new UnsupportedArtifactFeatureError(
421
+ "document",
422
+ "explicit table row heights",
423
+ "DOCX import",
424
+ );
425
+ }
426
+ let headerFill: string | undefined;
427
+ let filledHeaderCells = 0;
428
+ let unfilledHeaderCells = 0;
429
+ const rows = source.rows.map((row, rowIndex) =>
430
+ row.cells.map((cell, columnIndex) => {
431
+ if (cell.columnSpan !== 1 || cell.verticalMerge !== undefined) {
432
+ throw new UnsupportedArtifactFeatureError(
433
+ "document",
434
+ "merged DOCX table cells",
435
+ "DOCX import",
436
+ );
437
+ }
438
+ if (cell.verticalAlignment !== "center") {
439
+ throw new UnsupportedArtifactFeatureError(
440
+ "document",
441
+ `table cell vertical alignment ${cell.verticalAlignment ?? "default"}`,
442
+ "DOCX import",
443
+ );
444
+ }
445
+ if (cell.blocks.length !== 1 || cell.blocks[0]?.kind !== "paragraph") {
446
+ throw new UnsupportedArtifactFeatureError(
447
+ "document",
448
+ "multi-paragraph or nested DOCX table cells",
449
+ "DOCX import",
450
+ );
451
+ }
452
+ const explicitFill =
453
+ cell.fill && cell.fill.toLowerCase() !== "auto" ? normalizeHex(cell.fill) : undefined;
454
+ if (explicitFill) {
455
+ if (rowIndex >= normalizedHeaderRows) {
456
+ throw new UnsupportedArtifactFeatureError("document", "body cell shading", "DOCX import");
457
+ }
458
+ if (headerFill && headerFill !== explicitFill) {
459
+ throw new UnsupportedArtifactFeatureError(
460
+ "document",
461
+ "mixed table-header fills",
462
+ "DOCX import",
463
+ );
464
+ }
465
+ headerFill = explicitFill;
466
+ filledHeaderCells += 1;
467
+ } else if (rowIndex < normalizedHeaderRows) {
468
+ unfilledHeaderCells += 1;
469
+ }
470
+ const paragraph = cell.blocks[0];
471
+ if (
472
+ paragraph.inlines.some((inline) => inline.kind === "pageBreak") ||
473
+ paragraph.commentAnchors.length > 0
474
+ ) {
475
+ throw new UnsupportedArtifactFeatureError(
476
+ "document",
477
+ "page breaks or comments in table cells",
478
+ "DOCX import",
479
+ );
480
+ }
481
+ const inherited = resolveImportedStyle(paragraph.styleId, styles, "paragraph");
482
+ assertRepresentableTableCellParagraph(
483
+ { ...inherited.paragraph, ...paragraph.style },
484
+ paragraph.styleId,
485
+ listKinds,
486
+ );
487
+ if (cell.width) {
488
+ const expected = source.gridColumnWidthsPt[columnIndex];
489
+ if (
490
+ cell.width.unit !== "pt" ||
491
+ expected === undefined ||
492
+ Math.abs(cell.width.value - expected) > 0.05
493
+ ) {
494
+ throw new UnsupportedArtifactFeatureError(
495
+ "document",
496
+ "table cell widths inconsistent with the fixed column grid",
497
+ "DOCX import",
498
+ );
499
+ }
500
+ }
501
+ return paragraph.inlines.map((inline) => {
502
+ if (inline.kind !== "run") throw new Error("Unexpected non-run in imported table cell");
503
+ if (inline.changeId)
504
+ throw new UnsupportedArtifactFeatureError(
505
+ "document",
506
+ "tracked changes in table cells",
507
+ "DOCX import",
508
+ );
509
+ const runStyle = resolveImportedStyle(inline.styleId, styles, "character").run;
510
+ return new DocumentTextRun(
511
+ inline.text,
512
+ mapImportedRunStyle({ ...inherited.run, ...runStyle, ...inline.style }),
513
+ );
514
+ });
515
+ }),
516
+ );
517
+ if (filledHeaderCells > 0 && unfilledHeaderCells > 0) {
518
+ throw new UnsupportedArtifactFeatureError(
519
+ "document",
520
+ "partially shaded table-header rows",
521
+ "DOCX import",
522
+ );
523
+ }
524
+ const usable = page.widthPt - page.marginLeftPt - page.marginRightPt;
525
+ if (
526
+ source.gridColumnWidthsPt.length > 0 &&
527
+ (source.gridColumnWidthsPt.length !== columnCount ||
528
+ source.gridColumnWidthsPt.some((width) => !Number.isFinite(width) || width <= 0))
529
+ ) {
530
+ throw new UnsupportedArtifactFeatureError(
531
+ "document",
532
+ "invalid or incomplete fixed table column grid",
533
+ "DOCX import",
534
+ );
535
+ }
536
+ const widths =
537
+ source.gridColumnWidthsPt.length === columnCount ? source.gridColumnWidthsPt : undefined;
538
+ const widthPt =
539
+ source.width?.unit === "pt"
540
+ ? source.width.value
541
+ : (widths?.reduce((total, width) => total + width, 0) ?? usable);
542
+ if (source.width && source.width.unit !== "pt" && source.width.unit !== "auto") {
543
+ throw new UnsupportedArtifactFeatureError(
544
+ "document",
545
+ "percentage-width DOCX tables",
546
+ "DOCX import",
547
+ );
548
+ }
549
+ if (
550
+ source.width?.unit === "pt" &&
551
+ widths &&
552
+ Math.abs(source.width.value - widths.reduce((total, width) => total + width, 0)) > 0.05
553
+ ) {
554
+ throw new UnsupportedArtifactFeatureError(
555
+ "document",
556
+ "table width inconsistent with its fixed column grid",
557
+ "DOCX import",
558
+ );
559
+ }
560
+ if (source.alignment && source.alignment !== "left" && source.alignment !== "start") {
561
+ throw new UnsupportedArtifactFeatureError(
562
+ "document",
563
+ `table alignment ${source.alignment}`,
564
+ "DOCX import",
565
+ );
566
+ }
567
+ if (source.layout !== "fixed") {
568
+ throw new UnsupportedArtifactFeatureError("document", "autofit DOCX tables", "DOCX import");
569
+ }
570
+ const cellPaddingPt = importedUniformCellPadding(source);
571
+ const borderColor = importedUniformTableBorder(source.borders);
572
+ if (
573
+ !source.indent ||
574
+ source.indent.unit !== "pt" ||
575
+ Math.abs(source.indent.value - cellPaddingPt) > 0.01
576
+ ) {
577
+ throw new UnsupportedArtifactFeatureError(
578
+ "document",
579
+ "table indent not equal to its leading cell padding",
580
+ "DOCX import",
581
+ );
582
+ }
583
+ return {
584
+ rows,
585
+ style: {
586
+ widthPt,
587
+ ...(widths ? { columnWidthsPt: [...widths] } : {}),
588
+ headerRows: normalizedHeaderRows,
589
+ cellPaddingPt,
590
+ borderColor,
591
+ ...(headerFill ? { headerFill } : {}),
592
+ ...(!cannotSplit ? { allowRowSplit: true } : {}),
593
+ },
594
+ };
595
+ }
596
+
597
+ function importSectionStories(
598
+ document: Document,
599
+ source: ImportedSection,
600
+ target: DocumentSection,
601
+ headers: ReadonlyMap<string, ImportedHeaderFooter>,
602
+ footers: ReadonlyMap<string, ImportedHeaderFooter>,
603
+ styles: ReadonlyMap<string, ImportedStyle>,
604
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
605
+ paragraphs: ParagraphProjectionMap,
606
+ ): void {
607
+ const apply = (
608
+ references: ImportedSection["headers"],
609
+ available: ReadonlyMap<string, ImportedHeaderFooter>,
610
+ stories: DocumentSectionStories,
611
+ ): void => {
612
+ for (const reference of references) {
613
+ const key =
614
+ reference.kind === "default"
615
+ ? "default"
616
+ : reference.kind === "first"
617
+ ? "first"
618
+ : reference.kind === "even"
619
+ ? "even"
620
+ : undefined;
621
+ if (!key)
622
+ throw new UnsupportedArtifactFeatureError(
623
+ "document",
624
+ `header/footer variant ${reference.kind}`,
625
+ "DOCX import",
626
+ );
627
+ const importedStory = available.get(reference.partName);
628
+ if (!importedStory)
629
+ throw new Error(`Imported DOCX references missing story ${reference.partName}`);
630
+ const story = stories[key];
631
+ if (story.items.length > 0)
632
+ throw new Error(`Imported DOCX repeats ${key} header/footer for one section`);
633
+ if (isEmptyStoryPlaceholder(importedStory)) continue;
634
+ for (const block of importedStory.blocks)
635
+ appendImportedStoryBlock(
636
+ story,
637
+ block,
638
+ document,
639
+ styles,
640
+ listKinds,
641
+ paragraphs,
642
+ target.page,
643
+ );
644
+ }
645
+ };
646
+ apply(source.headers, headers, target.headers);
647
+ apply(source.footers, footers, target.footers);
648
+ }
649
+
650
+ function isEmptyStoryPlaceholder(story: ImportedHeaderFooter): boolean {
651
+ const block = story.blocks[0];
652
+ return (
653
+ story.blocks.length === 1 &&
654
+ block?.kind === "paragraph" &&
655
+ block.styleId === undefined &&
656
+ Object.keys(block.style).length === 0 &&
657
+ block.commentAnchors.length === 0 &&
658
+ block.inlines.every(
659
+ (inline) =>
660
+ inline.kind === "run" &&
661
+ inline.text.length === 0 &&
662
+ inline.styleId === undefined &&
663
+ Object.keys(inline.style).length === 0,
664
+ )
665
+ );
666
+ }
667
+
668
+ function importComments(
669
+ document: Document,
670
+ comments: readonly ImportedComment[],
671
+ paragraphs: ReadonlyMap<string, readonly ParagraphProjection[]>,
672
+ styles: ReadonlyMap<string, ImportedStyle>,
673
+ ): void {
674
+ type ExtendedComment = ImportedComment & { parentId?: string; resolved?: boolean };
675
+ const byId = new Map(comments.map((comment) => [comment.id, comment as ExtendedComment]));
676
+ const roots: ExtendedComment[] = [];
677
+ const repliesByParent = new Map<string, ExtendedComment[]>();
678
+ for (const raw of comments) {
679
+ const comment = raw as ExtendedComment;
680
+ if (comment.parentId === undefined) roots.push(comment);
681
+ else appendMap(repliesByParent, comment.parentId, comment);
682
+ }
683
+ const anchorsByComment = new Map<
684
+ string,
685
+ Array<{
686
+ source: ImportedParagraph;
687
+ target: DocumentParagraph;
688
+ start: number;
689
+ end: number;
690
+ }>
691
+ >();
692
+ // Build one review index. The previous root-by-paragraph scan was bounded,
693
+ // but still quadratic for a large legitimate document with many comments.
694
+ for (const projections of paragraphs.values()) {
695
+ for (const { source, target } of projections) {
696
+ const anchors = new Map<string, ImportedParagraph["commentAnchors"]>();
697
+ for (const anchor of source.commentAnchors) appendMap(anchors, anchor.commentId, anchor);
698
+ for (const [commentId, values] of anchors) {
699
+ const start =
700
+ values.find((anchor) => anchor.kind === "start")?.textOffset ??
701
+ values.find((anchor) => anchor.kind === "reference")?.textOffset;
702
+ const end = values.find((anchor) => anchor.kind === "end")?.textOffset ?? start;
703
+ if (start === undefined || end === undefined) {
704
+ throw new Error(`Imported comment ${commentId} has an incomplete anchor`);
705
+ }
706
+ appendMap(anchorsByComment, commentId, { source, target, start, end });
707
+ }
708
+ }
709
+ }
710
+ for (const root of roots) {
711
+ const matches = anchorsByComment.get(root.id) ?? [];
712
+ const sourceIds = new Set(matches.map((match) => match.source.id));
713
+ if (matches.length === 0 || sourceIds.size !== 1)
714
+ throw new Error(`Imported comment ${root.id} must have exactly one source text anchor`);
715
+ if (matches.length > 1) {
716
+ throw new UnsupportedArtifactFeatureError(
717
+ "document",
718
+ "comments in shared or inherited header/footer stories",
719
+ "DOCX import",
720
+ );
721
+ }
722
+ const rootText = importedCommentText(root, styles);
723
+ assertRepresentableCommentInitials(root.author ?? "Unknown", root.initials);
724
+ const replies = repliesByParent.get(root.id) ?? [];
725
+ for (const match of matches) {
726
+ const thread = document.comments.addThread(
727
+ { block: match.target, start: match.start, end: match.end },
728
+ rootText,
729
+ {
730
+ author: root.author ?? "Unknown",
731
+ createdAt: normalizedImportedDate(root.createdAt),
732
+ resolved: root.resolved ?? false,
733
+ },
734
+ );
735
+ for (const reply of replies) {
736
+ assertRepresentableCommentInitials(reply.author ?? "Unknown", reply.initials);
737
+ thread.addReply(
738
+ importedCommentText(reply, styles),
739
+ reply.author ?? "Unknown",
740
+ normalizedImportedDate(reply.createdAt),
741
+ );
742
+ }
743
+ }
744
+ byId.delete(root.id);
745
+ for (const reply of replies) byId.delete(reply.id);
746
+ }
747
+ if (byId.size > 0)
748
+ throw new Error(`Imported DOCX has orphaned comment replies: ${[...byId.keys()].join(", ")}`);
749
+ }
750
+
751
+ function importTrackedChanges(
752
+ document: Document,
753
+ imported: ImportedDocument,
754
+ paragraphs: ReadonlyMap<string, readonly ParagraphProjection[]>,
755
+ ): void {
756
+ for (const source of imported.trackedChanges) {
757
+ const projections = paragraphs.get(source.blockId);
758
+ if (!projections || projections.length === 0)
759
+ throw new UnsupportedArtifactFeatureError(
760
+ "document",
761
+ "tracked changes outside body/header/footer paragraphs",
762
+ "DOCX import",
763
+ );
764
+ if (projections.length > 1) {
765
+ throw new UnsupportedArtifactFeatureError(
766
+ "document",
767
+ "tracked changes in shared or inherited header/footer stories",
768
+ "DOCX import",
769
+ );
770
+ }
771
+ const extended = source as typeof source & { startTextOffset?: number; endTextOffset?: number };
772
+ for (const paragraph of projections) {
773
+ const start =
774
+ extended.startTextOffset ??
775
+ inlineIndexToTextOffset(paragraph.source, source.startInlineIndex);
776
+ const end =
777
+ extended.endTextOffset ?? inlineIndexToTextOffset(paragraph.source, source.endInlineIndex);
778
+ document.changes.add(
779
+ { block: paragraph.target, start, end },
780
+ source.kind,
781
+ source.author ?? "Unknown",
782
+ normalizedImportedDate(source.createdAt),
783
+ );
784
+ }
785
+ }
786
+ }
787
+
788
+ function validateImportedReviewProjection(document: Document): void {
789
+ const commentsByBlock = new Map<string, DocumentCommentThread[]>();
790
+ for (const comment of document.comments.items) {
791
+ appendMap(commentsByBlock, comment.blockId, comment);
792
+ }
793
+ const changesByBlock = new Map<string, TrackedChange[]>();
794
+ for (const change of document.changes.items) {
795
+ appendMap(changesByBlock, change.blockId, change);
796
+ }
797
+ for (const block of document.allStoryBlocks()) {
798
+ if (!(block instanceof DocumentParagraph)) continue;
799
+ const comments = commentsByBlock.get(block.id) ?? [];
800
+ const changes = changesByBlock.get(block.id) ?? [];
801
+ validateNonCrossingComments(comments);
802
+ validateNonOverlappingChanges(changes);
803
+ validateReviewMarkupForDocx(block, comments, changes);
804
+ }
805
+ }
806
+
807
+ function resolveImportedStyle(
808
+ styleId: string | undefined,
809
+ styles: ReadonlyMap<string, ImportedStyle>,
810
+ expectedKind: "paragraph" | "character",
811
+ seen = new Set<string>(),
812
+ applyDefault = true,
813
+ ): ResolvedImportedStyle {
814
+ const cacheKey = `${expectedKind}:${applyDefault ? "default" : "direct"}:${styleId ?? ""}`;
815
+ const cache =
816
+ importedStyleCache.get(styles as object) ?? new Map<string, ResolvedImportedStyle>();
817
+ if (!importedStyleCache.has(styles as object)) importedStyleCache.set(styles as object, cache);
818
+ if (seen.size === 0) {
819
+ const cached = cache.get(cacheKey);
820
+ if (cached) return cached;
821
+ }
822
+ if (!styleId) {
823
+ if (!applyDefault) {
824
+ const empty = { paragraph: {}, run: {} };
825
+ if (seen.size === 0) cache.set(cacheKey, empty);
826
+ return empty;
827
+ }
828
+ const defaults = [...styles.values()].filter(
829
+ (style) => style.isDefault && style.kind === expectedKind,
830
+ );
831
+ if (defaults.length > 1) {
832
+ throw new Error(`Imported DOCX defines multiple default ${expectedKind} styles`);
833
+ }
834
+ const defaultStyle = defaults[0];
835
+ const resolved = defaultStyle
836
+ ? resolveImportedStyle(defaultStyle.styleId, styles, expectedKind, seen, false)
837
+ : { paragraph: {}, run: {} };
838
+ if (seen.size === 0) cache.set(cacheKey, resolved);
839
+ return resolved;
840
+ }
841
+ if (seen.has(styleId)) throw new Error(`Imported DOCX style cycle at ${styleId}`);
842
+ const style = styles.get(styleId);
843
+ if (!style) {
844
+ if (["Normal", "DefaultParagraphFont", "ListParagraph"].includes(styleId)) {
845
+ return { paragraph: {}, run: {} };
846
+ }
847
+ throw new Error(`Imported DOCX references missing style ${styleId}`);
848
+ }
849
+ if (style.kind !== expectedKind) {
850
+ throw new UnsupportedArtifactFeatureError(
851
+ "document",
852
+ `${expectedKind} content using ${style.kind} style ${styleId}`,
853
+ "DOCX import",
854
+ );
855
+ }
856
+ const canonical =
857
+ expectedKind === "paragraph"
858
+ ? styleId === "Normal" ||
859
+ styleId === "ListParagraph" ||
860
+ /^(?:Heading\s*)[1-6]$/iu.test(styleId)
861
+ : styleId === "DefaultParagraphFont";
862
+ if (!canonical) {
863
+ throw new UnsupportedArtifactFeatureError(
864
+ "document",
865
+ `custom named ${expectedKind} style ${styleId}`,
866
+ "DOCX import",
867
+ );
868
+ }
869
+ seen.add(styleId);
870
+ const parent = resolveImportedStyle(style.basedOn, styles, expectedKind, seen, false);
871
+ seen.delete(styleId);
872
+ const resolved = {
873
+ paragraph: { ...parent.paragraph, ...style.paragraph },
874
+ run: { ...parent.run, ...style.run },
875
+ };
876
+ if (seen.size === 0) cache.set(cacheKey, resolved);
877
+ return resolved;
878
+ }
879
+
880
+ function mapImportedParagraphStyle(
881
+ style: ImportedParagraphStyle,
882
+ styleId: string | undefined,
883
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
884
+ ): DocumentParagraphStyle {
885
+ if (style.bidirectional)
886
+ throw new UnsupportedArtifactFeatureError(
887
+ "document",
888
+ "bidirectional paragraphs",
889
+ "DOCX import",
890
+ );
891
+ if (
892
+ (style.indentLeftPt ?? 0) !== 0 ||
893
+ (style.indentRightPt ?? 0) !== 0 ||
894
+ (style.firstLinePt ?? 0) !== 0 ||
895
+ (style.hangingPt ?? 0) !== 0
896
+ ) {
897
+ throw new UnsupportedArtifactFeatureError(
898
+ "document",
899
+ "direct paragraph indentation",
900
+ "DOCX import",
901
+ );
902
+ }
903
+ if (style.keepLines)
904
+ throw new UnsupportedArtifactFeatureError(
905
+ "document",
906
+ "keep-lines paragraph pagination",
907
+ "DOCX import",
908
+ );
909
+ if (style.lineRule && style.lineRule !== "auto")
910
+ throw new UnsupportedArtifactFeatureError(
911
+ "document",
912
+ `paragraph line rule ${style.lineRule}`,
913
+ "DOCX import",
914
+ );
915
+ const headingMatch = /(?:^|\s)heading\s*([1-6])$/iu.exec(styleId ?? "");
916
+ const importedHeadingLevel = headingMatch
917
+ ? (Number(headingMatch[1]) as 1 | 2 | 3 | 4 | 5 | 6)
918
+ : undefined;
919
+ if (
920
+ style.outlineLevel !== undefined &&
921
+ (style.outlineLevel < 0 ||
922
+ style.outlineLevel > 5 ||
923
+ importedHeadingLevel === undefined ||
924
+ style.outlineLevel + 1 !== importedHeadingLevel)
925
+ ) {
926
+ throw new UnsupportedArtifactFeatureError(
927
+ "document",
928
+ `outline level ${style.outlineLevel} without the matching supported heading style`,
929
+ "DOCX import",
930
+ );
931
+ }
932
+ const alignmentValue =
933
+ style.alignment === "both"
934
+ ? "justify"
935
+ : style.alignment === "center"
936
+ ? "center"
937
+ : style.alignment === "right" || style.alignment === "end"
938
+ ? "right"
939
+ : style.alignment === "left" || style.alignment === "start"
940
+ ? "left"
941
+ : undefined;
942
+ if (style.alignment && !alignmentValue)
943
+ throw new UnsupportedArtifactFeatureError(
944
+ "document",
945
+ `paragraph alignment ${style.alignment}`,
946
+ "DOCX import",
947
+ );
948
+ const list = style.list;
949
+ const listKind = list ? listKinds.get(list.numId)?.get(list.level) : undefined;
950
+ if (list && !listKind)
951
+ throw new Error(`Imported paragraph references missing numbering ${list.numId}/${list.level}`);
952
+ return {
953
+ ...(importedHeadingLevel ? { headingLevel: importedHeadingLevel } : {}),
954
+ ...(alignmentValue ? { alignment: alignmentValue } : {}),
955
+ ...(style.spaceBeforePt !== undefined ? { spaceBeforePt: style.spaceBeforePt } : {}),
956
+ ...(style.spaceAfterPt !== undefined ? { spaceAfterPt: style.spaceAfterPt } : {}),
957
+ ...(style.line !== undefined ? { lineHeight: style.line / 240 } : {}),
958
+ ...(style.keepNext !== undefined ? { keepNext: style.keepNext } : {}),
959
+ ...(style.pageBreakBefore !== undefined ? { pageBreakBefore: style.pageBreakBefore } : {}),
960
+ ...(list && listKind
961
+ ? { list: { kind: listKind, level: list.level, instanceId: list.numId } }
962
+ : {}),
963
+ };
964
+ }
965
+
966
+ function mapImportedRunStyle(style: ImportedRunStyle): DocumentTextStyle {
967
+ if (style.highlight && style.highlight !== "none")
968
+ throw new UnsupportedArtifactFeatureError("document", "text highlighting", "DOCX import");
969
+ if (style.verticalAlign && style.verticalAlign !== "baseline")
970
+ throw new UnsupportedArtifactFeatureError(
971
+ "document",
972
+ `text vertical alignment ${style.verticalAlign}`,
973
+ "DOCX import",
974
+ );
975
+ if (
976
+ style.fontSizeComplexScriptPt !== undefined &&
977
+ style.fontSizeComplexScriptPt !== style.fontSizePt
978
+ ) {
979
+ throw new UnsupportedArtifactFeatureError(
980
+ "document",
981
+ "script-specific font sizing",
982
+ "DOCX import",
983
+ );
984
+ }
985
+ if ((style.boldComplexScript ?? false) !== (style.bold ?? false)) {
986
+ throw new UnsupportedArtifactFeatureError(
987
+ "document",
988
+ "script-specific bold formatting",
989
+ "DOCX import",
990
+ );
991
+ }
992
+ if ((style.italicComplexScript ?? false) !== (style.italic ?? false)) {
993
+ throw new UnsupportedArtifactFeatureError(
994
+ "document",
995
+ "script-specific italic formatting",
996
+ "DOCX import",
997
+ );
998
+ }
999
+ if (style.language) {
1000
+ throw new UnsupportedArtifactFeatureError("document", "run language metadata", "DOCX import");
1001
+ }
1002
+ return {
1003
+ ...(style.fontFamily ? { fontFamily: style.fontFamily } : {}),
1004
+ ...(style.fontSizePt !== undefined ? { fontSizePt: style.fontSizePt } : {}),
1005
+ ...(style.color && style.color !== "auto" ? { color: normalizeHex(style.color) } : {}),
1006
+ ...(style.bold !== undefined ? { bold: style.bold } : {}),
1007
+ ...(style.italic !== undefined ? { italic: style.italic } : {}),
1008
+ ...(style.underline !== undefined ? { underline: style.underline !== "none" } : {}),
1009
+ ...(style.strike !== undefined ? { strike: style.strike } : {}),
1010
+ };
1011
+ }
1012
+
1013
+ function importedPage(section: ImportedSection): DocumentPageGeometry {
1014
+ return {
1015
+ widthPt: section.page.widthPt,
1016
+ heightPt: section.page.heightPt,
1017
+ marginTopPt: section.page.marginTopPt,
1018
+ marginRightPt: section.page.marginRightPt,
1019
+ marginBottomPt: section.page.marginBottomPt,
1020
+ marginLeftPt: section.page.marginLeftPt,
1021
+ headerPt: section.page.headerPt,
1022
+ footerPt: section.page.footerPt,
1023
+ gutterPt: section.page.gutterPt,
1024
+ };
1025
+ }
1026
+
1027
+ function validateImportedSections(
1028
+ imported: ImportedDocument,
1029
+ sections: readonly ImportedSection[],
1030
+ ): void {
1031
+ if (sections.length === 0) throw new Error("Imported DOCX has no sections");
1032
+ const storyReferences = new Set<string>();
1033
+ let expectedStart = 0;
1034
+ for (const section of sections) {
1035
+ for (const reference of [...section.headers, ...section.footers]) {
1036
+ if (storyReferences.has(reference.partName)) {
1037
+ throw new UnsupportedArtifactFeatureError(
1038
+ "document",
1039
+ "linked or inherited header/footer stories",
1040
+ "DOCX import",
1041
+ );
1042
+ }
1043
+ storyReferences.add(reference.partName);
1044
+ }
1045
+ if (
1046
+ section.startBlockIndex !== expectedStart ||
1047
+ !Number.isSafeInteger(section.endBlockIndex) ||
1048
+ section.endBlockIndex < section.startBlockIndex ||
1049
+ section.endBlockIndex > imported.blocks.length
1050
+ ) {
1051
+ throw new Error("Imported DOCX sections do not form a contiguous body partition");
1052
+ }
1053
+ expectedStart = section.endBlockIndex;
1054
+ if (section.type !== undefined && section.type !== "nextPage") {
1055
+ throw new UnsupportedArtifactFeatureError(
1056
+ "document",
1057
+ `section break type ${section.type}`,
1058
+ "DOCX import",
1059
+ );
1060
+ }
1061
+ const orientation = section.page.orientation;
1062
+ if (orientation !== undefined && orientation !== "portrait" && orientation !== "landscape") {
1063
+ throw new UnsupportedArtifactFeatureError(
1064
+ "document",
1065
+ `page orientation ${orientation}`,
1066
+ "DOCX import",
1067
+ );
1068
+ }
1069
+ if (
1070
+ (orientation === "landscape" && section.page.widthPt < section.page.heightPt) ||
1071
+ (orientation === "portrait" && section.page.widthPt > section.page.heightPt)
1072
+ ) {
1073
+ throw new UnsupportedArtifactFeatureError(
1074
+ "document",
1075
+ "page dimensions inconsistent with page orientation",
1076
+ "DOCX import",
1077
+ );
1078
+ }
1079
+ const hasFirstStory = [...section.headers, ...section.footers].some(
1080
+ (reference) => reference.kind === "first",
1081
+ );
1082
+ if (section.titlePage && !hasFirstStory) {
1083
+ throw new UnsupportedArtifactFeatureError(
1084
+ "document",
1085
+ "first-page header/footer mode without a representable first-page story",
1086
+ "DOCX import",
1087
+ );
1088
+ }
1089
+ }
1090
+ if (expectedStart !== imported.blocks.length) {
1091
+ throw new Error("Imported DOCX sections do not cover the full body");
1092
+ }
1093
+ const hasEvenStory = sections.some((section) =>
1094
+ [...section.headers, ...section.footers].some((reference) => reference.kind === "even"),
1095
+ );
1096
+ if (imported.evenAndOddHeaders && !hasEvenStory) {
1097
+ throw new UnsupportedArtifactFeatureError(
1098
+ "document",
1099
+ "even-page header/footer mode without a representable even-page story",
1100
+ "DOCX import",
1101
+ );
1102
+ }
1103
+ }
1104
+
1105
+ function importedListKinds(
1106
+ lists: readonly ImportedDocument["lists"][number][],
1107
+ ): Map<string, Map<number, "bullet" | "number">> {
1108
+ const result = new Map<string, Map<number, "bullet" | "number">>();
1109
+ for (const list of lists) {
1110
+ let instanceSupported = true;
1111
+ for (const override of list.overrides) {
1112
+ if (override.definition || (override.start !== undefined && override.start !== 1)) {
1113
+ instanceSupported = false;
1114
+ }
1115
+ }
1116
+ const levels = new Map<number, "bullet" | "number">();
1117
+ let definitionSupported = true;
1118
+ for (const level of list.levels) {
1119
+ if (
1120
+ level.start !== 1 ||
1121
+ level.restart !== undefined ||
1122
+ level.legal !== undefined ||
1123
+ (level.format !== "bullet" && level.format !== "decimal")
1124
+ ) {
1125
+ definitionSupported = false;
1126
+ continue;
1127
+ }
1128
+ const runtimeLevel = level as typeof level & { alignment?: string };
1129
+ const expectedText = level.format === "decimal" ? `%${level.level + 1}.` : undefined;
1130
+ const supportedBullet =
1131
+ level.format === "bullet" && level.text === ["•", "◦", "▪"][level.level % 3];
1132
+ const paragraphEntries = Object.entries(level.paragraph);
1133
+ const hasExpectedIndent =
1134
+ level.paragraph.indentLeftPt !== undefined &&
1135
+ Math.abs(level.paragraph.indentLeftPt - 36 * (level.level + 1)) <= 0.01 &&
1136
+ level.paragraph.hangingPt !== undefined &&
1137
+ Math.abs(level.paragraph.hangingPt - 18) <= 0.01;
1138
+ const onlyExpectedIndent = paragraphEntries.every(
1139
+ ([key]) => key === "indentLeftPt" || key === "hangingPt",
1140
+ );
1141
+ if (
1142
+ (level.format === "decimal" && level.text !== expectedText) ||
1143
+ (level.format === "bullet" && !supportedBullet) ||
1144
+ (level.suffix !== undefined && level.suffix !== "tab") ||
1145
+ level.paragraphStyleId !== undefined ||
1146
+ (runtimeLevel.alignment !== undefined && runtimeLevel.alignment !== "left") ||
1147
+ !hasExpectedIndent ||
1148
+ !onlyExpectedIndent ||
1149
+ Object.keys(level.run).length > 0
1150
+ ) {
1151
+ definitionSupported = false;
1152
+ continue;
1153
+ }
1154
+ levels.set(level.level, level.format === "bullet" ? "bullet" : "number");
1155
+ }
1156
+ result.set(list.numId, instanceSupported && definitionSupported ? levels : new Map());
1157
+ }
1158
+ return result;
1159
+ }
1160
+
1161
+ function assertRepresentableTableCellParagraph(
1162
+ style: ImportedParagraphStyle,
1163
+ styleId: string | undefined,
1164
+ listKinds: ReadonlyMap<string, ReadonlyMap<number, "bullet" | "number">>,
1165
+ ): void {
1166
+ const projected = mapImportedParagraphStyle(style, styleId, listKinds);
1167
+ const remaining = Object.entries(projected).filter(
1168
+ ([key, value]) =>
1169
+ !(
1170
+ (key === "alignment" && value === "left") ||
1171
+ (key === "spaceBeforePt" && value === 0) ||
1172
+ (key === "spaceAfterPt" && value === 0)
1173
+ ),
1174
+ );
1175
+ if (remaining.length > 0) {
1176
+ throw new UnsupportedArtifactFeatureError(
1177
+ "document",
1178
+ "paragraph formatting inside table cells",
1179
+ "DOCX import",
1180
+ );
1181
+ }
1182
+ }
1183
+
1184
+ function importedUniformCellPadding(source: ImportedTable): number {
1185
+ const hasExplicitMargins =
1186
+ source.cellMargins !== undefined ||
1187
+ source.rows.some((row) => row.cells.some((cell) => cell.margins !== undefined));
1188
+ if (!hasExplicitMargins) {
1189
+ throw new UnsupportedArtifactFeatureError(
1190
+ "document",
1191
+ "table cell padding inherited from an unrepresented style",
1192
+ "DOCX import",
1193
+ );
1194
+ }
1195
+ const values: number[] = [];
1196
+ for (const row of source.rows) {
1197
+ for (const cell of row.cells) {
1198
+ for (const side of ["top", "right", "bottom", "left"] as const) {
1199
+ const measure = boxMeasure(cell.margins, side) ?? boxMeasure(source.cellMargins, side);
1200
+ if (!measure || measure.unit !== "pt" || !Number.isFinite(measure.value)) {
1201
+ throw new UnsupportedArtifactFeatureError(
1202
+ "document",
1203
+ "asymmetric or non-point table cell margins",
1204
+ "DOCX import",
1205
+ );
1206
+ }
1207
+ values.push(measure.value);
1208
+ }
1209
+ }
1210
+ }
1211
+ const first = values[0];
1212
+ if (first === undefined || values.some((value) => Math.abs(value - first) > 0.01)) {
1213
+ throw new UnsupportedArtifactFeatureError(
1214
+ "document",
1215
+ "non-uniform table cell padding",
1216
+ "DOCX import",
1217
+ );
1218
+ }
1219
+ return first;
1220
+ }
1221
+
1222
+ function boxMeasure(
1223
+ box: ImportedBoxMeasures | undefined,
1224
+ side: "top" | "right" | "bottom" | "left",
1225
+ ): { value: number; unit: "pt" | "percent" | "auto" } | undefined {
1226
+ if (!box) return undefined;
1227
+ if (side === "left") return box.left ?? box.start;
1228
+ if (side === "right") return box.right ?? box.end;
1229
+ return box[side];
1230
+ }
1231
+
1232
+ function importedUniformTableBorder(borders: ImportedTable["borders"]): string {
1233
+ if (!borders) {
1234
+ throw new UnsupportedArtifactFeatureError(
1235
+ "document",
1236
+ "table borders inherited from an unrepresented style",
1237
+ "DOCX import",
1238
+ );
1239
+ }
1240
+ const edges: Array<ImportedBorder | undefined> = [
1241
+ borders.top,
1242
+ borders.right,
1243
+ borders.bottom,
1244
+ borders.left,
1245
+ borders.insideHorizontal,
1246
+ borders.insideVertical,
1247
+ ];
1248
+ if (edges.some((edge) => edge === undefined)) {
1249
+ throw new UnsupportedArtifactFeatureError("document", "partial table borders", "DOCX import");
1250
+ }
1251
+ const colors = edges.map((edge) => {
1252
+ if (
1253
+ edge?.style !== "single" ||
1254
+ edge.sizePt === undefined ||
1255
+ Math.abs(edge.sizePt - 0.5) > 0.01 ||
1256
+ (edge.spacePt ?? 0) !== 0 ||
1257
+ edge.shadow === true ||
1258
+ edge.frame === true ||
1259
+ !edge.color ||
1260
+ edge.color.toLowerCase() === "auto"
1261
+ ) {
1262
+ throw new UnsupportedArtifactFeatureError(
1263
+ "document",
1264
+ "non-uniform or decorated table borders",
1265
+ "DOCX import",
1266
+ );
1267
+ }
1268
+ return normalizeHex(edge.color);
1269
+ });
1270
+ const first = colors[0]!;
1271
+ if (colors.some((color) => color !== first)) {
1272
+ throw new UnsupportedArtifactFeatureError(
1273
+ "document",
1274
+ "mixed table border colors",
1275
+ "DOCX import",
1276
+ );
1277
+ }
1278
+ return first;
1279
+ }
1280
+
1281
+ function isOnlyPageBreak(
1282
+ paragraph: ImportedParagraph,
1283
+ styles: ReadonlyMap<string, ImportedStyle>,
1284
+ ): boolean {
1285
+ const inherited = resolveImportedStyle(paragraph.styleId, styles, "paragraph");
1286
+ return (
1287
+ paragraph.inlines.length === 1 &&
1288
+ paragraph.inlines[0]?.kind === "pageBreak" &&
1289
+ paragraph.commentAnchors.length === 0 &&
1290
+ Object.keys({ ...inherited.paragraph, ...paragraph.style }).length === 0 &&
1291
+ Object.keys(inherited.run).length === 0
1292
+ );
1293
+ }
1294
+
1295
+ function importedCommentText(
1296
+ comment: ImportedComment,
1297
+ styles: ReadonlyMap<string, ImportedStyle>,
1298
+ ): string {
1299
+ const paragraph = comment.blocks[0];
1300
+ if (
1301
+ comment.blocks.length !== 1 ||
1302
+ paragraph?.kind !== "paragraph" ||
1303
+ paragraph.styleId !== undefined ||
1304
+ Object.keys(paragraph.style).length > 0 ||
1305
+ paragraph.inlines.some(
1306
+ (inline) =>
1307
+ inline.kind !== "run" ||
1308
+ inline.styleId !== undefined ||
1309
+ Object.keys(inline.style).length > 0,
1310
+ ) ||
1311
+ paragraph.commentAnchors.length > 0
1312
+ ) {
1313
+ throw new UnsupportedArtifactFeatureError(
1314
+ "document",
1315
+ "multi-paragraph or formatted content inside comments",
1316
+ "DOCX import",
1317
+ );
1318
+ }
1319
+ // Resolve defaults solely to detect a formatted default that would otherwise
1320
+ // be silently flattened from this deliberately plain comment subset.
1321
+ const defaults = resolveImportedStyle(undefined, styles, "paragraph");
1322
+ if (Object.keys(defaults.paragraph).length > 0 || Object.keys(defaults.run).length > 0) {
1323
+ throw new UnsupportedArtifactFeatureError(
1324
+ "document",
1325
+ "formatted default style inside comments",
1326
+ "DOCX import",
1327
+ );
1328
+ }
1329
+ return paragraph.inlines.map((inline) => (inline.kind === "run" ? inline.text : "")).join("");
1330
+ }
1331
+
1332
+ function inlineIndexToTextOffset(paragraph: ImportedParagraph, index: number): number {
1333
+ if (!Number.isInteger(index) || index < 0 || index > paragraph.inlines.length)
1334
+ throw new Error("Imported tracked-change inline index is invalid");
1335
+ return paragraph.inlines
1336
+ .slice(0, index)
1337
+ .reduce((total, inline) => total + (inline.kind === "run" ? inline.text.length : 0), 0);
1338
+ }
1339
+
1340
+ function normalizedImportedDate(value: string | undefined): string {
1341
+ if (!value) {
1342
+ throw new UnsupportedArtifactFeatureError(
1343
+ "document",
1344
+ "comment or tracked-change timestamp omitted",
1345
+ "DOCX import",
1346
+ );
1347
+ }
1348
+ const date = new Date(value);
1349
+ if (!Number.isFinite(date.getTime()))
1350
+ throw new Error(`Imported DOCX contains an invalid timestamp: ${value}`);
1351
+ return date.toISOString();
1352
+ }
1353
+
1354
+ type DocxModule = typeof import("docx");
1355
+ type DocxBlock = import("docx").Paragraph | import("docx").Table;
1356
+ type DocxParagraphChild = import("docx").ParagraphChild;
1357
+ type DocxContext = {
1358
+ readonly commentsByBlock: Map<
1359
+ string,
1360
+ Array<{ thread: DocumentCommentThread; numericId: number }>
1361
+ >;
1362
+ readonly changesByBlock: Map<string, Array<{ change: TrackedChange; numericId: number }>>;
1363
+ readonly commentDefinitions: import("docx").ICommentOptions[];
1364
+ readonly listInstances: Map<string, number>;
1365
+ };
1366
+
1367
+ function buildDocxContext(document: Document, docx: DocxModule): DocxContext {
1368
+ const commentsByBlock = new Map<
1369
+ string,
1370
+ Array<{ thread: DocumentCommentThread; numericId: number }>
1371
+ >();
1372
+ const commentDefinitions: import("docx").ICommentOptions[] = [];
1373
+ let commentId = 0;
1374
+ for (const thread of document.comments.items) {
1375
+ const rootId = commentId++;
1376
+ appendMap(commentsByBlock, thread.blockId, { thread, numericId: rootId });
1377
+ thread.replies.forEach((reply, index) => {
1378
+ const id = index === 0 ? rootId : commentId++;
1379
+ commentDefinitions.push({
1380
+ id,
1381
+ ...(index > 0 ? { parentId: rootId } : {}),
1382
+ author: reply.author,
1383
+ initials: initials(reply.author),
1384
+ date: new Date(reply.createdAt),
1385
+ ...(index === 0 ? { resolved: thread.resolved } : {}),
1386
+ children: [
1387
+ new docx.Paragraph({
1388
+ children: [new docx.TextRun(docxTextRunOptions(reply.text, {}, docx))],
1389
+ }),
1390
+ ],
1391
+ });
1392
+ });
1393
+ }
1394
+ const changesByBlock = new Map<string, Array<{ change: TrackedChange; numericId: number }>>();
1395
+ document.changes.items.forEach((change, numericId) =>
1396
+ appendMap(changesByBlock, change.blockId, { change, numericId }),
1397
+ );
1398
+ return {
1399
+ commentsByBlock,
1400
+ changesByBlock,
1401
+ commentDefinitions,
1402
+ listInstances: buildListInstances(document),
1403
+ };
1404
+ }
1405
+
1406
+ function buildDocxStories(
1407
+ stories: DocumentSectionStories,
1408
+ page: DocumentPageGeometry,
1409
+ docx: DocxModule,
1410
+ context: DocxContext,
1411
+ ): {
1412
+ default?: import("docx").Header | import("docx").Footer;
1413
+ first?: import("docx").Header | import("docx").Footer;
1414
+ even?: import("docx").Header | import("docx").Footer;
1415
+ } {
1416
+ const make = (
1417
+ story: DocumentStory,
1418
+ emitEmpty: boolean,
1419
+ ): import("docx").Header | import("docx").Footer | undefined => {
1420
+ if (story.items.length === 0 && !emitEmpty) return undefined;
1421
+ const children =
1422
+ story.items.length > 0
1423
+ ? story.items.map((block) => toDocxBlock(block, page, docx, context))
1424
+ : [new docx.Paragraph({ children: [] })];
1425
+ return story.kind === "header" ? new docx.Header({ children }) : new docx.Footer({ children });
1426
+ };
1427
+ const result: {
1428
+ default?: import("docx").Header | import("docx").Footer;
1429
+ first?: import("docx").Header | import("docx").Footer;
1430
+ even?: import("docx").Header | import("docx").Footer;
1431
+ } = {};
1432
+ // Emit explicit empty variants as well as authored ones. Omitting a part
1433
+ // means "inherit from the prior section" in Word, which is not equivalent
1434
+ // to this model's intentionally empty editable story.
1435
+ const defaultStory = make(stories.default, true);
1436
+ const firstStory = make(stories.first, true);
1437
+ const evenStory = make(stories.even, true);
1438
+ if (defaultStory) result.default = defaultStory;
1439
+ if (firstStory) result.first = firstStory;
1440
+ if (evenStory) result.even = evenStory;
1441
+ return result;
1442
+ }
1443
+
1444
+ function toDocxBlock(
1445
+ block: DocumentBlock | DocumentStoryBlock,
1446
+ page: DocumentPageGeometry,
1447
+ docx: DocxModule,
1448
+ context: DocxContext,
1449
+ ): DocxBlock {
1450
+ if (block instanceof DocumentPageBreak) {
1451
+ return new docx.Paragraph({ children: [new docx.PageBreak()] });
1452
+ }
1453
+ if (block instanceof DocumentTable) return toDocxTable(block, page, docx);
1454
+ const heading = headingLevel(block.style.headingLevel, docx);
1455
+ const paragraphAlignment = alignment(block.style.alignment, docx);
1456
+ const spacing = {
1457
+ ...(block.style.spaceBeforePt !== undefined
1458
+ ? { before: ptToTwip(block.style.spaceBeforePt) }
1459
+ : {}),
1460
+ ...(block.style.spaceAfterPt !== undefined
1461
+ ? { after: ptToTwip(block.style.spaceAfterPt) }
1462
+ : {}),
1463
+ ...(block.style.lineHeight !== undefined
1464
+ ? { line: Math.round(block.style.lineHeight * 240) }
1465
+ : {}),
1466
+ };
1467
+ const list = block.style.list;
1468
+ return new docx.Paragraph({
1469
+ ...(heading ? { heading } : {}),
1470
+ ...(paragraphAlignment ? { alignment: paragraphAlignment } : {}),
1471
+ ...(Object.keys(spacing).length > 0 ? { spacing } : {}),
1472
+ ...(block.style.keepNext !== undefined ? { keepNext: block.style.keepNext } : {}),
1473
+ ...(block.style.pageBreakBefore !== undefined
1474
+ ? { pageBreakBefore: block.style.pageBreakBefore }
1475
+ : {}),
1476
+ ...(list
1477
+ ? {
1478
+ numbering: {
1479
+ reference: list.kind === "bullet" ? "opengeni-bullet" : "opengeni-number",
1480
+ level: list.level ?? 0,
1481
+ ...(context.listInstances.has(block.id)
1482
+ ? { instance: context.listInstances.get(block.id)! }
1483
+ : {}),
1484
+ },
1485
+ }
1486
+ : {}),
1487
+ children: toDocxParagraphChildren(block, docx, context),
1488
+ });
1489
+ }
1490
+
1491
+ function toDocxTable(
1492
+ block: DocumentTable,
1493
+ page: DocumentPageGeometry,
1494
+ docx: DocxModule,
1495
+ ): import("docx").Table {
1496
+ const columnCount = block.rows[0]?.length ?? 0;
1497
+ const usableWidth = page.widthPt - page.marginLeftPt - page.marginRightPt;
1498
+ const totalWidth =
1499
+ block.style.widthPt ??
1500
+ block.style.columnWidthsPt?.reduce((total, width) => total + width, 0) ??
1501
+ usableWidth;
1502
+ const columnWidths =
1503
+ block.style.columnWidthsPt ??
1504
+ Array.from({ length: columnCount }, () => totalWidth / columnCount);
1505
+ const columnWidthsTwip = columnWidths.map(ptToTwip);
1506
+ // Derive tblW from the exact serialized grid. Rounding the model width and
1507
+ // each grid column independently can otherwise make our own DOCX fail the
1508
+ // strict fixed-grid consistency check on import.
1509
+ const tableWidthTwip = columnWidthsTwip.reduce((sum, width) => sum + width, 0);
1510
+ const padding = ptToTwip(block.style.cellPaddingPt ?? 6);
1511
+ const borderColor = normalizeHex(block.style.borderColor ?? "#D1D5DB").slice(1);
1512
+ const border = { style: docx.BorderStyle.SINGLE, size: 4, color: borderColor };
1513
+ return new docx.Table({
1514
+ width: { size: tableWidthTwip, type: docx.WidthType.DXA },
1515
+ columnWidths: columnWidthsTwip,
1516
+ indent: { size: padding, type: docx.WidthType.DXA },
1517
+ layout: docx.TableLayoutType.FIXED,
1518
+ margins: { top: padding, bottom: padding, left: padding, right: padding },
1519
+ borders: {
1520
+ top: border,
1521
+ bottom: border,
1522
+ left: border,
1523
+ right: border,
1524
+ insideHorizontal: border,
1525
+ insideVertical: border,
1526
+ },
1527
+ rows: block.rows.map(
1528
+ (row, rowIndex) =>
1529
+ new docx.TableRow({
1530
+ tableHeader: rowIndex < (block.style.headerRows ?? 0),
1531
+ cantSplit: !(block.style.allowRowSplit ?? false),
1532
+ children: row.map(
1533
+ (cell, columnIndex) =>
1534
+ new docx.TableCell({
1535
+ width: {
1536
+ size: columnWidthsTwip[columnIndex] ?? ptToTwip(72),
1537
+ type: docx.WidthType.DXA,
1538
+ },
1539
+ verticalAlign: docx.VerticalAlign.CENTER,
1540
+ margins: { top: padding, bottom: padding, left: padding, right: padding },
1541
+ ...(rowIndex < (block.style.headerRows ?? 0) && block.style.headerFill
1542
+ ? { shading: { fill: normalizeHex(block.style.headerFill).slice(1) } }
1543
+ : {}),
1544
+ children: [
1545
+ new docx.Paragraph({ children: cell.map((run) => toDocxTextRun(run, docx)) }),
1546
+ ],
1547
+ }),
1548
+ ),
1549
+ }),
1550
+ ),
1551
+ });
1552
+ }
1553
+
1554
+ function toDocxParagraphChildren(
1555
+ block: DocumentParagraph,
1556
+ docx: DocxModule,
1557
+ context: DocxContext,
1558
+ ): DocxParagraphChild[] {
1559
+ const comments = context.commentsByBlock.get(block.id) ?? [];
1560
+ const changes = context.changesByBlock.get(block.id) ?? [];
1561
+ const paragraphText = block.text;
1562
+ validateNonCrossingComments(comments.map(({ thread }) => thread));
1563
+ validateNonOverlappingChanges(changes.map(({ change }) => change));
1564
+ validateReviewMarkupForDocx(
1565
+ block,
1566
+ comments.map(({ thread }) => thread),
1567
+ changes.map(({ change }) => change),
1568
+ );
1569
+ const boundaries = new Set<number>([0, paragraphText.length]);
1570
+ const runSpans: Array<{ start: number; end: number; run: DocumentTextRun }> = [];
1571
+ const pointComments = new Map<number, typeof comments>();
1572
+ const openingComments = new Map<number, typeof comments>();
1573
+ const closingComments = new Map<number, typeof comments>();
1574
+ let offset = 0;
1575
+ for (const run of block.runs) {
1576
+ const start = offset;
1577
+ offset += run.text.length;
1578
+ runSpans.push({ start, end: offset, run });
1579
+ boundaries.add(start);
1580
+ boundaries.add(offset);
1581
+ }
1582
+ for (const { thread } of comments) {
1583
+ boundaries.add(thread.start);
1584
+ boundaries.add(thread.end);
1585
+ }
1586
+ for (const comment of comments) {
1587
+ if (comment.thread.start === comment.thread.end) {
1588
+ appendMap(pointComments, comment.thread.start, comment);
1589
+ } else {
1590
+ appendMap(openingComments, comment.thread.start, comment);
1591
+ appendMap(closingComments, comment.thread.end, comment);
1592
+ }
1593
+ }
1594
+ for (const values of openingComments.values()) {
1595
+ values.sort((left, right) => right.thread.end - left.thread.end);
1596
+ }
1597
+ for (const values of closingComments.values()) {
1598
+ values.sort((left, right) => right.thread.start - left.thread.start);
1599
+ }
1600
+ for (const { change } of changes) {
1601
+ boundaries.add(change.start);
1602
+ boundaries.add(change.end);
1603
+ }
1604
+ const points = [...boundaries].sort((a, b) => a - b);
1605
+ const sortedChanges = [...changes].sort(
1606
+ (left, right) => left.change.start - right.change.start || left.change.end - right.change.end,
1607
+ );
1608
+ const children: DocxParagraphChild[] = [];
1609
+ if (paragraphText.length === 0) {
1610
+ for (const { numericId } of pointComments.get(0) ?? []) {
1611
+ children.push(
1612
+ new docx.CommentRangeStart(numericId),
1613
+ new docx.CommentRangeEnd(numericId),
1614
+ commentReferenceRun(numericId, docx),
1615
+ );
1616
+ }
1617
+ children.push(...block.runs.map((run) => toDocxTextRun(run, docx)));
1618
+ return children;
1619
+ }
1620
+ let runIndex = 0;
1621
+ let changeIndex = 0;
1622
+ for (let index = 0; index < points.length; index++) {
1623
+ const point = points[index]!;
1624
+ for (const { numericId } of pointComments.get(point) ?? []) {
1625
+ children.push(
1626
+ new docx.CommentRangeStart(numericId),
1627
+ new docx.CommentRangeEnd(numericId),
1628
+ commentReferenceRun(numericId, docx),
1629
+ );
1630
+ }
1631
+ for (const { numericId } of openingComments.get(point) ?? []) {
1632
+ children.push(new docx.CommentRangeStart(numericId));
1633
+ }
1634
+ const next = points[index + 1];
1635
+ if (next !== undefined && next > point) {
1636
+ while (runSpans[runIndex] && runSpans[runIndex]!.end <= point) runIndex += 1;
1637
+ const span = runSpans[runIndex];
1638
+ if (!span) throw new Error(`Document paragraph ${block.id} has a discontinuous run map`);
1639
+ if (span.start > point || span.end < next)
1640
+ throw new Error(`Document paragraph ${block.id} has a discontinuous run map`);
1641
+ const text = span.run.text.slice(point - span.start, next - span.start);
1642
+ while (
1643
+ sortedChanges[changeIndex]?.change.end !== undefined &&
1644
+ sortedChanges[changeIndex]!.change.end <= point
1645
+ ) {
1646
+ changeIndex += 1;
1647
+ }
1648
+ const candidate = sortedChanges[changeIndex];
1649
+ const active =
1650
+ candidate && candidate.change.start <= point && candidate.change.end >= next
1651
+ ? candidate
1652
+ : undefined;
1653
+ if (!active || point === active.change.start) {
1654
+ children.push(
1655
+ active
1656
+ ? toDocxChangedTextRun(
1657
+ paragraphText.slice(active.change.start, active.change.end),
1658
+ span.run.style,
1659
+ active,
1660
+ docx,
1661
+ )
1662
+ : toDocxChangedTextRun(text, span.run.style, undefined, docx),
1663
+ );
1664
+ }
1665
+ }
1666
+ if (next !== undefined) {
1667
+ for (const { numericId } of closingComments.get(next) ?? []) {
1668
+ children.push(new docx.CommentRangeEnd(numericId));
1669
+ children.push(commentReferenceRun(numericId, docx));
1670
+ }
1671
+ }
1672
+ }
1673
+ return children;
1674
+ }
1675
+
1676
+ function commentReferenceRun(numericId: number, docx: DocxModule): import("docx").TextRun {
1677
+ return new docx.TextRun({ children: [new docx.CommentReference(numericId)] });
1678
+ }
1679
+
1680
+ function toDocxChangedTextRun(
1681
+ text: string,
1682
+ style: DocumentTextStyle,
1683
+ active: { change: TrackedChange; numericId: number } | undefined,
1684
+ docx: DocxModule,
1685
+ ): DocxParagraphChild {
1686
+ const options = docxTextRunOptions(text, style, docx);
1687
+ if (!active) return materializedTextRun(text, options, docx);
1688
+ const changeOptions = {
1689
+ ...options,
1690
+ id: active.numericId,
1691
+ author: active.change.author,
1692
+ date: active.change.createdAt,
1693
+ };
1694
+ return active.change.kind === "insert"
1695
+ ? new docx.InsertedTextRun(changeOptions)
1696
+ : new docx.DeletedTextRun(changeOptions);
1697
+ }
1698
+
1699
+ function toDocxTextRun(run: DocumentTextRun, docx: DocxModule): import("docx").TextRun {
1700
+ return materializedTextRun(run.text, docxTextRunOptions(run.text, run.style, docx), docx);
1701
+ }
1702
+
1703
+ function materializedTextRun(
1704
+ text: string,
1705
+ options: import("docx").IRunOptions,
1706
+ docx: DocxModule,
1707
+ ): import("docx").TextRun {
1708
+ if (text.length > 0) return new docx.TextRun(options);
1709
+ // `docx` elides an empty TextRun entirely, including its run properties.
1710
+ // An explicit empty w:t keeps insertion-point formatting without adding a
1711
+ // Unicode sentinel that would alter the document's text semantics.
1712
+ const { text: _text, children: _children, ...properties } = options;
1713
+ return new docx.Run({
1714
+ ...properties,
1715
+ children: [new docx.EmptyElement("w:t")],
1716
+ }) as import("docx").TextRun;
1717
+ }
1718
+
1719
+ function docxTextRunOptions(
1720
+ text: string,
1721
+ style: DocumentTextStyle,
1722
+ docx: DocxModule,
1723
+ ): import("docx").IRunOptions {
1724
+ return {
1725
+ ...(text.includes("\t") || text.includes("\n")
1726
+ ? { children: docxTextChildren(text, docx) }
1727
+ : { text }),
1728
+ ...(style.fontFamily ? { font: style.fontFamily } : {}),
1729
+ ...(style.fontSizePt !== undefined ? { size: Math.round(style.fontSizePt * 2) } : {}),
1730
+ ...(style.color ? { color: normalizeHex(style.color).slice(1) } : {}),
1731
+ ...(style.bold !== undefined ? { bold: style.bold } : {}),
1732
+ ...(style.italic !== undefined ? { italics: style.italic } : {}),
1733
+ ...(style.underline !== undefined
1734
+ ? {
1735
+ underline: {
1736
+ type: style.underline ? docx.UnderlineType.SINGLE : docx.UnderlineType.NONE,
1737
+ },
1738
+ }
1739
+ : {}),
1740
+ ...(style.strike !== undefined ? { strike: style.strike } : {}),
1741
+ };
1742
+ }
1743
+
1744
+ function docxTextChildren(
1745
+ text: string,
1746
+ docx: DocxModule,
1747
+ ): Array<string | import("docx").Tab | import("docx").CarriageReturn> {
1748
+ const children: Array<string | import("docx").Tab | import("docx").CarriageReturn> = [];
1749
+ let start = 0;
1750
+ for (let index = 0; index < text.length; index += 1) {
1751
+ const character = text[index];
1752
+ if (character !== "\t" && character !== "\n") continue;
1753
+ if (index > start) children.push(text.slice(start, index));
1754
+ children.push(character === "\t" ? new docx.Tab() : new docx.CarriageReturn());
1755
+ start = index + 1;
1756
+ }
1757
+ if (start < text.length) children.push(text.slice(start));
1758
+ return children;
1759
+ }
1760
+
1761
+ function numberingConfig(docx: DocxModule): import("docx").INumberingOptions["config"] {
1762
+ const makeLevels = (kind: "bullet" | "number"): import("docx").ILevelsOptions[] =>
1763
+ Array.from({ length: 9 }, (_, level) => ({
1764
+ level,
1765
+ format: kind === "bullet" ? docx.LevelFormat.BULLET : docx.LevelFormat.DECIMAL,
1766
+ text: kind === "bullet" ? ["•", "◦", "▪"][level % 3]! : `%${level + 1}.`,
1767
+ alignment: docx.AlignmentType.LEFT,
1768
+ style: { paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } } },
1769
+ }));
1770
+ return [
1771
+ { reference: "opengeni-bullet", levels: makeLevels("bullet") },
1772
+ { reference: "opengeni-number", levels: makeLevels("number") },
1773
+ ];
1774
+ }
1775
+
1776
+ function buildListInstances(document: Document): Map<string, number> {
1777
+ const instances = new Map<string, number>();
1778
+ const explicitInstances = new Map<string, number>();
1779
+ let instance = 0;
1780
+ const visit = (blocks: readonly (DocumentBlock | DocumentStoryBlock)[]): void => {
1781
+ let activeKind: "bullet" | "number" | undefined;
1782
+ for (const block of blocks) {
1783
+ if (!(block instanceof DocumentParagraph) || !block.style.list) {
1784
+ activeKind = undefined;
1785
+ continue;
1786
+ }
1787
+ const explicitId = block.style.list.instanceId;
1788
+ if (explicitId) {
1789
+ const key = `${block.style.list.kind}:${explicitId}`;
1790
+ let explicitInstance = explicitInstances.get(key);
1791
+ if (explicitInstance === undefined) {
1792
+ explicitInstance = ++instance;
1793
+ explicitInstances.set(key, explicitInstance);
1794
+ }
1795
+ instances.set(block.id, explicitInstance);
1796
+ activeKind = undefined;
1797
+ continue;
1798
+ }
1799
+ if (activeKind !== block.style.list.kind) instance += 1;
1800
+ activeKind = block.style.list.kind;
1801
+ instances.set(block.id, instance);
1802
+ }
1803
+ };
1804
+ visit(document.blocks.items);
1805
+ for (const section of document.sections.items)
1806
+ for (const story of section.allStories()) visit(story.items);
1807
+ return instances;
1808
+ }
1809
+
1810
+ function headingLevel(
1811
+ level: DocumentParagraphStyle["headingLevel"],
1812
+ docx: DocxModule,
1813
+ ): (typeof import("docx").HeadingLevel)[keyof typeof import("docx").HeadingLevel] | undefined {
1814
+ if (!level) return undefined;
1815
+ return [
1816
+ docx.HeadingLevel.HEADING_1,
1817
+ docx.HeadingLevel.HEADING_2,
1818
+ docx.HeadingLevel.HEADING_3,
1819
+ docx.HeadingLevel.HEADING_4,
1820
+ docx.HeadingLevel.HEADING_5,
1821
+ docx.HeadingLevel.HEADING_6,
1822
+ ][level - 1];
1823
+ }
1824
+
1825
+ function alignment(
1826
+ value: DocumentParagraphStyle["alignment"],
1827
+ docx: DocxModule,
1828
+ ): (typeof import("docx").AlignmentType)[keyof typeof import("docx").AlignmentType] | undefined {
1829
+ if (!value) return undefined;
1830
+ return {
1831
+ left: docx.AlignmentType.LEFT,
1832
+ center: docx.AlignmentType.CENTER,
1833
+ right: docx.AlignmentType.RIGHT,
1834
+ justify: docx.AlignmentType.JUSTIFIED,
1835
+ }[value];
1836
+ }
1837
+
1838
+ function validateNonOverlappingChanges(changes: readonly TrackedChange[]): void {
1839
+ const sorted = [...changes].sort((a, b) => a.start - b.start || a.end - b.end);
1840
+ for (let index = 1; index < sorted.length; index++) {
1841
+ if (sorted[index]!.start < sorted[index - 1]!.end) {
1842
+ throw new UnsupportedArtifactFeatureError(
1843
+ "document",
1844
+ "overlapping tracked changes",
1845
+ "DOCX export",
1846
+ );
1847
+ }
1848
+ }
1849
+ }
1850
+
1851
+ function validateNonCrossingComments(comments: readonly DocumentCommentThread[]): void {
1852
+ const sorted = [...comments].sort(
1853
+ (left, right) => left.start - right.start || right.end - left.end,
1854
+ );
1855
+ const activeEnds: number[] = [];
1856
+ for (const comment of sorted) {
1857
+ while (activeEnds.length > 0 && comment.start >= activeEnds[activeEnds.length - 1]!) {
1858
+ activeEnds.pop();
1859
+ }
1860
+ const enclosingEnd = activeEnds[activeEnds.length - 1];
1861
+ if (enclosingEnd !== undefined && comment.end > enclosingEnd) {
1862
+ throw new UnsupportedArtifactFeatureError(
1863
+ "document",
1864
+ "crossing comment ranges",
1865
+ "DOCX export",
1866
+ );
1867
+ }
1868
+ if (comment.end > comment.start) activeEnds.push(comment.end);
1869
+ }
1870
+ }
1871
+
1872
+ function validateReviewMarkupForDocx(
1873
+ paragraph: DocumentParagraph,
1874
+ comments: readonly DocumentCommentThread[],
1875
+ changes: readonly TrackedChange[],
1876
+ ): void {
1877
+ const runSlices = paragraphRunSlices(paragraph);
1878
+ const sortedChanges = [...changes].sort(
1879
+ (left, right) => left.start - right.start || left.end - right.end,
1880
+ );
1881
+ let runIndex = 0;
1882
+ for (const change of sortedChanges) {
1883
+ while (runSlices[runIndex] && runSlices[runIndex]!.end <= change.start) runIndex += 1;
1884
+ let scanIndex = runIndex;
1885
+ let firstStyle: DocumentTextStyle | undefined;
1886
+ while (runSlices[scanIndex] && runSlices[scanIndex]!.start < change.end) {
1887
+ const style = runSlices[scanIndex]!.run.style;
1888
+ firstStyle ??= style;
1889
+ if (!sameTextStyle(style, firstStyle)) {
1890
+ throw new UnsupportedArtifactFeatureError(
1891
+ "document",
1892
+ "one tracked change spanning multiple text styles",
1893
+ "DOCX export",
1894
+ );
1895
+ }
1896
+ scanIndex += 1;
1897
+ }
1898
+ }
1899
+ const sortedComments = [...comments].sort(
1900
+ (left, right) => left.start - right.start || left.end - right.end,
1901
+ );
1902
+ let changeIndex = 0;
1903
+ for (const comment of sortedComments) {
1904
+ while (sortedChanges[changeIndex] && sortedChanges[changeIndex]!.end <= comment.start) {
1905
+ changeIndex += 1;
1906
+ }
1907
+ const change = sortedChanges[changeIndex];
1908
+ if (!change) continue;
1909
+ const pointInside =
1910
+ comment.start === comment.end && comment.start > change.start && comment.start < change.end;
1911
+ const rangesOverlap = comment.start < change.end && comment.end > change.start;
1912
+ const aligned = comment.start === change.start && comment.end === change.end;
1913
+ if (pointInside || (rangesOverlap && !aligned)) {
1914
+ throw new UnsupportedArtifactFeatureError(
1915
+ "document",
1916
+ "interleaved comments and tracked changes",
1917
+ "DOCX export",
1918
+ );
1919
+ }
1920
+ }
1921
+ }
1922
+
1923
+ function paragraphRunSlices(
1924
+ paragraph: DocumentParagraph,
1925
+ ): Array<{ start: number; end: number; run: DocumentTextRun }> {
1926
+ const result: Array<{ start: number; end: number; run: DocumentTextRun }> = [];
1927
+ let offset = 0;
1928
+ for (const run of paragraph.runs) {
1929
+ const start = offset;
1930
+ offset += run.text.length;
1931
+ result.push({ start, end: offset, run });
1932
+ }
1933
+ return result;
1934
+ }
1935
+
1936
+ function sameTextStyle(left: DocumentTextStyle, right: DocumentTextStyle): boolean {
1937
+ return (
1938
+ left.fontFamily === right.fontFamily &&
1939
+ left.fontSizePt === right.fontSizePt &&
1940
+ left.color === right.color &&
1941
+ left.bold === right.bold &&
1942
+ left.italic === right.italic &&
1943
+ left.underline === right.underline &&
1944
+ left.strike === right.strike
1945
+ );
1946
+ }
1947
+
1948
+ const MAX_DOCX_SOURCE_BYTES = 32 * 1024 * 1024;
1949
+
1950
+ function validateImportOptions(options: DocumentDocxImportOptions): void {
1951
+ assertPlainDataRecord(options, new Set(["unsupportedContent", "limits"]), "DOCX import options");
1952
+ if (
1953
+ options.unsupportedContent !== undefined &&
1954
+ options.unsupportedContent !== "preserve" &&
1955
+ options.unsupportedContent !== "error"
1956
+ )
1957
+ throw new TypeError("DOCX unsupportedContent import policy is invalid");
1958
+ if (options.limits !== undefined) {
1959
+ assertPlainDataRecord(options.limits, undefined, "DOCX import limits");
1960
+ }
1961
+ }
1962
+
1963
+ function validateExportOptions(options: DocumentDocxExportOptions): void {
1964
+ assertPlainDataRecord(
1965
+ options,
1966
+ new Set(["fileName", "unsupportedContent"]),
1967
+ "DOCX export options",
1968
+ );
1969
+ if (
1970
+ options.unsupportedContent !== undefined &&
1971
+ options.unsupportedContent !== "error" &&
1972
+ options.unsupportedContent !== "discard"
1973
+ )
1974
+ throw new TypeError("DOCX unsupportedContent export policy is invalid");
1975
+ if (
1976
+ options.fileName !== undefined &&
1977
+ (typeof options.fileName !== "string" ||
1978
+ options.fileName.length === 0 ||
1979
+ options.fileName.length > 255)
1980
+ )
1981
+ throw new TypeError("DOCX export fileName must contain 1 through 255 characters");
1982
+ }
1983
+
1984
+ function validateAttachOptions(options: Pick<DocumentDocxImportOptions, "limits">): void {
1985
+ assertPlainDataRecord(options, new Set(["limits"]), "DOCX envelope attach options");
1986
+ if (options.limits !== undefined) {
1987
+ assertPlainDataRecord(options.limits, undefined, "DOCX import limits");
1988
+ }
1989
+ }
1990
+
1991
+ function assertPlainDataRecord(
1992
+ value: unknown,
1993
+ allowedKeys: ReadonlySet<string> | undefined,
1994
+ label: string,
1995
+ ): asserts value is Record<string, unknown> {
1996
+ if (
1997
+ value === null ||
1998
+ typeof value !== "object" ||
1999
+ Array.isArray(value) ||
2000
+ Object.getPrototypeOf(value) !== Object.prototype
2001
+ )
2002
+ throw new TypeError(`${label} must be a plain data object`);
2003
+ if (Object.getOwnPropertySymbols(value).length > 0) {
2004
+ throw new TypeError(`${label} must not contain symbol properties`);
2005
+ }
2006
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
2007
+ if (!("value" in descriptor) || !descriptor.enumerable) {
2008
+ throw new TypeError(`${label} must contain only enumerable data properties`);
2009
+ }
2010
+ if (allowedKeys && !allowedKeys.has(key))
2011
+ throw new TypeError(`${label} contains unknown property ${key}`);
2012
+ }
2013
+ }
2014
+
2015
+ async function ownedDocxBytes(input: FileBlob | Blob | ArrayBuffer): Promise<Uint8Array> {
2016
+ if (input instanceof ArrayBuffer) {
2017
+ if (input.byteLength > MAX_DOCX_SOURCE_BYTES) {
2018
+ throw new DocxImportError("limit_exceeded", "Compressed DOCX exceeds maxCompressedBytes");
2019
+ }
2020
+ return new Uint8Array(input.slice(0));
2021
+ }
2022
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
2023
+ const size = Object.getOwnPropertyDescriptor(Blob.prototype, "size")?.get?.call(input);
2024
+ if (typeof size !== "number" || size > MAX_DOCX_SOURCE_BYTES) {
2025
+ throw new DocxImportError("limit_exceeded", "Compressed DOCX exceeds maxCompressedBytes");
2026
+ }
2027
+ return new Uint8Array(await Blob.prototype.arrayBuffer.call(input));
2028
+ }
2029
+ throw new DocxImportError("invalid_input", "DOCX input must be a Blob or ArrayBuffer");
2030
+ }
2031
+
2032
+ function ownedArrayBuffer(bytes: Uint8Array): ArrayBuffer {
2033
+ const copy = new Uint8Array(bytes.byteLength);
2034
+ copy.set(bytes);
2035
+ return copy.buffer;
2036
+ }
2037
+
2038
+ function deterministicCoreProperties(revision: number): string {
2039
+ return (
2040
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
2041
+ '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
2042
+ "<dc:creator>OpenGeni</dc:creator><cp:lastModifiedBy>OpenGeni</cp:lastModifiedBy>" +
2043
+ `<cp:revision>${revision}</cp:revision></cp:coreProperties>`
2044
+ );
2045
+ }
2046
+
2047
+ function hasOpaqueContent(content: DocumentOpaqueContent): boolean {
2048
+ return (
2049
+ content.parts.length > 0 || content.relationships.length > 0 || content.contentTypes.length > 0
2050
+ );
2051
+ }
2052
+
2053
+ function opaqueContentIssue(
2054
+ content: DocumentOpaqueContent,
2055
+ severity: DocumentFidelityIssue["severity"],
2056
+ ): DocumentFidelityIssue {
2057
+ return {
2058
+ code: severity === "warning" ? "content-preserved-in-source" : "content-will-be-discarded",
2059
+ severity,
2060
+ feature: "opaque-ooxml",
2061
+ message:
2062
+ severity === "warning"
2063
+ ? "The DOCX contains bounded inert OOXML outside the editable model; unchanged export preserves the package byte-for-byte"
2064
+ : "The DOCX contains bounded inert OOXML outside the editable model and cannot be imported under the requested strict fidelity policy",
2065
+ ...cloneOpaqueContent(content),
2066
+ };
2067
+ }
2068
+
2069
+ function cloneOpaqueContent(content: DocumentOpaqueContent): DocumentOpaqueContent {
2070
+ return {
2071
+ parts: [...content.parts],
2072
+ relationships: content.relationships.map((relationship) => ({ ...relationship })),
2073
+ contentTypes: content.contentTypes.map((contentType) => ({ ...contentType })),
2074
+ };
2075
+ }
2076
+
2077
+ function sameOpaqueContent(left: DocumentOpaqueContent, right: DocumentOpaqueContent): boolean {
2078
+ return JSON.stringify(left) === JSON.stringify(right);
2079
+ }
2080
+
2081
+ function normalizeLossPreservationEnvelope(value: unknown): DocumentLossPreservationEnvelope {
2082
+ assertPlainDataRecord(
2083
+ value,
2084
+ new Set([
2085
+ "version",
2086
+ "mediaType",
2087
+ "sourceBytes",
2088
+ "sourceDigest",
2089
+ "opaqueContent",
2090
+ "modelDigest",
2091
+ ]),
2092
+ "DOCX loss-preservation envelope",
2093
+ );
2094
+ if (value.version !== 1 || value.mediaType !== DOCX_MEDIA_TYPE) {
2095
+ throw new TypeError("DOCX loss-preservation envelope version or media type is invalid");
2096
+ }
2097
+ if (
2098
+ !(value.sourceBytes instanceof Uint8Array) ||
2099
+ Object.getPrototypeOf(value.sourceBytes) !== Uint8Array.prototype ||
2100
+ value.sourceBytes.byteLength > MAX_DOCX_SOURCE_BYTES
2101
+ )
2102
+ throw new TypeError("DOCX loss-preservation sourceBytes must be a bounded Uint8Array");
2103
+ if (typeof value.sourceDigest !== "string" || !/^[0-9a-f]{64}$/.test(value.sourceDigest)) {
2104
+ throw new TypeError("DOCX loss-preservation sourceDigest must be canonical SHA-256");
2105
+ }
2106
+ if (typeof value.modelDigest !== "string" || !/^[0-9a-f]{64}$/.test(value.modelDigest)) {
2107
+ throw new TypeError("DOCX loss-preservation modelDigest must be canonical SHA-256");
2108
+ }
2109
+ const opaqueContent = normalizeOpaqueContent(value.opaqueContent);
2110
+ if (!hasOpaqueContent(opaqueContent)) {
2111
+ throw new TypeError("DOCX loss-preservation envelope contains no opaque content");
2112
+ }
2113
+ return {
2114
+ version: 1,
2115
+ mediaType: DOCX_MEDIA_TYPE,
2116
+ sourceBytes: value.sourceBytes.slice(),
2117
+ sourceDigest: value.sourceDigest,
2118
+ opaqueContent,
2119
+ modelDigest: value.modelDigest,
2120
+ };
2121
+ }
2122
+
2123
+ function normalizeOpaqueContent(value: unknown): DocumentOpaqueContent {
2124
+ assertPlainDataRecord(
2125
+ value,
2126
+ new Set(["parts", "relationships", "contentTypes"]),
2127
+ "DOCX opaque-content metadata",
2128
+ );
2129
+ const parts = denseDataArray(value.parts, 1_024, "DOCX opaque part list").map((part) => {
2130
+ if (typeof part !== "string" || part.length === 0 || part.length > 1_025) {
2131
+ throw new TypeError("DOCX opaque part name is invalid");
2132
+ }
2133
+ return part;
2134
+ });
2135
+ const relationships = denseDataArray(
2136
+ value.relationships,
2137
+ 100_000,
2138
+ "DOCX opaque relationship list",
2139
+ ).map((relationship) => {
2140
+ assertPlainDataRecord(
2141
+ relationship,
2142
+ new Set(["sourcePart", "type", "targetPart"]),
2143
+ "DOCX opaque relationship",
2144
+ );
2145
+ const { sourcePart, type, targetPart } = relationship;
2146
+ if (
2147
+ typeof sourcePart !== "string" ||
2148
+ sourcePart.length > 1_025 ||
2149
+ typeof type !== "string" ||
2150
+ type.length === 0 ||
2151
+ type.length > 2_048 ||
2152
+ typeof targetPart !== "string" ||
2153
+ targetPart.length === 0 ||
2154
+ targetPart.length > 1_025
2155
+ )
2156
+ throw new TypeError("DOCX opaque relationship metadata is invalid");
2157
+ return { sourcePart, type, targetPart };
2158
+ });
2159
+ const contentTypes = denseDataArray(
2160
+ value.contentTypes,
2161
+ 1_024,
2162
+ "DOCX opaque content-type list",
2163
+ ).map((contentType) => {
2164
+ assertPlainDataRecord(
2165
+ contentType,
2166
+ new Set(["partName", "contentType"]),
2167
+ "DOCX opaque content type",
2168
+ );
2169
+ if (
2170
+ typeof contentType.partName !== "string" ||
2171
+ contentType.partName.length === 0 ||
2172
+ contentType.partName.length > 1_025 ||
2173
+ typeof contentType.contentType !== "string" ||
2174
+ contentType.contentType.length === 0 ||
2175
+ contentType.contentType.length > 2_048
2176
+ )
2177
+ throw new TypeError("DOCX opaque content-type metadata is invalid");
2178
+ return { partName: contentType.partName, contentType: contentType.contentType };
2179
+ });
2180
+ return { parts, relationships, contentTypes };
2181
+ }
2182
+
2183
+ function denseDataArray(value: unknown, maximum: number, label: string): unknown[] {
2184
+ if (
2185
+ !Array.isArray(value) ||
2186
+ Object.getPrototypeOf(value) !== Array.prototype ||
2187
+ value.length > maximum
2188
+ ) {
2189
+ throw new TypeError(`${label} must be a bounded ordinary array`);
2190
+ }
2191
+ const descriptors = Object.getOwnPropertyDescriptors(value);
2192
+ for (let index = 0; index < value.length; index += 1) {
2193
+ const descriptor = descriptors[String(index)];
2194
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
2195
+ throw new TypeError(`${label} must contain only dense data elements`);
2196
+ }
2197
+ }
2198
+ for (const key of Object.keys(descriptors)) {
2199
+ if (key !== "length" && !/^(?:0|[1-9][0-9]*)$/.test(key)) {
2200
+ throw new TypeError(`${label} contains an unknown property`);
2201
+ }
2202
+ }
2203
+ return value.slice();
2204
+ }
2205
+
2206
+ async function documentModelDigest(document: Document): Promise<string> {
2207
+ return sha256Hex(new TextEncoder().encode(JSON.stringify(document.toJSON())));
2208
+ }
2209
+
2210
+ async function sha256Hex(bytes: Uint8Array): Promise<string> {
2211
+ const ownedBytes = new Uint8Array(bytes.byteLength);
2212
+ ownedBytes.set(bytes);
2213
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", ownedBytes));
2214
+ return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2215
+ }
2216
+
2217
+ async function semanticNamespace(value: ImportedDocument): Promise<string> {
2218
+ const { opaqueContent: _opaqueContent, ...semanticValue } = value;
2219
+ const canonicalBytes = new TextEncoder().encode(JSON.stringify(semanticValue));
2220
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", canonicalBytes));
2221
+ return [...digest.subarray(0, 8)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2222
+ }
2223
+
2224
+ function appendMap<K, T>(map: Map<K, T[]>, key: K, value: T): void {
2225
+ const values = map.get(key);
2226
+ if (values) values.push(value);
2227
+ else map.set(key, [value]);
2228
+ }
2229
+
2230
+ function initials(author: string): string {
2231
+ const value = author
2232
+ .trim()
2233
+ .split(/\s+/u)
2234
+ .slice(0, 2)
2235
+ .map((part) => Array.from(part)[0]?.toUpperCase() ?? "")
2236
+ .join("");
2237
+ return Array.from(value).slice(0, 8).join("") || "U";
2238
+ }
2239
+
2240
+ function assertRepresentableCommentInitials(author: string, value: string | undefined): void {
2241
+ if (value !== undefined && value !== initials(author)) {
2242
+ throw new UnsupportedArtifactFeatureError("document", "custom comment initials", "DOCX import");
2243
+ }
2244
+ }
2245
+
2246
+ function ptToTwip(value: number): number {
2247
+ return Math.round(value * 20);
2248
+ }
2249
+
2250
+ function normalizeHex(value: string): string {
2251
+ if (!/^#?[0-9A-Fa-f]{6}$/.test(value)) throw new Error(`Invalid or unsafe color: ${value}`);
2252
+ return `#${value.replace(/^#/, "").toUpperCase()}`;
2253
+ }