@beyondwork/docx-react-component 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +325 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1506 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,265 @@
1
+ import type { RevisionRecord } from "../../review/store/revision-types.ts";
2
+ import {
3
+ mapRevisionBoundaries,
4
+ type RevisionParagraphBoundary,
5
+ } from "../ooxml/revision-boundaries.ts";
6
+
7
+ interface XmlReplacement {
8
+ start: number;
9
+ end: number;
10
+ replacement: string;
11
+ }
12
+
13
+ export interface SerializedRuntimeRevisionsResult {
14
+ documentXml: string;
15
+ serializedRevisionIds: string[];
16
+ skippedRevisionIds: string[];
17
+ }
18
+
19
+ export function serializeRuntimeRevisionsIntoDocumentXml(
20
+ documentXml: string,
21
+ revisions: readonly RevisionRecord[],
22
+ boundaries: readonly RevisionParagraphBoundary[] = mapRevisionBoundaries(documentXml),
23
+ ): SerializedRuntimeRevisionsResult {
24
+ const replacements: XmlReplacement[] = [];
25
+ const serializedRevisionIds: string[] = [];
26
+ const skippedRevisionIds: string[] = [];
27
+ const paragraphMarkers = new Map<
28
+ number,
29
+ { boundary: RevisionParagraphBoundary; markers: string[]; revisionIds: string[] }
30
+ >();
31
+
32
+ for (const revision of revisions) {
33
+ if (revision.status !== "active" || revision.anchor.kind !== "range") {
34
+ continue;
35
+ }
36
+
37
+ if (revision.kind !== "insertion" && revision.kind !== "deletion") {
38
+ continue;
39
+ }
40
+
41
+ const form = revision.metadata.importedRevisionForm;
42
+ if (form === "paragraph-insertion" || form === "paragraph-deletion") {
43
+ const paragraphBoundary = findParagraphBoundaryForAnchor(boundaries, revision);
44
+ if (!paragraphBoundary) {
45
+ skippedRevisionIds.push(revision.revisionId);
46
+ continue;
47
+ }
48
+
49
+ const entry = paragraphMarkers.get(paragraphBoundary.paragraphIndex) ?? {
50
+ boundary: paragraphBoundary,
51
+ markers: [],
52
+ revisionIds: [],
53
+ };
54
+ entry.markers.push(createParagraphRevisionMarker(revision));
55
+ entry.revisionIds.push(revision.revisionId);
56
+ paragraphMarkers.set(paragraphBoundary.paragraphIndex, entry);
57
+ serializedRevisionIds.push(revision.revisionId);
58
+ continue;
59
+ }
60
+
61
+ const rangeReplacement = createRangeRevisionReplacement(documentXml, boundaries, revision);
62
+ if (!rangeReplacement) {
63
+ skippedRevisionIds.push(revision.revisionId);
64
+ continue;
65
+ }
66
+
67
+ replacements.push(rangeReplacement);
68
+ serializedRevisionIds.push(revision.revisionId);
69
+ }
70
+
71
+ for (const { boundary, markers, revisionIds } of paragraphMarkers.values()) {
72
+ const paragraphInsertion = createParagraphRevisionInsertion(
73
+ documentXml,
74
+ boundary,
75
+ markers,
76
+ );
77
+ if (!paragraphInsertion) {
78
+ skippedRevisionIds.push(...revisionIds);
79
+ continue;
80
+ }
81
+
82
+ replacements.push(paragraphInsertion);
83
+ }
84
+
85
+ return {
86
+ documentXml: applyReplacements(documentXml, replacements),
87
+ serializedRevisionIds,
88
+ skippedRevisionIds,
89
+ };
90
+ }
91
+
92
+ function createRangeRevisionReplacement(
93
+ documentXml: string,
94
+ boundaries: readonly RevisionParagraphBoundary[],
95
+ revision: RevisionRecord,
96
+ ): XmlReplacement | undefined {
97
+ const paragraphBoundary = findParagraphBoundaryForRange(boundaries, revision.anchor.range.from, revision.anchor.range.to);
98
+ if (!paragraphBoundary) {
99
+ return undefined;
100
+ }
101
+
102
+ const startIndex = paragraphBoundary.boundaries.get(revision.anchor.range.from);
103
+ const endIndex = paragraphBoundary.boundaries.get(revision.anchor.range.to);
104
+ if (startIndex === undefined || endIndex === undefined || endIndex < startIndex) {
105
+ return undefined;
106
+ }
107
+
108
+ const xml = documentXml.slice(startIndex, endIndex);
109
+ const attributes = serializeRevisionAttributes(revision);
110
+ return {
111
+ start: startIndex,
112
+ end: endIndex,
113
+ replacement:
114
+ revision.kind === "insertion"
115
+ ? `<w:ins${attributes}>${xml}</w:ins>`
116
+ : `<w:del${attributes}>${convertRunsToDeletedContent(xml)}</w:del>`,
117
+ };
118
+ }
119
+
120
+ function createParagraphRevisionMarker(revision: RevisionRecord): string {
121
+ const markerName = revision.kind === "insertion" ? "w:ins" : "w:del";
122
+ return `<${markerName}${serializeRevisionAttributes(revision)}/>`;
123
+ }
124
+
125
+ function createParagraphRevisionInsertion(
126
+ documentXml: string,
127
+ paragraphBoundary: RevisionParagraphBoundary,
128
+ markers: readonly string[],
129
+ ): XmlReplacement | undefined {
130
+ const paragraphXml = documentXml.slice(
131
+ paragraphBoundary.paragraphStart,
132
+ paragraphBoundary.paragraphEnd,
133
+ );
134
+ const markerXml = markers.join("");
135
+ const paragraphRunPropertiesInsertionIndex = findClosingTagInsertionIndex(
136
+ documentXml,
137
+ paragraphBoundary.paragraphRunPropertiesStart,
138
+ paragraphBoundary.paragraphRunPropertiesEnd,
139
+ "w:rPr",
140
+ );
141
+ if (paragraphRunPropertiesInsertionIndex !== undefined) {
142
+ return {
143
+ start: paragraphRunPropertiesInsertionIndex,
144
+ end: paragraphRunPropertiesInsertionIndex,
145
+ replacement: markerXml,
146
+ };
147
+ }
148
+
149
+ const paragraphPropertiesInsertionIndex = findClosingTagInsertionIndex(
150
+ documentXml,
151
+ paragraphBoundary.paragraphPropertiesStart,
152
+ paragraphBoundary.paragraphPropertiesEnd,
153
+ "w:pPr",
154
+ );
155
+ if (paragraphPropertiesInsertionIndex !== undefined) {
156
+ return {
157
+ start: paragraphPropertiesInsertionIndex,
158
+ end: paragraphPropertiesInsertionIndex,
159
+ replacement: `<w:rPr>${markerXml}</w:rPr>`,
160
+ };
161
+ }
162
+
163
+ if (!/<w:p[\s>]/u.test(paragraphXml)) {
164
+ return undefined;
165
+ }
166
+
167
+ return {
168
+ start: paragraphBoundary.paragraphStartTagEnd,
169
+ end: paragraphBoundary.paragraphStartTagEnd,
170
+ replacement: `<w:pPr><w:rPr>${markerXml}</w:rPr></w:pPr>`,
171
+ };
172
+ }
173
+
174
+ function findParagraphBoundaryForRange(
175
+ boundaries: readonly RevisionParagraphBoundary[],
176
+ from: number,
177
+ to: number,
178
+ ): RevisionParagraphBoundary | undefined {
179
+ return boundaries.find(
180
+ (boundary) => from >= boundary.start && to <= boundary.end,
181
+ );
182
+ }
183
+
184
+ function findParagraphBoundaryForAnchor(
185
+ boundaries: readonly RevisionParagraphBoundary[],
186
+ revision: RevisionRecord,
187
+ ): RevisionParagraphBoundary | undefined {
188
+ const anchor = revision.anchor.kind === "range" ? revision.anchor.range.from : undefined;
189
+ if (anchor === undefined) {
190
+ return undefined;
191
+ }
192
+
193
+ return boundaries.find(
194
+ (boundary) =>
195
+ boundary.end === anchor ||
196
+ (anchor >= boundary.start && anchor <= boundary.end),
197
+ );
198
+ }
199
+
200
+ function serializeRevisionAttributes(revision: RevisionRecord): string {
201
+ const attributes = {
202
+ "w:id": revision.metadata.ooxmlRevisionId ?? sanitizeRevisionId(revision.revisionId),
203
+ "w:author": revision.authorId,
204
+ "w:date": revision.createdAt,
205
+ };
206
+
207
+ return Object.entries(attributes)
208
+ .filter(([, value]) => value && value.length > 0)
209
+ .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
210
+ .join("");
211
+ }
212
+
213
+ function findClosingTagInsertionIndex(
214
+ documentXml: string,
215
+ start: number | undefined,
216
+ end: number | undefined,
217
+ tagName: string,
218
+ ): number | undefined {
219
+ if (start === undefined || end === undefined) {
220
+ return undefined;
221
+ }
222
+
223
+ const closingTag = `</${tagName}>`;
224
+ const closingIndex = documentXml.lastIndexOf(closingTag, end);
225
+ if (closingIndex < start) {
226
+ return undefined;
227
+ }
228
+
229
+ return closingIndex;
230
+ }
231
+
232
+ function sanitizeRevisionId(revisionId: string): string {
233
+ const numericTail = /(\d+)$/.exec(revisionId)?.[1];
234
+ return numericTail ?? revisionId.replace(/[^A-Za-z0-9._-]/g, "-");
235
+ }
236
+
237
+ function convertRunsToDeletedContent(xml: string): string {
238
+ return xml
239
+ .replace(/<(\/?)w:t\b/g, "<$1w:delText")
240
+ .replace(/<(\/?)w:instrText\b/g, "<$1w:delInstrText");
241
+ }
242
+
243
+ function applyReplacements(documentXml: string, replacements: readonly XmlReplacement[]): string {
244
+ const sorted = replacements
245
+ .slice()
246
+ .sort((left, right) => right.start - left.start || right.end - left.end);
247
+ let output = documentXml;
248
+
249
+ for (const replacement of sorted) {
250
+ output =
251
+ output.slice(0, replacement.start) +
252
+ replacement.replacement +
253
+ output.slice(replacement.end);
254
+ }
255
+
256
+ return output;
257
+ }
258
+
259
+ function escapeAttribute(value: string): string {
260
+ return value
261
+ .replace(/&/g, "&amp;")
262
+ .replace(/</g, "&lt;")
263
+ .replace(/>/g, "&gt;")
264
+ .replace(/"/g, "&quot;");
265
+ }
@@ -0,0 +1,147 @@
1
+ import type {
2
+ ParsedBorderSpec,
3
+ ParsedCellMargins,
4
+ ParsedCellShading,
5
+ ParsedTable,
6
+ ParsedTableBorders,
7
+ ParsedTableCell,
8
+ ParsedTableCellBorders,
9
+ ParsedTableRow,
10
+ ParsedTableWidth,
11
+ } from "../ooxml/parse-tables.ts";
12
+
13
+ export function serializeTable(table: ParsedTable): string {
14
+ const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
15
+ const gridXml =
16
+ table.gridColumns.length > 0
17
+ ? `<w:tblGrid>${table.gridColumns
18
+ .map((width) => `<w:gridCol w:w="${width}"/>`)
19
+ .join("")}</w:tblGrid>`
20
+ : "";
21
+ const rowsXml = table.rows.map(serializeRow).join("");
22
+
23
+ return `<w:tbl>${propertiesXml}${gridXml}${rowsXml}</w:tbl>`;
24
+ }
25
+
26
+ function serializeRow(row: ParsedTableRow): string {
27
+ const propertiesXml = row.propertiesXml ?? buildRowPropertiesXml(row);
28
+ return `<w:tr>${propertiesXml}${row.cells.map(serializeCell).join("")}</w:tr>`;
29
+ }
30
+
31
+ function serializeCell(cell: ParsedTableCell): string {
32
+ const propertiesXml = ensureCellProperties(cell);
33
+ const blocksXml = cell.blocksXml.length > 0 ? cell.blocksXml.join("") : "<w:p/>";
34
+ return `<w:tc>${propertiesXml}${blocksXml}</w:tc>`;
35
+ }
36
+
37
+ function buildTablePropertiesXml(table: ParsedTable): string {
38
+ const children: string[] = [];
39
+ if (table.width) {
40
+ children.push(serializeWidth("tblW", table.width));
41
+ }
42
+ if (table.alignment) {
43
+ children.push(`<w:jc w:val="${table.alignment}"/>`);
44
+ }
45
+ if (table.borders) {
46
+ const bordersXml = serializeTableBorders(table.borders);
47
+ if (bordersXml) children.push(`<w:tblBorders>${bordersXml}</w:tblBorders>`);
48
+ }
49
+ if (table.cellMargins) {
50
+ const marginsXml = serializeTableCellMargins(table.cellMargins);
51
+ if (marginsXml) children.push(`<w:tblCellMar>${marginsXml}</w:tblCellMar>`);
52
+ }
53
+ return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
54
+ }
55
+
56
+ function buildRowPropertiesXml(row: ParsedTableRow): string {
57
+ const children: string[] = [];
58
+ if (row.height !== undefined) {
59
+ const hRuleAttr = row.heightRule ? ` w:hRule="${row.heightRule}"` : "";
60
+ children.push(`<w:trHeight w:val="${row.height}"${hRuleAttr}/>`);
61
+ }
62
+ if (row.isHeader) {
63
+ children.push(`<w:tblHeader/>`);
64
+ }
65
+ return children.length > 0 ? `<w:trPr>${children.join("")}</w:trPr>` : "";
66
+ }
67
+
68
+ function ensureCellProperties(cell: ParsedTableCell): string {
69
+ if (cell.propertiesXml) {
70
+ return cell.propertiesXml;
71
+ }
72
+
73
+ const children: string[] = [];
74
+ if (cell.width) {
75
+ children.push(serializeWidth("tcW", cell.width));
76
+ }
77
+ if (cell.gridSpan && cell.gridSpan > 1) {
78
+ children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
79
+ }
80
+ if (cell.verticalMerge) {
81
+ children.push(
82
+ cell.verticalMerge === "restart"
83
+ ? `<w:vMerge w:val="restart"/>`
84
+ : `<w:vMerge/>`,
85
+ );
86
+ }
87
+ if (cell.borders) {
88
+ const bordersXml = serializeCellBorders(cell.borders);
89
+ if (bordersXml) children.push(`<w:tcBorders>${bordersXml}</w:tcBorders>`);
90
+ }
91
+ if (cell.shading) {
92
+ children.push(serializeCellShading(cell.shading));
93
+ }
94
+ if (cell.verticalAlign) {
95
+ children.push(`<w:vAlign w:val="${cell.verticalAlign}"/>`);
96
+ }
97
+
98
+ return children.length > 0 ? `<w:tcPr>${children.join("")}</w:tcPr>` : "";
99
+ }
100
+
101
+ function serializeWidth(element: string, width: ParsedTableWidth): string {
102
+ return `<w:${element} w:w="${width.value}" w:type="${width.type}"/>`;
103
+ }
104
+
105
+ function serializeBorderSpec(element: string, spec: ParsedBorderSpec): string {
106
+ const attrs: string[] = [];
107
+ if (spec.value) attrs.push(`w:val="${spec.value}"`);
108
+ if (spec.size !== undefined) attrs.push(`w:sz="${spec.size}"`);
109
+ if (spec.space !== undefined) attrs.push(`w:space="${spec.space}"`);
110
+ if (spec.color) attrs.push(`w:color="${spec.color}"`);
111
+ const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
112
+ return `<w:${element}${attrsStr}/>`;
113
+ }
114
+
115
+ function serializeTableBorders(borders: ParsedTableBorders): string {
116
+ const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
117
+ return sides
118
+ .filter((side) => borders[side] !== undefined)
119
+ .map((side) => serializeBorderSpec(side, borders[side]!))
120
+ .join("");
121
+ }
122
+
123
+ function serializeCellBorders(borders: ParsedTableCellBorders): string {
124
+ const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
125
+ return sides
126
+ .filter((side) => borders[side] !== undefined)
127
+ .map((side) => serializeBorderSpec(side, borders[side]!))
128
+ .join("");
129
+ }
130
+
131
+ function serializeCellShading(shading: ParsedCellShading): string {
132
+ const attrs: string[] = [];
133
+ if (shading.val) attrs.push(`w:val="${shading.val}"`);
134
+ if (shading.color) attrs.push(`w:color="${shading.color}"`);
135
+ if (shading.fill) attrs.push(`w:fill="${shading.fill}"`);
136
+ const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
137
+ return `<w:shd${attrsStr}/>`;
138
+ }
139
+
140
+ function serializeTableCellMargins(margins: ParsedCellMargins): string {
141
+ const parts: string[] = [];
142
+ if (margins.top !== undefined) parts.push(`<w:top w:w="${margins.top}" w:type="dxa"/>`);
143
+ if (margins.left !== undefined) parts.push(`<w:left w:w="${margins.left}" w:type="dxa"/>`);
144
+ if (margins.bottom !== undefined) parts.push(`<w:bottom w:w="${margins.bottom}" w:type="dxa"/>`);
145
+ if (margins.right !== undefined) parts.push(`<w:right w:w="${margins.right}" w:type="dxa"/>`);
146
+ return parts.join("");
147
+ }
@@ -0,0 +1,194 @@
1
+ import type {
2
+ DocumentRootNode,
3
+ HyperlinkNode,
4
+ InlineNode,
5
+ ParagraphNode,
6
+ TextNode,
7
+ } from "../../model/canonical-document.ts";
8
+ import type { CommentThread } from "../../review/store/comment-store.ts";
9
+ import type { RevisionRecord } from "../../review/store/revision-types.ts";
10
+
11
+ export function splitDocumentAtReviewBoundaries(
12
+ content: DocumentRootNode,
13
+ comments: readonly CommentThread[],
14
+ revisions: readonly RevisionRecord[],
15
+ ): DocumentRootNode {
16
+ const splitPositions = collectSplitPositions(comments, revisions);
17
+ if (splitPositions.size === 0) {
18
+ return content;
19
+ }
20
+
21
+ let cursor = 0;
22
+ const children = content.children.map((block, index) => {
23
+ if (index > 0 && content.children[index - 1]?.type === "paragraph" && block.type === "paragraph") {
24
+ cursor += 1;
25
+ }
26
+
27
+ if (block.type !== "paragraph") {
28
+ cursor += 1;
29
+ return block;
30
+ }
31
+
32
+ const next = splitParagraph(block, splitPositions, cursor);
33
+ cursor = next.cursor;
34
+ return next.paragraph;
35
+ });
36
+
37
+ return {
38
+ type: "doc",
39
+ children,
40
+ };
41
+ }
42
+
43
+ function collectSplitPositions(
44
+ comments: readonly CommentThread[],
45
+ revisions: readonly RevisionRecord[],
46
+ ): Set<number> {
47
+ const positions = new Set<number>();
48
+
49
+ for (const thread of comments) {
50
+ if (thread.anchor.kind !== "range") {
51
+ continue;
52
+ }
53
+
54
+ positions.add(thread.anchor.range.from);
55
+ positions.add(thread.anchor.range.to);
56
+ }
57
+
58
+ for (const revision of revisions) {
59
+ if (revision.status !== "active" || revision.anchor.kind !== "range") {
60
+ continue;
61
+ }
62
+
63
+ if (
64
+ revision.metadata.importedRevisionForm === "paragraph-insertion" ||
65
+ revision.metadata.importedRevisionForm === "paragraph-deletion"
66
+ ) {
67
+ continue;
68
+ }
69
+
70
+ positions.add(revision.anchor.range.from);
71
+ positions.add(revision.anchor.range.to);
72
+ }
73
+
74
+ return positions;
75
+ }
76
+
77
+ function splitParagraph(
78
+ paragraph: ParagraphNode,
79
+ splitPositions: ReadonlySet<number>,
80
+ cursor: number,
81
+ ): { paragraph: ParagraphNode; cursor: number } {
82
+ const children: InlineNode[] = [];
83
+ let nextCursor = cursor;
84
+
85
+ for (const child of paragraph.children) {
86
+ if (child.type === "text") {
87
+ const split = splitTextNode(child, splitPositions, nextCursor);
88
+ children.push(...split.children);
89
+ nextCursor = split.cursor;
90
+ continue;
91
+ }
92
+
93
+ if (child.type === "hyperlink") {
94
+ const split = splitHyperlinkNode(child, splitPositions, nextCursor);
95
+ children.push(...split.children);
96
+ nextCursor = split.cursor;
97
+ continue;
98
+ }
99
+
100
+ children.push(child);
101
+ nextCursor += 1;
102
+ }
103
+
104
+ return {
105
+ paragraph: {
106
+ ...paragraph,
107
+ children,
108
+ },
109
+ cursor: nextCursor,
110
+ };
111
+ }
112
+
113
+ function splitHyperlinkNode(
114
+ node: HyperlinkNode,
115
+ splitPositions: ReadonlySet<number>,
116
+ cursor: number,
117
+ ): { children: HyperlinkNode[]; cursor: number } {
118
+ let nextCursor = cursor;
119
+ const groups: Array<HyperlinkNode["children"]> = [[]];
120
+
121
+ for (const child of node.children) {
122
+ if (child.type === "text") {
123
+ const split = splitTextNode(child, splitPositions, nextCursor);
124
+ for (const piece of split.children) {
125
+ groups[groups.length - 1]?.push(piece);
126
+ nextCursor += Array.from(piece.text).length;
127
+ if (splitPositions.has(nextCursor)) {
128
+ groups.push([]);
129
+ }
130
+ }
131
+ continue;
132
+ }
133
+
134
+ groups[groups.length - 1]?.push(child);
135
+ nextCursor += 1;
136
+ if (splitPositions.has(nextCursor)) {
137
+ groups.push([]);
138
+ }
139
+ }
140
+
141
+ const hyperlinks = groups
142
+ .filter((children) => children.length > 0)
143
+ .map((children) => ({
144
+ type: "hyperlink" as const,
145
+ href: node.href,
146
+ children,
147
+ }));
148
+
149
+ return {
150
+ children: hyperlinks.length > 0 ? hyperlinks : [node],
151
+ cursor: nextCursor,
152
+ };
153
+ }
154
+
155
+ function splitTextNode(
156
+ node: TextNode,
157
+ splitPositions: ReadonlySet<number>,
158
+ cursor: number,
159
+ ): { children: TextNode[]; cursor: number } {
160
+ const codepoints = Array.from(node.text);
161
+ if (codepoints.length === 0) {
162
+ return { children: [], cursor };
163
+ }
164
+
165
+ const boundaries = new Set<number>([0, codepoints.length]);
166
+ for (let index = 1; index < codepoints.length; index += 1) {
167
+ if (splitPositions.has(cursor + index)) {
168
+ boundaries.add(index);
169
+ }
170
+ }
171
+
172
+ const ordered = [...boundaries].sort((left, right) => left - right);
173
+ const children: TextNode[] = [];
174
+
175
+ for (let index = 0; index < ordered.length - 1; index += 1) {
176
+ const start = ordered[index] ?? 0;
177
+ const end = ordered[index + 1] ?? codepoints.length;
178
+ const text = codepoints.slice(start, end).join("");
179
+ if (text.length === 0) {
180
+ continue;
181
+ }
182
+
183
+ children.push({
184
+ type: "text",
185
+ text,
186
+ ...(node.marks && node.marks.length > 0 ? { marks: node.marks } : {}),
187
+ });
188
+ }
189
+
190
+ return {
191
+ children: children.length > 0 ? children : [node],
192
+ cursor: cursor + codepoints.length,
193
+ };
194
+ }
@@ -0,0 +1,3 @@
1
+ # IO Normalize
2
+
3
+ Normalization from OOXML structures into canonical state belongs here.