@beyondwork/docx-react-component 1.0.27 → 1.0.29

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 (356) hide show
  1. package/dist/canonical-document-BLEbzL2J.d.cts +844 -0
  2. package/dist/canonical-document-BLEbzL2J.d.ts +844 -0
  3. package/dist/chunk-2FJS5GZM.js +763 -0
  4. package/dist/chunk-2FJS5GZM.js.map +1 -0
  5. package/{src/core/commands/section-layout-commands.ts → dist/chunk-2OQBZS3F.js} +106 -340
  6. package/dist/chunk-2OQBZS3F.js.map +1 -0
  7. package/dist/chunk-2S7W4KFO.js +127 -0
  8. package/dist/chunk-2S7W4KFO.js.map +1 -0
  9. package/dist/chunk-2TG72QSW.js +3874 -0
  10. package/dist/chunk-2TG72QSW.js.map +1 -0
  11. package/{src/core/commands/table-structure-commands.ts → dist/chunk-36QNIZBO.js} +126 -315
  12. package/dist/chunk-36QNIZBO.js.map +1 -0
  13. package/dist/chunk-4AQOYAW4.js +3069 -0
  14. package/dist/chunk-4AQOYAW4.js.map +1 -0
  15. package/dist/chunk-4D5EWJ3P.js +77 -0
  16. package/dist/chunk-4D5EWJ3P.js.map +1 -0
  17. package/dist/chunk-5FN54NDH.js +2257 -0
  18. package/dist/chunk-5FN54NDH.js.map +1 -0
  19. package/dist/chunk-BOYGQYRQ.js +7306 -0
  20. package/dist/chunk-BOYGQYRQ.js.map +1 -0
  21. package/dist/chunk-CN3XMECL.js +212 -0
  22. package/dist/chunk-CN3XMECL.js.map +1 -0
  23. package/dist/chunk-EBI3BX6U.js +164 -0
  24. package/dist/chunk-EBI3BX6U.js.map +1 -0
  25. package/dist/chunk-EILUG3VB.js +1275 -0
  26. package/dist/chunk-EILUG3VB.js.map +1 -0
  27. package/dist/chunk-FUDY333O.js +70 -0
  28. package/dist/chunk-FUDY333O.js.map +1 -0
  29. package/dist/chunk-GBVOWFIK.js +1237 -0
  30. package/dist/chunk-GBVOWFIK.js.map +1 -0
  31. package/dist/chunk-H4TQ3H3Y.js +262 -0
  32. package/dist/chunk-H4TQ3H3Y.js.map +1 -0
  33. package/{src/core/commands/style-commands.ts → dist/chunk-JGB3IXZO.js} +40 -113
  34. package/dist/chunk-JGB3IXZO.js.map +1 -0
  35. package/dist/chunk-KD2QRQPY.js +4342 -0
  36. package/dist/chunk-KD2QRQPY.js.map +1 -0
  37. package/dist/chunk-KLMXQVYK.js +369 -0
  38. package/dist/chunk-KLMXQVYK.js.map +1 -0
  39. package/dist/chunk-KZUG5KFQ.js +214 -0
  40. package/dist/chunk-KZUG5KFQ.js.map +1 -0
  41. package/{src/core/state/text-transaction.ts → dist/chunk-QDAQ4CJU.js} +79 -236
  42. package/dist/chunk-QDAQ4CJU.js.map +1 -0
  43. package/{src/legal/bookmarks.ts → dist/chunk-RMH72RZI.js} +44 -130
  44. package/dist/chunk-RMH72RZI.js.map +1 -0
  45. package/dist/chunk-SWKWQZXM.js +117 -0
  46. package/dist/chunk-SWKWQZXM.js.map +1 -0
  47. package/{src/core/commands/formatting-commands.ts → dist/chunk-TJBP2K4T.js} +196 -536
  48. package/dist/chunk-TJBP2K4T.js.map +1 -0
  49. package/dist/chunk-TLCEAQDQ.js +542 -0
  50. package/dist/chunk-TLCEAQDQ.js.map +1 -0
  51. package/{src/core/commands/text-commands.ts → dist/chunk-UZXBISGO.js} +86 -142
  52. package/dist/chunk-UZXBISGO.js.map +1 -0
  53. package/dist/chunk-WGBAKP3Q.js +3220 -0
  54. package/dist/chunk-WGBAKP3Q.js.map +1 -0
  55. package/dist/compare/index.cjs +5475 -0
  56. package/dist/compare/index.cjs.map +1 -0
  57. package/dist/compare/index.d.cts +114 -0
  58. package/dist/compare/index.d.ts +114 -0
  59. package/dist/compare/index.js +731 -0
  60. package/dist/compare/index.js.map +1 -0
  61. package/dist/core/commands/formatting-commands.cjs +828 -0
  62. package/dist/core/commands/formatting-commands.cjs.map +1 -0
  63. package/dist/core/commands/formatting-commands.d.cts +63 -0
  64. package/dist/core/commands/formatting-commands.d.ts +63 -0
  65. package/dist/core/commands/formatting-commands.js +37 -0
  66. package/dist/core/commands/formatting-commands.js.map +1 -0
  67. package/dist/core/commands/image-commands.cjs +2023 -0
  68. package/dist/core/commands/image-commands.cjs.map +1 -0
  69. package/dist/core/commands/image-commands.d.cts +58 -0
  70. package/dist/core/commands/image-commands.d.ts +58 -0
  71. package/dist/core/commands/image-commands.js +18 -0
  72. package/dist/core/commands/image-commands.js.map +1 -0
  73. package/dist/core/commands/section-layout-commands.cjs +477 -0
  74. package/dist/core/commands/section-layout-commands.cjs.map +1 -0
  75. package/dist/core/commands/section-layout-commands.d.cts +62 -0
  76. package/dist/core/commands/section-layout-commands.d.ts +62 -0
  77. package/dist/core/commands/section-layout-commands.js +21 -0
  78. package/dist/core/commands/section-layout-commands.js.map +1 -0
  79. package/dist/core/commands/style-commands.cjs +214 -0
  80. package/dist/core/commands/style-commands.cjs.map +1 -0
  81. package/dist/core/commands/style-commands.d.cts +13 -0
  82. package/dist/core/commands/style-commands.d.ts +13 -0
  83. package/dist/core/commands/style-commands.js +9 -0
  84. package/dist/core/commands/style-commands.js.map +1 -0
  85. package/dist/core/commands/table-structure-commands.cjs +1883 -0
  86. package/dist/core/commands/table-structure-commands.cjs.map +1 -0
  87. package/dist/core/commands/table-structure-commands.d.cts +59 -0
  88. package/dist/core/commands/table-structure-commands.d.ts +59 -0
  89. package/dist/core/commands/table-structure-commands.js +12 -0
  90. package/dist/core/commands/table-structure-commands.js.map +1 -0
  91. package/dist/core/commands/text-commands.cjs +2391 -0
  92. package/dist/core/commands/text-commands.cjs.map +1 -0
  93. package/dist/core/commands/text-commands.d.cts +24 -0
  94. package/dist/core/commands/text-commands.d.ts +24 -0
  95. package/dist/core/commands/text-commands.js +28 -0
  96. package/dist/core/commands/text-commands.js.map +1 -0
  97. package/dist/core/selection/mapping.cjs +200 -0
  98. package/dist/core/selection/mapping.cjs.map +1 -0
  99. package/dist/core/selection/mapping.d.cts +2 -0
  100. package/dist/core/selection/mapping.d.ts +2 -0
  101. package/dist/core/selection/mapping.js +31 -0
  102. package/dist/core/selection/mapping.js.map +1 -0
  103. package/dist/core/state/editor-state.cjs +2278 -0
  104. package/dist/core/state/editor-state.cjs.map +1 -0
  105. package/dist/core/state/editor-state.d.cts +2 -0
  106. package/dist/core/state/editor-state.d.ts +2 -0
  107. package/dist/core/state/editor-state.js +26 -0
  108. package/dist/core/state/editor-state.js.map +1 -0
  109. package/dist/index.cjs +38553 -0
  110. package/dist/index.cjs.map +1 -0
  111. package/dist/index.d.cts +15 -0
  112. package/dist/index.d.ts +15 -0
  113. package/dist/index.js +7856 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/io/docx-session.cjs +16236 -0
  116. package/dist/io/docx-session.cjs.map +1 -0
  117. package/dist/io/docx-session.d.cts +21 -0
  118. package/dist/io/docx-session.d.ts +21 -0
  119. package/dist/io/docx-session.js +18 -0
  120. package/dist/io/docx-session.js.map +1 -0
  121. package/dist/legal/index.cjs +3900 -0
  122. package/dist/legal/index.cjs.map +1 -0
  123. package/dist/legal/index.d.cts +86 -0
  124. package/dist/legal/index.d.ts +86 -0
  125. package/dist/legal/index.js +616 -0
  126. package/dist/legal/index.js.map +1 -0
  127. package/dist/public-types-7ZL_94cz.d.ts +1573 -0
  128. package/dist/public-types-CeMaDueh.d.cts +1573 -0
  129. package/dist/public-types.cjs +19 -0
  130. package/dist/public-types.cjs.map +1 -0
  131. package/dist/public-types.d.cts +2 -0
  132. package/dist/public-types.d.ts +2 -0
  133. package/dist/public-types.js +1 -0
  134. package/dist/public-types.js.map +1 -0
  135. package/dist/runtime/document-runtime.cjs +11140 -0
  136. package/dist/runtime/document-runtime.cjs.map +1 -0
  137. package/dist/runtime/document-runtime.d.cts +231 -0
  138. package/dist/runtime/document-runtime.d.ts +231 -0
  139. package/dist/runtime/document-runtime.js +21 -0
  140. package/dist/runtime/document-runtime.js.map +1 -0
  141. package/dist/structural-helpers-CilgOVhh.d.cts +10 -0
  142. package/dist/structural-helpers-q0Gd-eBN.d.ts +10 -0
  143. package/dist/ui-tailwind/editor-surface/search-plugin.cjs +313 -0
  144. package/dist/ui-tailwind/editor-surface/search-plugin.cjs.map +1 -0
  145. package/dist/ui-tailwind/editor-surface/search-plugin.d.cts +67 -0
  146. package/dist/ui-tailwind/editor-surface/search-plugin.d.ts +67 -0
  147. package/dist/ui-tailwind/editor-surface/search-plugin.js +23 -0
  148. package/dist/ui-tailwind/editor-surface/search-plugin.js.map +1 -0
  149. package/dist/ui-tailwind/index.cjs +4833 -0
  150. package/dist/ui-tailwind/index.cjs.map +1 -0
  151. package/dist/ui-tailwind/index.d.cts +617 -0
  152. package/dist/ui-tailwind/index.d.ts +617 -0
  153. package/dist/ui-tailwind/index.js +575 -0
  154. package/dist/ui-tailwind/index.js.map +1 -0
  155. package/package.json +64 -54
  156. package/src/README.md +0 -85
  157. package/src/api/README.md +0 -26
  158. package/src/api/public-types.ts +0 -1418
  159. package/src/api/session-state.ts +0 -60
  160. package/src/compare/diff-engine.ts +0 -623
  161. package/src/compare/export-redlines.ts +0 -280
  162. package/src/compare/index.ts +0 -25
  163. package/src/compare/snapshot.ts +0 -97
  164. package/src/component-inventory.md +0 -99
  165. package/src/core/README.md +0 -10
  166. package/src/core/commands/README.md +0 -3
  167. package/src/core/commands/image-commands.ts +0 -373
  168. package/src/core/commands/index.ts +0 -1757
  169. package/src/core/commands/list-commands.ts +0 -565
  170. package/src/core/commands/paragraph-layout-commands.ts +0 -339
  171. package/src/core/commands/review-commands.ts +0 -108
  172. package/src/core/commands/structural-helpers.ts +0 -309
  173. package/src/core/schema/README.md +0 -3
  174. package/src/core/schema/text-schema.ts +0 -516
  175. package/src/core/search/search-text.ts +0 -357
  176. package/src/core/selection/README.md +0 -3
  177. package/src/core/selection/mapping.ts +0 -289
  178. package/src/core/selection/review-anchors.ts +0 -183
  179. package/src/core/state/README.md +0 -3
  180. package/src/core/state/editor-state.ts +0 -892
  181. package/src/formats/xlsx/io/parse-shared-strings.ts +0 -41
  182. package/src/formats/xlsx/io/parse-sheet.ts +0 -459
  183. package/src/formats/xlsx/io/parse-styles.ts +0 -59
  184. package/src/formats/xlsx/io/parse-workbook.ts +0 -75
  185. package/src/formats/xlsx/io/serialize-shared-strings.ts +0 -72
  186. package/src/formats/xlsx/io/serialize-sheet.ts +0 -333
  187. package/src/formats/xlsx/io/serialize-styles.ts +0 -98
  188. package/src/formats/xlsx/io/serialize-workbook.ts +0 -429
  189. package/src/formats/xlsx/io/xlsx-session.ts +0 -314
  190. package/src/formats/xlsx/model/cell.ts +0 -189
  191. package/src/formats/xlsx/model/sheet.ts +0 -326
  192. package/src/formats/xlsx/model/styles.ts +0 -118
  193. package/src/formats/xlsx/model/workbook.ts +0 -453
  194. package/src/formats/xlsx/runtime/cell-commands.ts +0 -567
  195. package/src/formats/xlsx/runtime/sheet-commands.ts +0 -206
  196. package/src/formats/xlsx/runtime/workbook-runtime.ts +0 -177
  197. package/src/formats/xlsx/runtime/workbook-transaction.ts +0 -822
  198. package/src/index.ts +0 -101
  199. package/src/io/README.md +0 -10
  200. package/src/io/docx-session.ts +0 -2882
  201. package/src/io/export/README.md +0 -3
  202. package/src/io/export/export-session.ts +0 -220
  203. package/src/io/export/minimal-docx.ts +0 -115
  204. package/src/io/export/reattach-preserved-parts.ts +0 -54
  205. package/src/io/export/serialize-comments.ts +0 -947
  206. package/src/io/export/serialize-footnotes.ts +0 -399
  207. package/src/io/export/serialize-headers-footers.ts +0 -372
  208. package/src/io/export/serialize-main-document.ts +0 -1376
  209. package/src/io/export/serialize-numbering.ts +0 -118
  210. package/src/io/export/serialize-revisions.ts +0 -389
  211. package/src/io/export/serialize-runtime-revisions.ts +0 -269
  212. package/src/io/export/serialize-tables.ts +0 -174
  213. package/src/io/export/split-review-boundaries.ts +0 -356
  214. package/src/io/normalize/README.md +0 -3
  215. package/src/io/normalize/normalize-text.ts +0 -639
  216. package/src/io/ooxml/README.md +0 -3
  217. package/src/io/ooxml/highlight-colors.ts +0 -39
  218. package/src/io/ooxml/numbering-sentinels.ts +0 -44
  219. package/src/io/ooxml/parse-comments.ts +0 -846
  220. package/src/io/ooxml/parse-complex-content.ts +0 -287
  221. package/src/io/ooxml/parse-fields.ts +0 -834
  222. package/src/io/ooxml/parse-footnotes.ts +0 -896
  223. package/src/io/ooxml/parse-headers-footers.ts +0 -1169
  224. package/src/io/ooxml/parse-inline-media.ts +0 -461
  225. package/src/io/ooxml/parse-main-document.ts +0 -2877
  226. package/src/io/ooxml/parse-numbering.ts +0 -432
  227. package/src/io/ooxml/parse-revisions.ts +0 -931
  228. package/src/io/ooxml/parse-settings.ts +0 -184
  229. package/src/io/ooxml/parse-shapes.ts +0 -296
  230. package/src/io/ooxml/parse-styles.ts +0 -463
  231. package/src/io/ooxml/parse-tables.ts +0 -618
  232. package/src/io/ooxml/parse-theme.ts +0 -346
  233. package/src/io/ooxml/part-manifest.ts +0 -136
  234. package/src/io/ooxml/revision-boundaries.ts +0 -351
  235. package/src/io/opc/README.md +0 -3
  236. package/src/io/opc/corrupt-package.ts +0 -166
  237. package/src/io/opc/docx-package.ts +0 -74
  238. package/src/io/opc/package-reader.ts +0 -325
  239. package/src/io/opc/package-writer.ts +0 -273
  240. package/src/io/source-package-provenance.ts +0 -241
  241. package/src/legal/cross-references.ts +0 -414
  242. package/src/legal/defined-terms.ts +0 -203
  243. package/src/legal/index.ts +0 -32
  244. package/src/legal/signature-blocks.ts +0 -259
  245. package/src/model/README.md +0 -3
  246. package/src/model/canonical-document.ts +0 -2632
  247. package/src/model/cds-1.0.0.ts +0 -212
  248. package/src/model/snapshot.ts +0 -649
  249. package/src/preservation/README.md +0 -3
  250. package/src/preservation/markup-compatibility.ts +0 -48
  251. package/src/preservation/opaque-fragment-store.ts +0 -89
  252. package/src/preservation/opaque-region.ts +0 -233
  253. package/src/preservation/package-preservation.ts +0 -113
  254. package/src/preservation/preserved-part-manifest.ts +0 -56
  255. package/src/preservation/relationship-retention.ts +0 -57
  256. package/src/preservation/store.ts +0 -185
  257. package/src/review/README.md +0 -16
  258. package/src/review/store/README.md +0 -3
  259. package/src/review/store/comment-anchors.ts +0 -70
  260. package/src/review/store/comment-remapping.ts +0 -154
  261. package/src/review/store/comment-store.ts +0 -331
  262. package/src/review/store/comment-thread.ts +0 -109
  263. package/src/review/store/revision-actions.ts +0 -394
  264. package/src/review/store/revision-store.ts +0 -312
  265. package/src/review/store/revision-types.ts +0 -171
  266. package/src/review/store/runtime-comment-store.ts +0 -43
  267. package/src/runtime/README.md +0 -3
  268. package/src/runtime/ai-action-policy.ts +0 -764
  269. package/src/runtime/collab-review-sync.ts +0 -254
  270. package/src/runtime/document-layout.ts +0 -332
  271. package/src/runtime/document-navigation.ts +0 -603
  272. package/src/runtime/document-runtime.ts +0 -3159
  273. package/src/runtime/document-search.ts +0 -145
  274. package/src/runtime/numbering-prefix.ts +0 -216
  275. package/src/runtime/page-layout-estimation.ts +0 -212
  276. package/src/runtime/read-only-diagnostics-runtime.ts +0 -241
  277. package/src/runtime/review-runtime.ts +0 -44
  278. package/src/runtime/revision-runtime.ts +0 -107
  279. package/src/runtime/session-capabilities.ts +0 -192
  280. package/src/runtime/story-context.ts +0 -164
  281. package/src/runtime/story-targeting.ts +0 -162
  282. package/src/runtime/surface-projection.ts +0 -1357
  283. package/src/runtime/table-commands.ts +0 -173
  284. package/src/runtime/table-schema.ts +0 -309
  285. package/src/runtime/view-state.ts +0 -477
  286. package/src/runtime/virtualized-rendering.ts +0 -258
  287. package/src/runtime/workflow-markup.ts +0 -353
  288. package/src/ui/README.md +0 -30
  289. package/src/ui/WordReviewEditor.tsx +0 -4097
  290. package/src/ui/browser-export.ts +0 -52
  291. package/src/ui/comments/README.md +0 -3
  292. package/src/ui/compatibility/README.md +0 -3
  293. package/src/ui/editor-command-bag.ts +0 -120
  294. package/src/ui/editor-runtime-boundary.ts +0 -1457
  295. package/src/ui/editor-shell-view.tsx +0 -142
  296. package/src/ui/editor-surface/README.md +0 -3
  297. package/src/ui/editor-surface-controller.tsx +0 -63
  298. package/src/ui/headless/comment-decoration-model.ts +0 -124
  299. package/src/ui/headless/preserve-editor-selection.ts +0 -5
  300. package/src/ui/headless/revision-decoration-model.ts +0 -128
  301. package/src/ui/headless/selection-helpers.ts +0 -54
  302. package/src/ui/headless/selection-toolbar-model.ts +0 -34
  303. package/src/ui/headless/use-editor-keyboard.ts +0 -103
  304. package/src/ui/review/README.md +0 -3
  305. package/src/ui/runtime-snapshot-selectors.ts +0 -197
  306. package/src/ui/shared/revision-filters.ts +0 -31
  307. package/src/ui/status/README.md +0 -3
  308. package/src/ui/theme/README.md +0 -3
  309. package/src/ui/toolbar/README.md +0 -3
  310. package/src/ui/workflow-surface-blocked-rails.ts +0 -94
  311. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +0 -64
  312. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +0 -129
  313. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +0 -114
  314. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +0 -34
  315. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +0 -386
  316. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -186
  317. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +0 -139
  318. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +0 -128
  319. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +0 -58
  320. package/src/ui-tailwind/chrome/use-before-unload.ts +0 -20
  321. package/src/ui-tailwind/editor-surface/perf-probe.ts +0 -179
  322. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
  323. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +0 -178
  324. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +0 -31
  325. package/src/ui-tailwind/editor-surface/pm-decorations.ts +0 -427
  326. package/src/ui-tailwind/editor-surface/pm-position-map.ts +0 -123
  327. package/src/ui-tailwind/editor-surface/pm-schema.ts +0 -876
  328. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +0 -504
  329. package/src/ui-tailwind/editor-surface/search-plugin.ts +0 -168
  330. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +0 -61
  331. package/src/ui-tailwind/editor-surface/tw-caret.tsx +0 -12
  332. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +0 -150
  333. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +0 -129
  334. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +0 -58
  335. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +0 -151
  336. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +0 -973
  337. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +0 -111
  338. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -436
  339. package/src/ui-tailwind/index.ts +0 -62
  340. package/src/ui-tailwind/page-chrome-model.ts +0 -27
  341. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +0 -406
  342. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -149
  343. package/src/ui-tailwind/review/tw-review-rail.tsx +0 -120
  344. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +0 -164
  345. package/src/ui-tailwind/status/tw-status-bar.tsx +0 -61
  346. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +0 -52
  347. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +0 -1064
  348. package/src/ui-tailwind/tw-review-workspace.tsx +0 -1417
  349. package/src/validation/README.md +0 -3
  350. package/src/validation/compatibility-engine.ts +0 -634
  351. package/src/validation/compatibility-report.ts +0 -161
  352. package/src/validation/diagnostics.ts +0 -204
  353. package/src/validation/docx-comment-proof.ts +0 -707
  354. package/src/validation/import-diagnostics.ts +0 -128
  355. package/src/validation/low-priority-word-surfaces.ts +0 -373
  356. /package/{src → dist}/ui-tailwind/theme/editor-theme.css +0 -0
@@ -1,3159 +0,0 @@
1
- import {
2
- createEditorState,
3
- createPersistedEditorSnapshot,
4
- deriveDocumentStats,
5
- createSelectionSnapshot,
6
- type CanonicalDocumentEnvelope,
7
- type CommentEntryRecord,
8
- type CommentThreadRecord,
9
- type CompatibilityFeatureEntry as InternalCompatibilityFeatureEntry,
10
- type CompatibilityReport as InternalCompatibilityReport,
11
- type EditorError as InternalEditorError,
12
- type EditorState,
13
- type EditorWarning as InternalEditorWarning,
14
- } from "../core/state/editor-state.ts";
15
- import type {
16
- AddCommentParams,
17
- CommentSidebarSnapshot,
18
- CommentSidebarThreadSnapshot,
19
- CompatibilityReport,
20
- DocumentMode,
21
- DocumentNavigationSnapshot,
22
- EditorSessionState,
23
- EditorAnchorProjection,
24
- EditorError,
25
- EditorStoryTarget,
26
- EditorViewStateSnapshot,
27
- EditorWarning,
28
- FieldEntrySnapshot,
29
- FieldSnapshot,
30
- HeaderFooterLinkPatch,
31
- ExportDocxOptions,
32
- ExportResult,
33
- HostAnnotationOverlay,
34
- HostAnnotationSnapshot,
35
- InteractionGuardSnapshot,
36
- PageLayoutSnapshot,
37
- PersistedEditorSnapshot,
38
- ProtectionSnapshot,
39
- RuntimeRenderSnapshot,
40
- SelectionSnapshot,
41
- StyleCatalogSnapshot,
42
- TocRefreshOptions,
43
- TocRefreshResult,
44
- TrackedChangeEntrySnapshot,
45
- TrackedChangesSnapshot,
46
- UpdateFieldsOptions,
47
- UpdateFieldsResult,
48
- ViewMode,
49
- WorkflowCandidateRange,
50
- WorkflowCandidateRangeOptions,
51
- WorkflowBlockedCommandReason,
52
- WorkflowMarkupSnapshot,
53
- WorkflowOverlay,
54
- WorkflowScopeSnapshot,
55
- WorkspaceMode,
56
- WordReviewEditorEvent,
57
- ZoomLevel,
58
- } from "../api/public-types";
59
- import {
60
- editorSessionStateFromPersistedSnapshot,
61
- persistedSnapshotFromEditorSessionState,
62
- } from "../api/session-state.ts";
63
- import {
64
- executeEditorCommand,
65
- selectionChanged,
66
- type CommandOrigin,
67
- type EditorCommand,
68
- type EditorTransaction,
69
- } from "../core/commands/index.ts";
70
- import {
71
- createDetachedAnchor,
72
- createEmptyMapping,
73
- createNodeAnchor,
74
- createRangeAnchor,
75
- mapRange,
76
- MAIN_STORY_TARGET,
77
- storyTargetsEqual,
78
- type EditorAnchorProjection as InternalEditorAnchorProjection,
79
- } from "../core/selection/mapping.ts";
80
- import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors.ts";
81
- import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
82
- import {
83
- describeOpaqueFragment,
84
- findOpaqueFragmentsIntersectingRange,
85
- } from "../preservation/store.ts";
86
- import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
87
- import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
88
- import {
89
- createRevisionSidebarProjection,
90
- type RevisionStore,
91
- } from "../review/store/revision-store.ts";
92
- import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
93
- import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
94
- import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
95
- import {
96
- collectWorkflowMarkupSnapshot,
97
- deriveWorkflowCandidateRangesFromMarkup,
98
- } from "./workflow-markup.ts";
99
- import {
100
- createDocumentNavigationSnapshot,
101
- findPageForOffset,
102
- } from "./document-navigation.ts";
103
- import {
104
- buildPageLayoutSnapshot,
105
- buildResolvedSections,
106
- resolveActiveSection,
107
- } from "./document-layout.ts";
108
- import { normalizeHeaderFooterTarget } from "./story-context.ts";
109
- import {
110
- getStoryBlocks,
111
- replaceStoryBlocks,
112
- storyTargetKey,
113
- } from "./story-targeting.ts";
114
- import {
115
- createViewState,
116
- setViewMode as applyViewMode,
117
- setDocumentMode as applyDocumentMode,
118
- setWorkspaceMode as applyWorkspaceMode,
119
- setZoomLevel as applyZoomLevel,
120
- setFocused as applyFocused,
121
- setCaretAffinity as applyCaretAffinity,
122
- setActivePageRegion as applyActivePageRegion,
123
- setActiveObjectFrame as applyActiveObjectFrame,
124
- createEditorViewStateSnapshot,
125
- type ViewState,
126
- } from "./view-state.ts";
127
- import type {
128
- BlockNode,
129
- FieldNode,
130
- FieldRefreshStatus,
131
- InlineNode,
132
- PageMargins,
133
- ParagraphNode,
134
- SubPartsCatalog,
135
- } from "../model/canonical-document.ts";
136
- import {
137
- buildFieldRegistry,
138
- isSupportedFieldFamily,
139
- parseTocLevelRange,
140
- resolveRefFieldText,
141
- } from "../io/ooxml/parse-fields.ts";
142
- import {
143
- incrementInvalidationCounter,
144
- recordPerfSample,
145
- } from "../ui-tailwind/editor-surface/perf-probe.ts";
146
-
147
- export type Unsubscribe = () => void;
148
-
149
- type RuntimeReadySource = "docx" | "session" | "snapshot" | "datastore" | "canonical";
150
-
151
- export type DocumentRuntimeEvent =
152
- | (Omit<Extract<WordReviewEditorEvent, { type: "ready" }>, "source"> & {
153
- source: RuntimeReadySource;
154
- })
155
- | Exclude<WordReviewEditorEvent, { type: "ready" }>;
156
-
157
- export type ActiveStoryTextCommand =
158
- | Extract<EditorCommand, { type: "text.insert" }>
159
- | Extract<EditorCommand, { type: "text.delete-backward" }>
160
- | Extract<EditorCommand, { type: "text.delete-forward" }>
161
- | Extract<EditorCommand, { type: "text.insert-tab" }>
162
- | Extract<EditorCommand, { type: "text.insert-hard-break" }>
163
- | Extract<EditorCommand, { type: "paragraph.split" }>;
164
-
165
- export interface DocumentRuntime {
166
- subscribe(listener: () => void): Unsubscribe;
167
- subscribeToEvents(listener: (event: DocumentRuntimeEvent) => void): Unsubscribe;
168
- getRenderSnapshot(): RuntimeRenderSnapshot;
169
- getCanonicalDocument(): CanonicalDocumentEnvelope;
170
- getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
171
- replaceText(text: string, target?: EditorAnchorProjection): void;
172
- applyActiveStoryTextCommand(command: ActiveStoryTextCommand): void;
173
- dispatch(command: EditorCommand): void;
174
- emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
175
- undo(): void;
176
- redo(): void;
177
- focus(): void;
178
- blur(): void;
179
- setDefaultAuthorId?(authorId?: string): void;
180
- addComment(params: AddCommentParams): string;
181
- openComment(commentId: string): void;
182
- resolveComment(commentId: string): void;
183
- reopenComment(commentId: string): void;
184
- addCommentReply(commentId: string, body: string, authorId?: string): void;
185
- editCommentBody(commentId: string, body: string): void;
186
- acceptChange(changeId: string): void;
187
- rejectChange(changeId: string): void;
188
- acceptAllChanges(): void;
189
- rejectAllChanges(): void;
190
- openStory(target: EditorStoryTarget): boolean;
191
- closeStory(): void;
192
- getActiveStory(): EditorStoryTarget;
193
- getViewState(): EditorViewStateSnapshot;
194
- setViewMode(mode: ViewMode): void;
195
- setDocumentMode(mode: DocumentMode): void;
196
- getProtectionSnapshot(): ProtectionSnapshot;
197
- setWorkspaceMode(mode: WorkspaceMode): void;
198
- setZoom(level: ZoomLevel): void;
199
- getPageLayoutSnapshot(): PageLayoutSnapshot | null;
200
- getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
201
- getFieldSnapshot(): FieldSnapshot;
202
- updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult;
203
- updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult;
204
- getSessionState(): EditorSessionState;
205
- getPersistedSnapshot(): PersistedEditorSnapshot;
206
- getCompatibilityReport(): CompatibilityReport;
207
- getWarnings(): EditorWarning[];
208
- exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
209
- setWorkflowOverlay(overlay: WorkflowOverlay): void;
210
- clearWorkflowOverlay(): void;
211
- getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
212
- getInteractionGuardSnapshot(): InteractionGuardSnapshot;
213
- getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
214
- setHostAnnotationOverlay(overlay: HostAnnotationOverlay): void;
215
- clearHostAnnotationOverlay(): void;
216
- getHostAnnotationSnapshot(): HostAnnotationSnapshot;
217
- getWorkflowCandidateRanges(options?: WorkflowCandidateRangeOptions): WorkflowCandidateRange[];
218
- replaceWorkflowMarkupText(markupId: string, text: string): void;
219
- }
220
-
221
- export interface CreateDocumentRuntimeOptions {
222
- documentId: string;
223
- initialSessionState?: EditorSessionState;
224
- initialSnapshot?: PersistedEditorSnapshot;
225
- initialCanonicalDocument?: CanonicalDocumentEnvelope;
226
- sourceLabel?: string;
227
- sourceKind?: RuntimeReadySource;
228
- readOnly?: boolean;
229
- editorBuild?: string;
230
- defaultAuthorId?: string;
231
- fatalError?: EditorError;
232
- clock?: () => string;
233
- exportDocx?: (
234
- sessionState: EditorSessionState,
235
- options?: ExportDocxOptions,
236
- ) => Promise<ExportResult>;
237
- onEvent?: (event: DocumentRuntimeEvent) => void;
238
- onWarning?: (warning: EditorWarning) => void;
239
- onError?: (error: EditorError) => void;
240
- initialViewState?: Partial<ViewState>;
241
- protectionSnapshot?: ProtectionSnapshot;
242
- }
243
-
244
- interface HistoryState {
245
- past: EditorState[];
246
- future: EditorState[];
247
- }
248
-
249
- export function createDocumentRuntime(
250
- options: CreateDocumentRuntimeOptions,
251
- ): DocumentRuntime {
252
- const clock = options.clock ?? (() => new Date().toISOString());
253
- const editorBuild = options.editorBuild ?? "dev";
254
- let defaultAuthorId = options.defaultAuthorId;
255
- const sessionId = createSessionId(options.documentId, clock());
256
- const listeners = new Set<() => void>();
257
- const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
258
- const history: HistoryState = {
259
- past: [],
260
- future: [],
261
- };
262
-
263
- let activeStory: EditorStoryTarget = MAIN_STORY_TARGET;
264
- const storySelections = new Map<string, EditorState["selection"]>();
265
- let viewState: ViewState = createViewState(options.initialViewState);
266
- let protectionSnapshot: ProtectionSnapshot =
267
- options.protectionSnapshot ??
268
- options.initialSessionState?.protectionSnapshot ??
269
- options.initialSnapshot?.protectionSnapshot ?? {
270
- hasDocumentProtection: false,
271
- enforcementActive: false,
272
- ranges: [],
273
- enforcedRangeCount: 0,
274
- preservedRangeCount: 0,
275
- };
276
- let workflowOverlay: WorkflowOverlay | null = null;
277
- let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
278
- const initialPersistedSnapshot = options.initialSessionState
279
- ? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
280
- savedAt: options.initialSessionState.updatedAt,
281
- })
282
- : options.initialSnapshot;
283
-
284
- let state = createEditorState({
285
- documentId: options.documentId,
286
- sessionId,
287
- sourceLabel: options.sourceLabel,
288
- readOnly: options.readOnly,
289
- persistedSnapshot: initialPersistedSnapshot as never,
290
- canonicalDocument: options.initialCanonicalDocument,
291
- fatalError: options.fatalError as never,
292
- });
293
- storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
294
- let cachedSurface:
295
- | {
296
- revisionToken: string;
297
- activeStoryKey: string;
298
- snapshot: RuntimeRenderSnapshot["surface"];
299
- }
300
- | undefined;
301
- let cachedCompatibility:
302
- | {
303
- revisionToken: string;
304
- warnings: EditorState["warnings"];
305
- fatalError: EditorState["fatalError"];
306
- report: RuntimeRenderSnapshot["compatibility"];
307
- }
308
- | undefined;
309
- let cachedComments:
310
- | {
311
- comments: CanonicalDocumentEnvelope["review"]["comments"];
312
- activeCommentId: EditorState["runtime"]["activeCommentId"];
313
- snapshot: CommentSidebarSnapshot;
314
- }
315
- | undefined;
316
- let cachedTrackedChanges:
317
- | {
318
- revisions: CanonicalDocumentEnvelope["review"]["revisions"];
319
- revisionToken: string;
320
- snapshot: TrackedChangesSnapshot;
321
- }
322
- | undefined;
323
- let cachedPageLayout:
324
- | {
325
- revisionToken: string;
326
- activeStoryKey: string;
327
- activeSectionIndex: number | string;
328
- snapshot: PageLayoutSnapshot | null;
329
- }
330
- | undefined;
331
- let cachedNavigation:
332
- | {
333
- revisionToken: string;
334
- activeStoryKey: string;
335
- selectionHead: number;
336
- snapshot: DocumentNavigationSnapshot;
337
- }
338
- | undefined;
339
- let cachedViewStateSnapshot:
340
- | {
341
- revisionToken: string;
342
- activeStoryKey: string;
343
- selection: EditorState["selection"];
344
- viewStateRef: ViewState;
345
- pageLayout: PageLayoutSnapshot | null | undefined;
346
- snapshot: EditorViewStateSnapshot;
347
- }
348
- | undefined;
349
- let cachedInteractionGuardSnapshot:
350
- | {
351
- revisionToken: string;
352
- activeStoryKey: string;
353
- selection: EditorState["selection"];
354
- readOnly: boolean;
355
- documentMode: DocumentMode;
356
- protectionSnapshot: ProtectionSnapshot;
357
- workflowOverlay: WorkflowOverlay | null;
358
- snapshot: InteractionGuardSnapshot;
359
- }
360
- | undefined;
361
- let cachedWorkflowScopeSnapshot:
362
- | {
363
- workflowOverlay: WorkflowOverlay;
364
- interactionGuardSnapshot: InteractionGuardSnapshot;
365
- snapshot: WorkflowScopeSnapshot;
366
- }
367
- | undefined;
368
- let cachedWorkflowMarkupSnapshot:
369
- | {
370
- revisionToken: string;
371
- activeStoryKey: string;
372
- protectionSnapshot: ProtectionSnapshot;
373
- preservation: CanonicalDocumentEnvelope["preservation"];
374
- snapshot: WorkflowMarkupSnapshot;
375
- }
376
- | undefined;
377
-
378
- function getCachedSurface(
379
- document: CanonicalDocumentEnvelope,
380
- nextActiveStory: EditorStoryTarget,
381
- ): RuntimeRenderSnapshot["surface"] {
382
- const activeStoryKey = storyTargetKey(nextActiveStory);
383
- if (
384
- cachedSurface &&
385
- cachedSurface.revisionToken === state.revisionToken &&
386
- cachedSurface.activeStoryKey === activeStoryKey
387
- ) {
388
- return cachedSurface.snapshot;
389
- }
390
-
391
- const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory);
392
- recordPerfSample("snapshot.surface");
393
- incrementInvalidationCounter("runtime.snapshot.surfaceMisses");
394
- cachedSurface = {
395
- revisionToken: state.revisionToken,
396
- activeStoryKey,
397
- snapshot,
398
- };
399
- return snapshot;
400
- }
401
-
402
- function getCachedCompatibilityReport(
403
- nextState: EditorState,
404
- ): RuntimeRenderSnapshot["compatibility"] {
405
- if (
406
- cachedCompatibility &&
407
- cachedCompatibility.revisionToken === nextState.revisionToken &&
408
- cachedCompatibility.warnings === nextState.warnings &&
409
- cachedCompatibility.fatalError === nextState.fatalError
410
- ) {
411
- return cachedCompatibility.report;
412
- }
413
-
414
- const derived = createDerivedCompatibility(nextState);
415
- recordPerfSample("snapshot.compatibility");
416
- incrementInvalidationCounter("runtime.snapshot.compatibilityMisses");
417
- const report = {
418
- blockExport: derived.blockExport,
419
- blockExportReasons: listBlockExportReasons(derived),
420
- warningCount: derived.warnings.length,
421
- errorCount: derived.errors.length,
422
- featureEntries: derived.featureEntries.map((entry) =>
423
- toPublicCompatibilityFeatureEntry(entry),
424
- ),
425
- };
426
- cachedCompatibility = {
427
- revisionToken: nextState.revisionToken,
428
- warnings: nextState.warnings,
429
- fatalError: nextState.fatalError,
430
- report,
431
- };
432
- return report;
433
- }
434
-
435
- function getCachedCommentSidebarSnapshot(nextState: EditorState): CommentSidebarSnapshot {
436
- if (
437
- cachedComments &&
438
- cachedComments.comments === nextState.document.review.comments &&
439
- cachedComments.activeCommentId === nextState.runtime.activeCommentId
440
- ) {
441
- return cachedComments.snapshot;
442
- }
443
-
444
- const snapshot = toPublicCommentSidebarSnapshot(nextState);
445
- cachedComments = {
446
- comments: nextState.document.review.comments,
447
- activeCommentId: nextState.runtime.activeCommentId,
448
- snapshot,
449
- };
450
- return snapshot;
451
- }
452
-
453
- function getCachedTrackedChangesSnapshot(
454
- nextState: EditorState,
455
- _surface: RuntimeRenderSnapshot["surface"],
456
- ): TrackedChangesSnapshot {
457
- if (
458
- cachedTrackedChanges &&
459
- cachedTrackedChanges.revisions === nextState.document.review.revisions &&
460
- cachedTrackedChanges.revisionToken === nextState.revisionToken
461
- ) {
462
- return cachedTrackedChanges.snapshot;
463
- }
464
-
465
- const snapshot = toPublicTrackedChangesSnapshot(nextState);
466
- cachedTrackedChanges = {
467
- revisions: nextState.document.review.revisions,
468
- revisionToken: nextState.revisionToken,
469
- snapshot,
470
- };
471
- return snapshot;
472
- }
473
-
474
- function getCachedDocumentNavigationSnapshot(
475
- nextState: EditorState,
476
- nextActiveStory: EditorStoryTarget,
477
- ): DocumentNavigationSnapshot {
478
- const activeStoryKey = storyTargetKey(nextActiveStory);
479
- if (
480
- cachedNavigation &&
481
- cachedNavigation.revisionToken === nextState.revisionToken &&
482
- cachedNavigation.activeStoryKey === activeStoryKey
483
- ) {
484
- if (cachedNavigation.selectionHead === nextState.selection.head) {
485
- return cachedNavigation.snapshot;
486
- }
487
-
488
- const snapshot = createDocumentNavigationSnapshot(
489
- nextState.document,
490
- nextState.selection.head,
491
- nextActiveStory,
492
- );
493
- if (
494
- snapshot.activePageIndex === cachedNavigation.snapshot.activePageIndex &&
495
- snapshot.activeSectionIndex === cachedNavigation.snapshot.activeSectionIndex
496
- ) {
497
- cachedNavigation = {
498
- revisionToken: nextState.revisionToken,
499
- activeStoryKey,
500
- selectionHead: nextState.selection.head,
501
- snapshot: cachedNavigation.snapshot,
502
- };
503
- return cachedNavigation.snapshot;
504
- }
505
- cachedNavigation = {
506
- revisionToken: nextState.revisionToken,
507
- activeStoryKey,
508
- selectionHead: nextState.selection.head,
509
- snapshot,
510
- };
511
- return snapshot;
512
- }
513
-
514
- const snapshot = createDocumentNavigationSnapshot(
515
- nextState.document,
516
- nextState.selection.head,
517
- nextActiveStory,
518
- );
519
- recordPerfSample("snapshot.navigation");
520
- incrementInvalidationCounter("runtime.snapshot.navigationMisses");
521
- cachedNavigation = {
522
- revisionToken: nextState.revisionToken,
523
- activeStoryKey,
524
- selectionHead: nextState.selection.head,
525
- snapshot,
526
- };
527
- return snapshot;
528
- }
529
-
530
- function resolvePageLayoutActiveSectionIndex(
531
- nextState: EditorState,
532
- nextActiveStory: EditorStoryTarget,
533
- ): number | string {
534
- if (nextActiveStory.kind === "main") {
535
- return getCachedDocumentNavigationSnapshot(nextState, nextActiveStory).activeSectionIndex;
536
- }
537
-
538
- if ("sectionIndex" in nextActiveStory && typeof nextActiveStory.sectionIndex === "number") {
539
- return nextActiveStory.sectionIndex;
540
- }
541
-
542
- return storyTargetKey(nextActiveStory);
543
- }
544
-
545
- function getCachedPageLayoutSnapshot(
546
- nextState: EditorState,
547
- nextActiveStory: EditorStoryTarget,
548
- ): PageLayoutSnapshot | null {
549
- const activeStoryKey = storyTargetKey(nextActiveStory);
550
- const activeSectionIndex = resolvePageLayoutActiveSectionIndex(
551
- nextState,
552
- nextActiveStory,
553
- );
554
- if (
555
- cachedPageLayout &&
556
- cachedPageLayout.revisionToken === nextState.revisionToken &&
557
- cachedPageLayout.activeStoryKey === activeStoryKey &&
558
- cachedPageLayout.activeSectionIndex === activeSectionIndex
559
- ) {
560
- return cachedPageLayout.snapshot;
561
- }
562
-
563
- const snapshot = derivePageLayoutSnapshot(nextState, nextActiveStory, storySelections);
564
- cachedPageLayout = {
565
- revisionToken: nextState.revisionToken,
566
- activeStoryKey,
567
- activeSectionIndex,
568
- snapshot,
569
- };
570
- return snapshot;
571
- }
572
-
573
- function evaluateWorkflowBlockedReasons(
574
- selection: EditorState["selection"],
575
- commandType?: string,
576
- ): WorkflowBlockedCommandReason[] {
577
- const reasons: WorkflowBlockedCommandReason[] = [];
578
- const selectionBounds = {
579
- from: Math.min(selection.anchor, selection.head),
580
- to: Math.max(selection.anchor, selection.head),
581
- };
582
- const selectionRange = expandSelectionRange(selectionBounds);
583
- const opaqueReason = deriveOpaqueWorkflowBlockedReason(selectionRange);
584
-
585
- if (opaqueReason) {
586
- reasons.push(opaqueReason);
587
- }
588
-
589
- if (state.readOnly) {
590
- reasons.push({
591
- code: "document_read_only",
592
- message: "Document is in read-only mode.",
593
- });
594
- }
595
-
596
- if (viewState.documentMode === "viewing") {
597
- reasons.push({
598
- code: "document_viewing_mode",
599
- message: "Document is in viewing mode.",
600
- });
601
- }
602
-
603
- if (
604
- isBlockedByProtection(protectionSnapshot, selection)
605
- ) {
606
- reasons.push({
607
- code: "protected_range",
608
- message: "Selection falls within a protected range.",
609
- });
610
- }
611
-
612
- const effectiveDocumentMode = getEffectiveDocumentMode(selection);
613
-
614
- if (effectiveDocumentMode === "suggesting" && commandType) {
615
- if (
616
- activeStory.kind !== "main" &&
617
- SUGGESTING_SECONDARY_STORY_UNSUPPORTED_COMMANDS.has(commandType)
618
- ) {
619
- reasons.push({
620
- code: "suggesting_unsupported",
621
- message: "Suggesting mode is not yet export-safe in this story.",
622
- });
623
- }
624
- if (SUGGESTING_UNSUPPORTED_COMMANDS.has(commandType)) {
625
- reasons.push({
626
- code: "suggesting_unsupported",
627
- message: `"${commandType}" is not supported in suggesting mode.`,
628
- });
629
- }
630
- }
631
-
632
- if (workflowOverlay) {
633
- const matchingScope = getMatchingWorkflowScope(selection);
634
-
635
- if (!matchingScope && workflowOverlay.scopes.length > 0) {
636
- reasons.push({
637
- code: "outside_workflow_scope",
638
- message: "Selection is outside any active workflow scope.",
639
- });
640
- } else if (matchingScope) {
641
- if (matchingScope.mode === "comment") {
642
- const isCommentCommand =
643
- commandType?.startsWith("comment.") ?? false;
644
- if (!isCommentCommand) {
645
- reasons.push({
646
- code: "workflow_comment_only",
647
- message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
648
- scopeId: matchingScope.scopeId,
649
- workItemId: matchingScope.workItemId,
650
- });
651
- }
652
- } else if (matchingScope.mode === "view") {
653
- reasons.push({
654
- code: "workflow_view_only",
655
- message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
656
- scopeId: matchingScope.scopeId,
657
- workItemId: matchingScope.workItemId,
658
- });
659
- }
660
- }
661
- }
662
-
663
- return reasons;
664
- }
665
-
666
- function getMatchingWorkflowScope(
667
- selection: EditorState["selection"],
668
- ): WorkflowOverlay["scopes"][number] | null {
669
- if (!workflowOverlay) {
670
- return null;
671
- }
672
-
673
- const selectionBounds = {
674
- from: Math.min(selection.anchor, selection.head),
675
- to: Math.max(selection.anchor, selection.head),
676
- };
677
- const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
678
- return activeScopes.find((scope) => {
679
- if (scope.anchor.kind === "detached") return false;
680
- const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
681
- const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
682
- return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
683
- }) ?? null;
684
- }
685
-
686
- function getEffectiveDocumentMode(
687
- selection: EditorState["selection"],
688
- ): DocumentMode {
689
- if (viewState.documentMode === "viewing") {
690
- return "viewing";
691
- }
692
- const matchingScope = getMatchingWorkflowScope(selection);
693
- if (matchingScope?.mode === "suggest") {
694
- return "suggesting";
695
- }
696
- return viewState.documentMode;
697
- }
698
-
699
- function expandSelectionRange(
700
- range: { from: number; to: number },
701
- ): { from: number; to: number } {
702
- return {
703
- from: range.from,
704
- to: range.to > range.from ? range.to : range.from + 1,
705
- };
706
- }
707
-
708
- function deriveOpaqueWorkflowBlockedReason(
709
- range: { from: number; to: number },
710
- ): WorkflowBlockedCommandReason | null {
711
- const targetPartPath = getStoryTargetOpaquePartPath(activeStory);
712
- if (!targetPartPath) {
713
- return null;
714
- }
715
- const fragments = findOpaqueFragmentsIntersectingRange(
716
- state.document.preservation,
717
- range,
718
- ).filter((fragment) => fragment.packagePartName === targetPartPath);
719
-
720
- if (fragments.length === 0) {
721
- return null;
722
- }
723
-
724
- const blockedImportFeatureKeys = new Set([
725
- "alt-chunk",
726
- "alternate-content",
727
- "custom-xml",
728
- ]);
729
- const blockedImportFragment =
730
- fragments.find((fragment) =>
731
- blockedImportFeatureKeys.has(describeOpaqueFragment(fragment).featureKey),
732
- ) ?? null;
733
- const fragment = blockedImportFragment ?? fragments[0]!;
734
- const descriptor = describeOpaqueFragment(fragment);
735
- const isBlockedImport = blockedImportFragment !== null;
736
-
737
- return {
738
- code: isBlockedImport ? "workflow_blocked_import" : "workflow_preserve_only",
739
- message: isBlockedImport
740
- ? `${descriptor.label} remains a blocked import and cannot be edited.`
741
- : `${descriptor.label} remains preserve-only and cannot be edited.`,
742
- anchor: toPublicAnchorProjection(
743
- createRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to, {
744
- start: -1,
745
- end: 1,
746
- }),
747
- ),
748
- storyTarget: activeStory,
749
- };
750
- }
751
-
752
- function getStoryTargetOpaquePartPath(storyTarget: EditorStoryTarget): string | null {
753
- if (storyTarget.kind === "main") {
754
- return "/word/document.xml";
755
- }
756
- if (storyTarget.kind === "header") {
757
- return state.document.subParts?.headers.find(
758
- (header) =>
759
- header.relationshipId === storyTarget.relationshipId
760
- && header.variant === storyTarget.variant
761
- && header.sectionIndex === storyTarget.sectionIndex,
762
- )?.partPath ?? null;
763
- }
764
- if (storyTarget.kind === "footer") {
765
- return state.document.subParts?.footers.find(
766
- (footer) =>
767
- footer.relationshipId === storyTarget.relationshipId
768
- && footer.variant === storyTarget.variant
769
- && footer.sectionIndex === storyTarget.sectionIndex,
770
- )?.partPath ?? null;
771
- }
772
- if (storyTarget.kind === "footnote") {
773
- return "/word/footnotes.xml";
774
- }
775
- if (storyTarget.kind === "endnote") {
776
- return "/word/endnotes.xml";
777
- }
778
- return null;
779
- }
780
-
781
- function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
782
- if (!workflowOverlay) return null;
783
- const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
784
- const activeItem = workflowOverlay.activeWorkItemId
785
- ? workflowOverlay.workItems?.find(
786
- (item) => item.workItemId === workflowOverlay!.activeWorkItemId,
787
- )
788
- : undefined;
789
- return {
790
- overlayPresent: true,
791
- activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
792
- activeWorkItem: activeItem,
793
- scopes: workflowOverlay.scopes,
794
- candidates: workflowOverlay.candidates ?? [],
795
- blockedReasons,
796
- };
797
- }
798
-
799
- function deriveHostAnnotationSnapshot(): HostAnnotationSnapshot {
800
- return {
801
- totalCount: hostAnnotationOverlay?.annotations.length ?? 0,
802
- annotations: structuredClone(hostAnnotationOverlay?.annotations ?? []),
803
- };
804
- }
805
-
806
- function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
807
- const activeWorkItemId = overlay.activeWorkItemId ?? null;
808
- const activeWorkItemScopeIds =
809
- activeWorkItemId === null
810
- ? null
811
- : new Set(
812
- overlay.workItems?.find((item) => item.workItemId === activeWorkItemId)?.scopeIds ?? [],
813
- );
814
-
815
- return overlay.scopes.filter((scope) => {
816
- const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
817
- if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
818
- return false;
819
- }
820
-
821
- if (activeWorkItemId === null) {
822
- return true;
823
- }
824
-
825
- return (
826
- scope.workItemId === activeWorkItemId ||
827
- activeWorkItemScopeIds?.has(scope.scopeId) === true
828
- );
829
- });
830
- }
831
-
832
- function getCachedInteractionGuardSnapshot(): InteractionGuardSnapshot {
833
- const activeStoryKey = storyTargetKey(activeStory);
834
- if (
835
- cachedInteractionGuardSnapshot &&
836
- cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
837
- cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
838
- cachedInteractionGuardSnapshot.selection === state.selection &&
839
- cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
840
- cachedInteractionGuardSnapshot.documentMode === viewState.documentMode &&
841
- cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
842
- cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay
843
- ) {
844
- return cachedInteractionGuardSnapshot.snapshot;
845
- }
846
-
847
- const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
848
- const matchingScope = getMatchingWorkflowScope(state.selection);
849
- const primaryBlockedReason = blockedReasons[0];
850
- const snapshot: InteractionGuardSnapshot = {
851
- effectiveMode: primaryBlockedReason
852
- ? (
853
- primaryBlockedReason.code === "workflow_comment_only"
854
- ? "comment"
855
- : primaryBlockedReason.code === "workflow_view_only"
856
- ? "view"
857
- : "blocked"
858
- )
859
- : getEffectiveDocumentMode(state.selection) === "suggesting"
860
- ? "suggest"
861
- : matchingScope?.mode ?? "edit",
862
- ...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
863
- ...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
864
- ...(primaryBlockedReason ? { disabledReason: primaryBlockedReason.message } : {}),
865
- blockedReasons,
866
- };
867
- cachedInteractionGuardSnapshot = {
868
- revisionToken: state.revisionToken,
869
- activeStoryKey,
870
- selection: state.selection,
871
- readOnly: state.readOnly,
872
- documentMode: viewState.documentMode,
873
- protectionSnapshot,
874
- workflowOverlay,
875
- snapshot,
876
- };
877
- return snapshot;
878
- }
879
-
880
- function getCachedWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
881
- if (!workflowOverlay) {
882
- return null;
883
- }
884
-
885
- const interactionGuardSnapshot = getCachedInteractionGuardSnapshot();
886
- if (
887
- cachedWorkflowScopeSnapshot &&
888
- cachedWorkflowScopeSnapshot.workflowOverlay === workflowOverlay &&
889
- cachedWorkflowScopeSnapshot.interactionGuardSnapshot === interactionGuardSnapshot
890
- ) {
891
- return cachedWorkflowScopeSnapshot.snapshot;
892
- }
893
-
894
- const snapshot = deriveWorkflowScopeSnapshot()!;
895
- cachedWorkflowScopeSnapshot = {
896
- workflowOverlay,
897
- interactionGuardSnapshot,
898
- snapshot,
899
- };
900
- return snapshot;
901
- }
902
-
903
- function getCachedWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot {
904
- const activeStoryKey = storyTargetKey(activeStory);
905
- if (
906
- cachedWorkflowMarkupSnapshot &&
907
- cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
908
- cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
909
- cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
910
- cachedWorkflowMarkupSnapshot.preservation === state.document.preservation
911
- ) {
912
- return cachedWorkflowMarkupSnapshot.snapshot;
913
- }
914
-
915
- const snapshot = collectWorkflowMarkupSnapshot({
916
- renderSnapshot: cachedRenderSnapshot,
917
- fieldSnapshot: buildFieldSnapshot(state.document),
918
- protectionSnapshot,
919
- preservation: state.document.preservation,
920
- });
921
- cachedWorkflowMarkupSnapshot = {
922
- revisionToken: state.revisionToken,
923
- activeStoryKey,
924
- protectionSnapshot,
925
- preservation: state.document.preservation,
926
- snapshot,
927
- };
928
- return snapshot;
929
- }
930
-
931
- function refreshRenderSnapshot(): RuntimeRenderSnapshot {
932
- const surface = getCachedSurface(state.document, activeStory);
933
- return {
934
- documentId: state.documentId,
935
- sessionId: state.sessionId,
936
- sourceLabel: state.sourceLabel,
937
- revisionToken: state.revisionToken,
938
- isReady: state.phase === "ready",
939
- isDirty: state.isDirty,
940
- readOnly: state.readOnly,
941
- documentMode: viewState.documentMode,
942
- selection: toPublicSelectionSnapshot(state.selection, activeStory),
943
- activeStory,
944
- pageLayout: getCachedPageLayoutSnapshot(state, activeStory) ?? undefined,
945
- documentStats: toPublicDocumentStats(state),
946
- comments: getCachedCommentSidebarSnapshot(state),
947
- trackedChanges: getCachedTrackedChangesSnapshot(state, surface),
948
- compatibility: getCachedCompatibilityReport(state),
949
- warnings: state.warnings.map((warning) => toPublicWarning(warning)),
950
- fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
951
- commandState: {
952
- canUndo: history.past.length > 0,
953
- canRedo: history.future.length > 0,
954
- readOnly: state.readOnly,
955
- },
956
- surface,
957
- protectionSnapshot,
958
- };
959
- }
960
-
961
- function getCachedViewStateSnapshot(): EditorViewStateSnapshot {
962
- const activeStoryKey = storyTargetKey(activeStory);
963
- const pageLayout = cachedRenderSnapshot.pageLayout;
964
- if (
965
- cachedViewStateSnapshot &&
966
- cachedViewStateSnapshot.revisionToken === state.revisionToken &&
967
- cachedViewStateSnapshot.activeStoryKey === activeStoryKey &&
968
- cachedViewStateSnapshot.selection === state.selection &&
969
- cachedViewStateSnapshot.viewStateRef === viewState &&
970
- cachedViewStateSnapshot.pageLayout === pageLayout
971
- ) {
972
- return cachedViewStateSnapshot.snapshot;
973
- }
974
-
975
- const surface = cachedRenderSnapshot.surface;
976
- const mainSurface =
977
- activeStory.kind === "main"
978
- ? surface
979
- : getCachedSurface(state.document, MAIN_STORY_TARGET);
980
- const snapshot = createEditorViewStateSnapshot(
981
- viewState,
982
- activeStory,
983
- toPublicSelectionSnapshot(state.selection, activeStory),
984
- surface,
985
- mainSurface,
986
- pageLayout,
987
- state.document.numbering,
988
- );
989
- cachedViewStateSnapshot = {
990
- revisionToken: state.revisionToken,
991
- activeStoryKey,
992
- selection: state.selection,
993
- viewStateRef: viewState,
994
- pageLayout,
995
- snapshot,
996
- };
997
- return snapshot;
998
- }
999
-
1000
- let cachedRenderSnapshot = refreshRenderSnapshot();
1001
-
1002
- emit({
1003
- type: "ready",
1004
- documentId: state.documentId,
1005
- sessionId: state.sessionId,
1006
- source:
1007
- options.sourceKind ??
1008
- (options.initialSessionState
1009
- ? "session"
1010
- : options.initialSnapshot
1011
- ? "snapshot"
1012
- : "canonical"),
1013
- stats: toPublicDocumentStats(state),
1014
- compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
1015
- comments: cachedRenderSnapshot.comments,
1016
- trackedChanges: cachedRenderSnapshot.trackedChanges,
1017
- });
1018
- if (options.fatalError) {
1019
- emit({
1020
- type: "error",
1021
- documentId: state.documentId,
1022
- error: options.fatalError,
1023
- });
1024
- }
1025
-
1026
- return {
1027
- subscribe(listener) {
1028
- listeners.add(listener);
1029
- return () => {
1030
- listeners.delete(listener);
1031
- };
1032
- },
1033
- subscribeToEvents(listener) {
1034
- eventListeners.add(listener);
1035
- return () => {
1036
- eventListeners.delete(listener);
1037
- };
1038
- },
1039
- getRenderSnapshot() {
1040
- return cachedRenderSnapshot;
1041
- },
1042
- getCanonicalDocument() {
1043
- return state.document;
1044
- },
1045
- getSourcePackage() {
1046
- return state.sourcePackage;
1047
- },
1048
- emitBlockedCommand(command, reasons) {
1049
- emit({
1050
- type: "command_blocked",
1051
- documentId: state.documentId,
1052
- command,
1053
- reasons,
1054
- });
1055
- },
1056
- dispatch(command) {
1057
- const commandSelection = getCommandSelection(command, state.selection);
1058
- if (isMutationCommand(command)) {
1059
- const blockedReasons = evaluateWorkflowBlockedReasons(
1060
- commandSelection,
1061
- command.type,
1062
- );
1063
- if (blockedReasons.length > 0) {
1064
- emit({
1065
- type: "command_blocked",
1066
- documentId: state.documentId,
1067
- command: command.type,
1068
- reasons: blockedReasons,
1069
- });
1070
- return;
1071
- }
1072
- }
1073
-
1074
- if (command.type === "history.undo") {
1075
- if (viewState.documentMode === "viewing") return;
1076
- applyHistory("undo");
1077
- return;
1078
- }
1079
-
1080
- if (command.type === "history.redo") {
1081
- if (viewState.documentMode === "viewing") return;
1082
- applyHistory("redo");
1083
- return;
1084
- }
1085
- try {
1086
- const transaction = executeEditorCommand(state, command, {
1087
- timestamp: command.origin?.timestamp ?? clock(),
1088
- documentMode: getEffectiveDocumentMode(commandSelection),
1089
- defaultAuthorId: defaultAuthorId ?? undefined,
1090
- });
1091
- commit(transaction);
1092
- } catch (error) {
1093
- emitError(toRuntimeError(error));
1094
- }
1095
- },
1096
- undo() {
1097
- this.dispatch({
1098
- type: "history.undo",
1099
- origin: createOrigin("runtime", clock()),
1100
- });
1101
- },
1102
- redo() {
1103
- this.dispatch({
1104
- type: "history.redo",
1105
- origin: createOrigin("runtime", clock()),
1106
- });
1107
- },
1108
- focus() {
1109
- viewState = applyFocused(viewState, true);
1110
- this.dispatch({
1111
- type: "runtime.focus",
1112
- focused: true,
1113
- origin: createOrigin("api", clock()),
1114
- });
1115
- },
1116
- blur() {
1117
- viewState = applyFocused(viewState, false);
1118
- this.dispatch({
1119
- type: "runtime.focus",
1120
- focused: false,
1121
- origin: createOrigin("api", clock()),
1122
- });
1123
- },
1124
- setDefaultAuthorId(authorId) {
1125
- defaultAuthorId = authorId;
1126
- },
1127
- replaceText(text, target) {
1128
- try {
1129
- const timestamp = clock();
1130
- applyTextCommandInActiveStory(
1131
- {
1132
- type: "text.insert",
1133
- text,
1134
- origin: createOrigin("api", timestamp),
1135
- },
1136
- {
1137
- selection: target ? createSelectionFromPublicAnchor(target) : state.selection,
1138
- blockedCommandName: "replaceText",
1139
- },
1140
- );
1141
- } catch (error) {
1142
- emitError(toRuntimeError(error));
1143
- }
1144
- },
1145
- applyActiveStoryTextCommand(command) {
1146
- try {
1147
- applyTextCommandInActiveStory(command);
1148
- } catch (error) {
1149
- emitError(toRuntimeError(error));
1150
- }
1151
- },
1152
- addComment(params) {
1153
- if (viewState.documentMode === "viewing") {
1154
- throw new Error("Cannot add comments in viewing mode.");
1155
- }
1156
- const commentId = createEntityId("comment", state.document.review.comments, clock());
1157
- const anchor = params.anchor
1158
- ? toInternalAnchorProjection(params.anchor)
1159
- : state.selection.activeRange;
1160
- const selection = params.anchor
1161
- ? createSelectionFromPublicAnchor(params.anchor)
1162
- : state.selection;
1163
- if (!canCreateDocxCommentAnchor(state.document.content, anchor)) {
1164
- const message =
1165
- "DOCX comments must use a non-empty range that stays within a single paragraph.";
1166
- emitError({
1167
- errorId: createSessionId("comment-anchor", clock()),
1168
- code: "validation_failed",
1169
- isFatal: false,
1170
- message,
1171
- source: "runtime",
1172
- });
1173
- throw new Error(message);
1174
- }
1175
- const authorId = params.authorId ?? defaultAuthorId ?? "unknown";
1176
- const createdAt = clock();
1177
- const entries: CommentEntryRecord[] = [
1178
- {
1179
- entryId: `${commentId}-entry-1`,
1180
- authorId,
1181
- body: params.body ?? "",
1182
- createdAt,
1183
- },
1184
- ];
1185
- const comment: CommentThreadRecord = {
1186
- commentId,
1187
- anchor,
1188
- createdAt,
1189
- createdBy: authorId,
1190
- authorId,
1191
- body: params.body ?? "",
1192
- entries,
1193
- status: anchor.kind === "detached" ? "detached" : "open",
1194
- warningIds: [],
1195
- isResolved: false,
1196
- metadata: {
1197
- source: "runtime",
1198
- },
1199
- };
1200
-
1201
- this.dispatch({
1202
- type: "comment.add",
1203
- comment,
1204
- selection,
1205
- origin: createOrigin("api", clock()),
1206
- });
1207
-
1208
- return commentId;
1209
- },
1210
- openComment(commentId) {
1211
- this.dispatch({
1212
- type: "comment.open",
1213
- commentId,
1214
- origin: createOrigin("api", clock()),
1215
- });
1216
- },
1217
- resolveComment(commentId) {
1218
- this.dispatch({
1219
- type: "comment.resolve",
1220
- commentId,
1221
- resolvedBy: defaultAuthorId ?? "unknown",
1222
- origin: createOrigin("api", clock()),
1223
- });
1224
- },
1225
- reopenComment(commentId) {
1226
- this.dispatch({
1227
- type: "comment.reopen",
1228
- commentId,
1229
- origin: createOrigin("api", clock()),
1230
- });
1231
- },
1232
- addCommentReply(commentId, body, authorId) {
1233
- this.dispatch({
1234
- type: "comment.add-reply",
1235
- commentId,
1236
- body,
1237
- authorId: authorId ?? defaultAuthorId,
1238
- origin: createOrigin("api", clock()),
1239
- });
1240
- },
1241
- editCommentBody(commentId, body) {
1242
- this.dispatch({
1243
- type: "comment.edit-body",
1244
- commentId,
1245
- body,
1246
- origin: createOrigin("api", clock()),
1247
- });
1248
- },
1249
- acceptChange(changeId) {
1250
- this.dispatch({
1251
- type: "change.accept",
1252
- changeId,
1253
- origin: createOrigin("api", clock()),
1254
- });
1255
- },
1256
- rejectChange(changeId) {
1257
- this.dispatch({
1258
- type: "change.reject",
1259
- changeId,
1260
- origin: createOrigin("api", clock()),
1261
- });
1262
- },
1263
- acceptAllChanges() {
1264
- this.dispatch({
1265
- type: "change.accept-all",
1266
- origin: createOrigin("api", clock()),
1267
- });
1268
- },
1269
- rejectAllChanges() {
1270
- this.dispatch({
1271
- type: "change.reject-all",
1272
- origin: createOrigin("api", clock()),
1273
- });
1274
- },
1275
- openStory(target) {
1276
- const normalizedTarget =
1277
- target.kind === "header" || target.kind === "footer"
1278
- ? normalizeHeaderFooterTarget(
1279
- state.document,
1280
- target,
1281
- cachedRenderSnapshot.pageLayout?.sectionIndex,
1282
- ) ?? target
1283
- : target;
1284
- if (storyTargetsEqual(activeStory, normalizedTarget)) {
1285
- return true;
1286
- }
1287
- if (!isValidStoryTarget(state, normalizedTarget)) {
1288
- return false;
1289
- }
1290
- switchActiveStory(normalizedTarget);
1291
- return true;
1292
- },
1293
- closeStory() {
1294
- if (activeStory.kind === "main") {
1295
- return;
1296
- }
1297
- switchActiveStory(MAIN_STORY_TARGET);
1298
- },
1299
- getActiveStory() {
1300
- return activeStory;
1301
- },
1302
- getViewState() {
1303
- return getCachedViewStateSnapshot();
1304
- },
1305
- setViewMode(mode) {
1306
- viewState = applyViewMode(viewState, mode);
1307
- cachedRenderSnapshot = refreshRenderSnapshot();
1308
- for (const listener of listeners) {
1309
- listener();
1310
- }
1311
- },
1312
- setDocumentMode(mode) {
1313
- viewState = applyDocumentMode(viewState, mode);
1314
- cachedRenderSnapshot = refreshRenderSnapshot();
1315
- for (const listener of listeners) {
1316
- listener();
1317
- }
1318
- },
1319
- getProtectionSnapshot() {
1320
- return cachedRenderSnapshot.protectionSnapshot;
1321
- },
1322
- setWorkspaceMode(mode) {
1323
- viewState = applyWorkspaceMode(viewState, mode);
1324
- cachedRenderSnapshot = refreshRenderSnapshot();
1325
- for (const listener of listeners) {
1326
- listener();
1327
- }
1328
- },
1329
- setZoom(level) {
1330
- viewState = applyZoomLevel(viewState, level);
1331
- cachedRenderSnapshot = refreshRenderSnapshot();
1332
- for (const listener of listeners) {
1333
- listener();
1334
- }
1335
- },
1336
- getPageLayoutSnapshot() {
1337
- return getCachedPageLayoutSnapshot(state, activeStory);
1338
- },
1339
- getDocumentNavigationSnapshot() {
1340
- return getCachedDocumentNavigationSnapshot(state, activeStory);
1341
- },
1342
- getFieldSnapshot() {
1343
- return buildFieldSnapshot(state.document);
1344
- },
1345
- updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult {
1346
- const refreshed = refreshDocumentFields(
1347
- state.document,
1348
- state.selection.head,
1349
- activeStory,
1350
- options,
1351
- );
1352
- if (refreshed.changed) {
1353
- this.dispatch({
1354
- type: "document.replace",
1355
- document: refreshed.document,
1356
- mapping: createEmptyMapping(),
1357
- protectionSelection: refreshed.protectionSelection,
1358
- origin: createOrigin("api", clock()),
1359
- });
1360
- }
1361
- const snapshot = buildFieldSnapshot(refreshed.document);
1362
- return {
1363
- totalCount: snapshot.totalCount,
1364
- updatedCount: refreshed.updatedCount,
1365
- preserveOnlyCount: snapshot.preserveOnlyCount,
1366
- };
1367
- },
1368
- updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult {
1369
- const refreshed = refreshDocumentTableOfContents(
1370
- state.document,
1371
- state.selection.head,
1372
- activeStory,
1373
- options,
1374
- );
1375
- if (refreshed.changed) {
1376
- this.dispatch({
1377
- type: "document.replace",
1378
- document: refreshed.document,
1379
- mapping: createEmptyMapping(),
1380
- protectionSelection: refreshed.protectionSelection,
1381
- origin: createOrigin("api", clock()),
1382
- });
1383
- }
1384
- return refreshed.result;
1385
- },
1386
- getSessionState() {
1387
- const compatibility = createDerivedCompatibility(state);
1388
- return editorSessionStateFromPersistedSnapshot(
1389
- createPersistedEditorSnapshot(state, {
1390
- editorBuild,
1391
- savedAt: clock(),
1392
- compatibility,
1393
- protectionSnapshot,
1394
- }) as unknown as PersistedEditorSnapshot,
1395
- );
1396
- },
1397
- getPersistedSnapshot() {
1398
- return persistedSnapshotFromEditorSessionState(this.getSessionState(), {
1399
- savedAt: clock(),
1400
- });
1401
- },
1402
- getCompatibilityReport() {
1403
- return toPublicCompatibilityReport(createDerivedCompatibility(state));
1404
- },
1405
- getWarnings() {
1406
- return state.warnings.map((warning) => toPublicWarning(warning));
1407
- },
1408
- async exportDocx(exportOptions) {
1409
- if (!options.exportDocx) {
1410
- const error: InternalEditorError = {
1411
- errorId: createEntityId("error", {}, clock()),
1412
- code: "export_failed",
1413
- isFatal: false,
1414
- message: "DOCX export requires an injected exporter until the IO substrate lands.",
1415
- source: "export",
1416
- details: {
1417
- requestedOptions: exportOptions ?? {},
1418
- },
1419
- };
1420
- emitError(error);
1421
- throw new Error(error.message);
1422
- }
1423
-
1424
- const result = await options.exportDocx(this.getSessionState(), exportOptions);
1425
-
1426
- emit({
1427
- type: "export_completed",
1428
- documentId: state.documentId,
1429
- result,
1430
- });
1431
-
1432
- return result;
1433
- },
1434
- setWorkflowOverlay(overlay) {
1435
- workflowOverlay = structuredClone(overlay);
1436
- cachedRenderSnapshot = refreshRenderSnapshot();
1437
- const snapshot = deriveWorkflowScopeSnapshot()!;
1438
- emit({
1439
- type: "workflow_overlay_changed",
1440
- documentId: state.documentId,
1441
- snapshot,
1442
- });
1443
- if (workflowOverlay.activeWorkItemId !== undefined) {
1444
- emit({
1445
- type: "workflow_active_work_item_changed",
1446
- documentId: state.documentId,
1447
- activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
1448
- });
1449
- }
1450
- for (const listener of listeners) {
1451
- listener();
1452
- }
1453
- },
1454
- clearWorkflowOverlay() {
1455
- workflowOverlay = null;
1456
- cachedRenderSnapshot = refreshRenderSnapshot();
1457
- emit({
1458
- type: "workflow_active_work_item_changed",
1459
- documentId: state.documentId,
1460
- activeWorkItemId: null,
1461
- });
1462
- emit({
1463
- type: "workflow_overlay_changed",
1464
- documentId: state.documentId,
1465
- snapshot: {
1466
- overlayPresent: false,
1467
- activeWorkItemId: null,
1468
- scopes: [],
1469
- candidates: [],
1470
- blockedReasons: [],
1471
- },
1472
- });
1473
- for (const listener of listeners) {
1474
- listener();
1475
- }
1476
- },
1477
- getWorkflowScopeSnapshot() {
1478
- return getCachedWorkflowScopeSnapshot();
1479
- },
1480
- getInteractionGuardSnapshot() {
1481
- return getCachedInteractionGuardSnapshot();
1482
- },
1483
- getWorkflowMarkupSnapshot() {
1484
- return getCachedWorkflowMarkupSnapshot();
1485
- },
1486
- setHostAnnotationOverlay(overlay) {
1487
- hostAnnotationOverlay = structuredClone(overlay);
1488
- emit({
1489
- type: "host_annotation_overlay_changed",
1490
- documentId: state.documentId,
1491
- snapshot: deriveHostAnnotationSnapshot(),
1492
- });
1493
- for (const listener of listeners) {
1494
- listener();
1495
- }
1496
- },
1497
- clearHostAnnotationOverlay() {
1498
- hostAnnotationOverlay = null;
1499
- emit({
1500
- type: "host_annotation_overlay_changed",
1501
- documentId: state.documentId,
1502
- snapshot: deriveHostAnnotationSnapshot(),
1503
- });
1504
- for (const listener of listeners) {
1505
- listener();
1506
- }
1507
- },
1508
- getHostAnnotationSnapshot() {
1509
- return deriveHostAnnotationSnapshot();
1510
- },
1511
- getWorkflowCandidateRanges(options) {
1512
- return deriveWorkflowCandidateRangesFromMarkup(this.getWorkflowMarkupSnapshot(), options);
1513
- },
1514
- replaceWorkflowMarkupText(markupId, text) {
1515
- const target = this
1516
- .getWorkflowMarkupSnapshot()
1517
- .items.find((item) => item.markupId === markupId);
1518
- if (!target || target.anchor.kind === "detached") {
1519
- return;
1520
- }
1521
- const targetStory = target.storyTarget ?? MAIN_STORY_TARGET;
1522
- if (!storyTargetsEqual(activeStory, targetStory)) {
1523
- if (targetStory.kind === "main") {
1524
- this.closeStory();
1525
- } else if (!this.openStory(targetStory)) {
1526
- return;
1527
- }
1528
- }
1529
- this.replaceText(text, target.anchor);
1530
- },
1531
- };
1532
-
1533
- function applyHistory(direction: "undo" | "redo"): void {
1534
- const source = direction === "undo" ? history.past : history.future;
1535
- const target = source.pop();
1536
-
1537
- if (!target) {
1538
- return;
1539
- }
1540
-
1541
- const counterpart = direction === "undo" ? history.future : history.past;
1542
- counterpart.push(state);
1543
-
1544
- const previous = state;
1545
- // Undo/redo changes the document — must mint a new revisionToken so
1546
- // autosave/export checkpoint dedup treats it as fresh content.
1547
- state = finalizeState(target, true, clock(), previous.revision);
1548
- storySelections.set(storyTargetKey(activeStory), state.selection);
1549
- cachedRenderSnapshot = refreshRenderSnapshot();
1550
- notify(previous, state, {
1551
- nextState: state,
1552
- mapping: { steps: [] },
1553
- effects: {
1554
- warningsAdded: [],
1555
- warningsCleared: [],
1556
- },
1557
- historyBoundary: "skip",
1558
- markDirty: true,
1559
- });
1560
- }
1561
-
1562
- function commit(transaction: EditorTransaction): void {
1563
- const previous = state;
1564
-
1565
- if (transaction.historyBoundary === "push") {
1566
- history.past.push(state);
1567
- history.future = [];
1568
- }
1569
-
1570
- protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
1571
- state = finalizeState(transaction.nextState, transaction.markDirty, clock());
1572
- storySelections.set(storyTargetKey(activeStory), state.selection);
1573
- cachedRenderSnapshot = refreshRenderSnapshot();
1574
- notify(previous, state, transaction);
1575
- }
1576
-
1577
- function notify(
1578
- previous: EditorState,
1579
- next: EditorState,
1580
- transaction: EditorTransaction,
1581
- ): void {
1582
- if (previous.isDirty !== next.isDirty) {
1583
- emit({
1584
- type: "dirty_changed",
1585
- documentId: next.documentId,
1586
- isDirty: next.isDirty,
1587
- });
1588
- }
1589
-
1590
- if (selectionChanged(previous.selection, next.selection)) {
1591
- emit({
1592
- type: "selection_changed",
1593
- documentId: next.documentId,
1594
- selection: toPublicSelectionSnapshot(next.selection, activeStory),
1595
- });
1596
- }
1597
-
1598
- if (transaction.effects.commentAdded) {
1599
- emit({
1600
- type: "comment_added",
1601
- documentId: next.documentId,
1602
- commentId: transaction.effects.commentAdded.commentId,
1603
- anchor: toPublicAnchorProjection(transaction.effects.commentAdded.anchor),
1604
- });
1605
- }
1606
-
1607
- if (transaction.effects.commentResolved) {
1608
- emit({
1609
- type: "comment_resolved",
1610
- documentId: next.documentId,
1611
- commentId: transaction.effects.commentResolved.commentId,
1612
- });
1613
- }
1614
-
1615
- if (transaction.effects.changeAccepted) {
1616
- emit({
1617
- type: "change_accepted",
1618
- documentId: next.documentId,
1619
- changeId: transaction.effects.changeAccepted.changeId,
1620
- });
1621
- }
1622
-
1623
- if (transaction.effects.changeRejected) {
1624
- emit({
1625
- type: "change_rejected",
1626
- documentId: next.documentId,
1627
- changeId: transaction.effects.changeRejected.changeId,
1628
- });
1629
- }
1630
-
1631
- if (transaction.effects.revisionAuthored) {
1632
- emit({
1633
- type: "change_authored",
1634
- documentId: next.documentId,
1635
- changeId: transaction.effects.revisionAuthored.changeId,
1636
- kind: transaction.effects.revisionAuthored.kind,
1637
- });
1638
- }
1639
-
1640
- if (transaction.effects.commandBlocked) {
1641
- emit({
1642
- type: "command_blocked",
1643
- documentId: next.documentId,
1644
- command: transaction.effects.commandBlocked.code,
1645
- reasons: [{
1646
- code: transaction.effects.commandBlocked.code as WorkflowBlockedCommandReason["code"],
1647
- message: transaction.effects.commandBlocked.message,
1648
- }],
1649
- });
1650
- }
1651
-
1652
- for (const warning of transaction.effects.warningsAdded) {
1653
- const publicWarning = toPublicWarning(warning);
1654
- emit({
1655
- type: "warning_added",
1656
- documentId: next.documentId,
1657
- warning: publicWarning,
1658
- });
1659
- options.onWarning?.(publicWarning);
1660
- }
1661
-
1662
- for (const cleared of transaction.effects.warningsCleared) {
1663
- emit({
1664
- type: "warning_cleared",
1665
- documentId: next.documentId,
1666
- warningId: cleared.warningId,
1667
- code: cleared.code,
1668
- });
1669
- }
1670
-
1671
- for (const listener of listeners) {
1672
- listener();
1673
- }
1674
- }
1675
-
1676
- function applyTextCommandInActiveStory(
1677
- command: ActiveStoryTextCommand,
1678
- options: {
1679
- selection?: EditorState["selection"];
1680
- blockedCommandName?: string;
1681
- } = {},
1682
- ): void {
1683
- const selection = options.selection ?? state.selection;
1684
- const blockedReasons = evaluateWorkflowBlockedReasons(selection, command.type);
1685
- if (blockedReasons.length > 0) {
1686
- emit({
1687
- type: "command_blocked",
1688
- documentId: state.documentId,
1689
- command: options.blockedCommandName ?? command.type,
1690
- reasons: blockedReasons,
1691
- });
1692
- return;
1693
- }
1694
-
1695
- const timestamp = command.origin?.timestamp ?? clock();
1696
- const context = {
1697
- timestamp,
1698
- documentMode: getEffectiveDocumentMode(selection),
1699
- defaultAuthorId: defaultAuthorId ?? undefined,
1700
- } as const;
1701
- const baseState = selection === state.selection
1702
- ? state
1703
- : {
1704
- ...state,
1705
- selection,
1706
- };
1707
-
1708
- if (activeStory.kind === "main") {
1709
- commit(executeEditorCommand(baseState, command, context));
1710
- return;
1711
- }
1712
-
1713
- const localState = createEditorState({
1714
- documentId: state.documentId,
1715
- sessionId,
1716
- sourceLabel: state.sourceLabel,
1717
- readOnly: state.readOnly,
1718
- canonicalDocument: {
1719
- ...state.document,
1720
- content: {
1721
- type: "doc",
1722
- children: [...getStoryBlocks(state.document, activeStory)],
1723
- },
1724
- review: createSecondaryStoryLocalReviewState(state.document.review, activeStory),
1725
- },
1726
- compatibility: state.compatibility,
1727
- warnings: state.warnings,
1728
- fatalError: state.fatalError,
1729
- });
1730
- localState.selection = selection;
1731
- const localTransaction = executeEditorCommand(localState, command, context);
1732
-
1733
- if (!localTransaction.markDirty) {
1734
- notify(state, state, {
1735
- nextState: state,
1736
- mapping: createEmptyMapping(),
1737
- effects: localTransaction.effects,
1738
- historyBoundary: "skip",
1739
- markDirty: false,
1740
- });
1741
- return;
1742
- }
1743
-
1744
- const nextDocument = replaceStoryBlocks(
1745
- state.document,
1746
- activeStory,
1747
- localTransaction.nextState.document.content.children,
1748
- );
1749
- const nextDocumentWithReview = {
1750
- ...nextDocument,
1751
- review: mergeSecondaryStoryReviewState(
1752
- state.document.review,
1753
- localTransaction.nextState.document.review,
1754
- localTransaction.effects,
1755
- activeStory,
1756
- ),
1757
- };
1758
- const fullTransaction = executeEditorCommand(
1759
- baseState,
1760
- {
1761
- type: "document.replace",
1762
- document: nextDocumentWithReview,
1763
- selection: localTransaction.nextState.selection,
1764
- mapping: createEmptyMapping(),
1765
- protectionSelection: selection,
1766
- origin: command.origin,
1767
- },
1768
- context,
1769
- );
1770
-
1771
- commit({
1772
- ...fullTransaction,
1773
- effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
1774
- });
1775
- }
1776
-
1777
- function mergeTransactionEffects(
1778
- base: EditorTransaction["effects"],
1779
- local: EditorTransaction["effects"],
1780
- ): EditorTransaction["effects"] {
1781
- return {
1782
- warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
1783
- warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
1784
- commentAdded: base.commentAdded ?? local.commentAdded,
1785
- commentResolved: base.commentResolved ?? local.commentResolved,
1786
- commentReopened: base.commentReopened ?? local.commentReopened,
1787
- commentReplyAdded: base.commentReplyAdded ?? local.commentReplyAdded,
1788
- commentBodyEdited: base.commentBodyEdited ?? local.commentBodyEdited,
1789
- changeAccepted: base.changeAccepted ?? local.changeAccepted,
1790
- changeRejected: base.changeRejected ?? local.changeRejected,
1791
- revisionAuthored: base.revisionAuthored ?? local.revisionAuthored,
1792
- commandBlocked: base.commandBlocked ?? local.commandBlocked,
1793
- };
1794
- }
1795
-
1796
- function mergeSecondaryStoryReviewState(
1797
- currentReview: EditorState["document"]["review"],
1798
- localReview: EditorState["document"]["review"],
1799
- effects: EditorTransaction["effects"],
1800
- storyTarget: EditorStoryTarget,
1801
- ): EditorState["document"]["review"] {
1802
- const nextReview: EditorState["document"]["review"] = {
1803
- comments: { ...currentReview.comments },
1804
- revisions: { ...currentReview.revisions },
1805
- };
1806
-
1807
- const currentStoryRevisionIds = Object.values(currentReview.revisions)
1808
- .filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
1809
- .map((revision) => revision.changeId);
1810
- for (const revisionId of currentStoryRevisionIds) {
1811
- delete nextReview.revisions[revisionId];
1812
- }
1813
- for (const revision of Object.values(localReview.revisions)) {
1814
- nextReview.revisions[revision.changeId] = {
1815
- ...revision,
1816
- metadata: {
1817
- ...revision.metadata,
1818
- storyTarget: createRevisionStoryTargetRecord(storyTarget),
1819
- },
1820
- };
1821
- }
1822
-
1823
- if (effects.commentAdded) {
1824
- const commentId = effects.commentAdded.commentId;
1825
- const comment = localReview.comments[commentId];
1826
- if (comment) {
1827
- nextReview.comments[commentId] = comment;
1828
- }
1829
- }
1830
-
1831
- return nextReview;
1832
- }
1833
-
1834
- function emit(event: DocumentRuntimeEvent): void {
1835
- options.onEvent?.(event);
1836
- for (const listener of eventListeners) {
1837
- listener(event);
1838
- }
1839
- }
1840
-
1841
- function emitError(error: InternalEditorError): void {
1842
- const nextState: EditorState = {
1843
- ...state,
1844
- phase: error.isFatal ? "error" : state.phase,
1845
- fatalError: error.isFatal ? error : state.fatalError,
1846
- };
1847
- state = nextState;
1848
- storySelections.set(storyTargetKey(activeStory), state.selection);
1849
- cachedRenderSnapshot = refreshRenderSnapshot();
1850
- const publicError = toPublicError(error);
1851
- options.onError?.(publicError);
1852
- emit({
1853
- type: "error",
1854
- documentId: state.documentId,
1855
- error: publicError,
1856
- });
1857
- for (const listener of listeners) {
1858
- listener();
1859
- }
1860
- }
1861
-
1862
- function switchActiveStory(target: EditorStoryTarget): void {
1863
- const previousStory = activeStory;
1864
- const previousSelection = state.selection;
1865
- storySelections.set(storyTargetKey(previousStory), previousSelection);
1866
-
1867
- const restoredSelection =
1868
- storySelections.get(storyTargetKey(target)) ?? createSelectionSnapshot(0, 0);
1869
- activeStory = target;
1870
- state = {
1871
- ...state,
1872
- selection: restoredSelection,
1873
- };
1874
- storySelections.set(storyTargetKey(target), restoredSelection);
1875
- cachedRenderSnapshot = refreshRenderSnapshot();
1876
-
1877
- if (selectionChanged(previousSelection, restoredSelection)) {
1878
- emit({
1879
- type: "selection_changed",
1880
- documentId: state.documentId,
1881
- selection: toPublicSelectionSnapshot(restoredSelection, activeStory),
1882
- });
1883
- }
1884
-
1885
- emit({
1886
- type: "story_changed",
1887
- documentId: state.documentId,
1888
- activeStory,
1889
- });
1890
- for (const listener of listeners) {
1891
- listener();
1892
- }
1893
- }
1894
- }
1895
-
1896
- function createSessionId(documentId: string, timestamp: string): string {
1897
- return `session-${documentId}-${timestamp.replace(/[^0-9]/gu, "")}`;
1898
- }
1899
-
1900
- function createOrigin(
1901
- source: CommandOrigin["source"],
1902
- timestamp: string,
1903
- ): CommandOrigin {
1904
- return {
1905
- source,
1906
- timestamp,
1907
- };
1908
- }
1909
-
1910
- function createEntityId(
1911
- prefix: string,
1912
- existing: Record<string, unknown>,
1913
- timestamp: string,
1914
- ): string {
1915
- let counter = Object.keys(existing).length;
1916
- let nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
1917
-
1918
- while (existing[nextId]) {
1919
- counter += 1;
1920
- nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
1921
- }
1922
-
1923
- return nextId;
1924
- }
1925
-
1926
- function finalizeState(
1927
- state: EditorState,
1928
- markDirty: boolean,
1929
- timestamp: string,
1930
- baseRevision?: number,
1931
- ): EditorState {
1932
- // Only increment revision on actual document mutations (markDirty=true).
1933
- // Selection-only changes must not churn the revisionToken, which would
1934
- // cause autosave/checkpoint dedup to treat cursor movement as new content.
1935
- const revision = markDirty ? (baseRevision ?? state.revision) + 1 : state.revision;
1936
-
1937
- return {
1938
- ...state,
1939
- document: {
1940
- ...state.document,
1941
- updatedAt: markDirty ? timestamp : state.document.updatedAt,
1942
- },
1943
- selection: state.selection,
1944
- revision,
1945
- revisionToken: `${state.sessionId}:${revision}`,
1946
- isDirty: state.isDirty || markDirty,
1947
- };
1948
- }
1949
-
1950
- function toRuntimeError(error: unknown): InternalEditorError {
1951
- if (typeof error === "object" && error && "message" in error) {
1952
- return {
1953
- errorId: createSessionId("runtime-error", new Date().toISOString()),
1954
- code: "internal_invariant",
1955
- isFatal: false,
1956
- message: String((error as { message?: unknown }).message ?? "Runtime error"),
1957
- source: "runtime",
1958
- };
1959
- }
1960
-
1961
- return {
1962
- errorId: createSessionId("runtime-error", new Date().toISOString()),
1963
- code: "internal_invariant",
1964
- isFatal: false,
1965
- message: "Runtime error",
1966
- source: "runtime",
1967
- };
1968
- }
1969
-
1970
- function toPublicDocumentStats(state: Pick<EditorState, "document">) {
1971
- const stats = deriveDocumentStats(state);
1972
- return {
1973
- storyLength: stats.characterCount,
1974
- commentCount: stats.commentCount,
1975
- revisionCount: stats.revisionCount,
1976
- opaqueFragmentCount: countOpaqueFragments(state.document.preservation.opaqueFragments),
1977
- };
1978
- }
1979
-
1980
- function toPublicSelectionSnapshot(
1981
- selection: EditorState["selection"],
1982
- storyTarget?: EditorStoryTarget,
1983
- ): SelectionSnapshot {
1984
- return {
1985
- anchor: selection.anchor,
1986
- head: selection.head,
1987
- isCollapsed: selection.isCollapsed,
1988
- activeRange: toPublicAnchorProjection(selection.activeRange),
1989
- ...(storyTarget && storyTarget.kind !== "main" ? { storyTarget } : {}),
1990
- };
1991
- }
1992
-
1993
- function toPublicAnchorProjection(
1994
- anchor: InternalEditorAnchorProjection,
1995
- ): EditorAnchorProjection {
1996
- switch (anchor.kind) {
1997
- case "range":
1998
- return {
1999
- kind: "range",
2000
- from: anchor.range.from,
2001
- to: anchor.range.to,
2002
- assoc: anchor.assoc,
2003
- };
2004
- case "node":
2005
- return {
2006
- kind: "node",
2007
- at: anchor.at,
2008
- assoc: anchor.assoc,
2009
- };
2010
- case "detached":
2011
- return {
2012
- kind: "detached",
2013
- lastKnownRange: anchor.lastKnownRange,
2014
- reason: anchor.reason,
2015
- };
2016
- }
2017
- }
2018
-
2019
- function toInternalAnchorProjection(
2020
- anchor: EditorAnchorProjection,
2021
- ): InternalEditorAnchorProjection {
2022
- switch (anchor.kind) {
2023
- case "range":
2024
- return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
2025
- case "node":
2026
- return createNodeAnchor(anchor.at, anchor.assoc);
2027
- case "detached":
2028
- return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
2029
- }
2030
- }
2031
-
2032
- function createSelectionFromPublicAnchor(
2033
- anchor: EditorAnchorProjection,
2034
- ): import("../core/state/editor-state.ts").SelectionSnapshot {
2035
- switch (anchor.kind) {
2036
- case "range":
2037
- return createSelectionSnapshot(anchor.from, anchor.to);
2038
- case "node":
2039
- return createSelectionSnapshot(anchor.at, anchor.at);
2040
- case "detached":
2041
- return createSelectionSnapshot(
2042
- anchor.lastKnownRange.from,
2043
- anchor.lastKnownRange.to,
2044
- );
2045
- }
2046
- }
2047
-
2048
- function toPublicCompatibilityReport(
2049
- report: InternalCompatibilityReport,
2050
- ): CompatibilityReport {
2051
- return {
2052
- reportVersion: report.reportVersion,
2053
- generatedAt: report.generatedAt,
2054
- blockExport: report.blockExport,
2055
- featureEntries: report.featureEntries.map((entry) =>
2056
- toPublicCompatibilityFeatureEntry(entry),
2057
- ),
2058
- warnings: report.warnings.map((warning) => toPublicWarning(warning)),
2059
- errors: report.errors.map((error) => toPublicError(error)),
2060
- };
2061
- }
2062
-
2063
- function toPublicCompatibilityFeatureEntry(
2064
- entry: InternalCompatibilityFeatureEntry,
2065
- ) {
2066
- return {
2067
- ...entry,
2068
- affectedAnchor: entry.affectedAnchor
2069
- ? toPublicAnchorProjection(entry.affectedAnchor)
2070
- : undefined,
2071
- };
2072
- }
2073
-
2074
- function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
2075
- return {
2076
- ...warning,
2077
- affectedAnchor: warning.affectedAnchor
2078
- ? toPublicAnchorProjection(warning.affectedAnchor)
2079
- : undefined,
2080
- };
2081
- }
2082
-
2083
- function toPublicError(error: InternalEditorError): EditorError {
2084
- return { ...error };
2085
- }
2086
-
2087
- function countOpaqueFragments(opaqueFragments: Record<string, unknown>): number {
2088
- return Object.keys(opaqueFragments).length;
2089
- }
2090
-
2091
- function createDerivedCompatibility(state: EditorState): InternalCompatibilityReport {
2092
- const derived = buildCompatibilityReport({
2093
- document: state.document,
2094
- warnings: state.warnings,
2095
- fatalError: state.fatalError,
2096
- generatedAt: state.document.updatedAt,
2097
- });
2098
-
2099
- return mergeCompatibilityReports([state.compatibility as never, derived as never], {
2100
- generatedAt: state.document.updatedAt,
2101
- blockExport: state.compatibility.blockExport || derived.blockExport,
2102
- }) as InternalCompatibilityReport;
2103
- }
2104
-
2105
- function toPublicCommentSidebarSnapshot(
2106
- state: EditorState,
2107
- ): CommentSidebarSnapshot {
2108
- const projection = createCommentSidebarProjection(
2109
- createCommentStoreFromRuntimeComments(state.document.review.comments),
2110
- state.runtime.activeCommentId,
2111
- );
2112
-
2113
- return {
2114
- activeCommentId: state.runtime.activeCommentId,
2115
- openCommentIds: projection.openCommentIds,
2116
- resolvedCommentIds: projection.resolvedCommentIds,
2117
- detachedCommentIds: projection.detachedCommentIds,
2118
- totalCount: projection.totalCount,
2119
- threads: projection.threads.map((thread): CommentSidebarThreadSnapshot => {
2120
- const sourceThread = state.document.review.comments[thread.commentId];
2121
- const projectedEntries =
2122
- sourceThread?.entries?.map((entry) => ({
2123
- entryId: entry.entryId,
2124
- authorId: entry.authorId,
2125
- body: entry.body,
2126
- createdAt: entry.createdAt,
2127
- })) ??
2128
- (sourceThread?.body
2129
- ? [
2130
- {
2131
- entryId: `${thread.commentId}-entry-1`,
2132
- authorId:
2133
- sourceThread.authorId ?? sourceThread.createdBy ?? "unknown",
2134
- body: sourceThread.body,
2135
- createdAt: sourceThread.createdAt,
2136
- },
2137
- ]
2138
- : []);
2139
-
2140
- return {
2141
- commentId: thread.commentId,
2142
- status: thread.status,
2143
- anchor: toPublicAnchorProjection(
2144
- sourceThread?.anchor ??
2145
- createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
2146
- ),
2147
- excerpt: thread.excerpt,
2148
- entryCount: thread.entryCount,
2149
- createdAt: thread.createdAt,
2150
- createdBy: thread.createdBy,
2151
- warningCount: thread.warningCount,
2152
- anchorLabel: thread.anchorLabel,
2153
- isActive: thread.isActive,
2154
- resolvedAt: thread.resolvedAt,
2155
- resolvedBy: thread.resolvedBy,
2156
- entries: projectedEntries,
2157
- };
2158
- }),
2159
- };
2160
- }
2161
-
2162
- function toPublicTrackedChangesSnapshot(
2163
- state: EditorState,
2164
- ): TrackedChangesSnapshot {
2165
- const projection = createRevisionSidebarProjection(
2166
- createRevisionStoreFromDocument(state),
2167
- );
2168
- const storyPlainTextCache = new Map<string, string>();
2169
-
2170
- return {
2171
- pendingChangeIds: projection.activeRevisionIds,
2172
- acceptedChangeIds: projection.acceptedRevisionIds,
2173
- rejectedChangeIds: projection.rejectedRevisionIds,
2174
- detachedChangeIds: projection.detachedRevisionIds,
2175
- actionableChangeIds: projection.actionableRevisionIds,
2176
- preserveOnlyChangeIds: projection.preserveOnlyRevisionIds,
2177
- totalCount: projection.totalCount,
2178
- revisions: projection.revisions.map((revision): TrackedChangeEntrySnapshot => {
2179
- const sourceRevision = state.document.review.revisions[revision.revisionId];
2180
- const storyTarget = getRevisionStoryTarget(sourceRevision);
2181
- const preview = describeRevisionPreview(
2182
- revision,
2183
- sourceRevision?.anchor ??
2184
- createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
2185
- getStoryPlainText(state.document, storyTarget, storyPlainTextCache),
2186
- );
2187
-
2188
- return {
2189
- revisionId: revision.revisionId,
2190
- kind: revision.kind,
2191
- label: revision.label,
2192
- status: revision.status,
2193
- actionability: revision.actionability,
2194
- storyTarget,
2195
- anchor: toPublicAnchorProjection(
2196
- sourceRevision?.anchor ??
2197
- createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
2198
- ),
2199
- anchorLabel: revision.anchorLabel,
2200
- createdAt: revision.createdAt,
2201
- authorId: revision.authorId,
2202
- warningCount: revision.warningCount,
2203
- canAccept: revision.canAccept,
2204
- canReject: revision.canReject,
2205
- importedRevisionForm: sourceRevision?.metadata?.importedRevisionForm,
2206
- preserveOnlyReason: revision.preserveOnlyReason,
2207
- excerpt: preview.excerpt,
2208
- detail: preview.detail,
2209
- };
2210
- }),
2211
- };
2212
- }
2213
-
2214
- function createRevisionStoreFromDocument(
2215
- state: Pick<EditorState, "document">,
2216
- ): RevisionStore {
2217
- return {
2218
- version: "revision-store/1",
2219
- revisions: Object.fromEntries(
2220
- Object.values(state.document.review.revisions).map((revision) => [
2221
- revision.changeId,
2222
- {
2223
- revisionId: revision.changeId,
2224
- kind: revision.kind,
2225
- anchor: revision.anchor,
2226
- authorId: revision.authorId ?? "unknown",
2227
- createdAt: revision.createdAt,
2228
- status:
2229
- revision.status === "open"
2230
- ? "active"
2231
- : revision.status,
2232
- warningIds: [...(revision.warningIds ?? [])],
2233
- metadata: {
2234
- source: revision.metadata?.source ?? "runtime",
2235
- storyTarget: revision.metadata?.storyTarget,
2236
- preserveOnlyReason: revision.metadata?.preserveOnlyReason,
2237
- importedRevisionForm: revision.metadata?.importedRevisionForm,
2238
- originalRevisionType: revision.metadata?.originalRevisionType,
2239
- ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
2240
- },
2241
- },
2242
- ]),
2243
- ),
2244
- };
2245
- }
2246
-
2247
- function getRevisionStoryTarget(
2248
- revision: EditorState["document"]["review"]["revisions"][string] | undefined,
2249
- ): EditorStoryTarget {
2250
- const storyTarget = revision?.metadata?.storyTarget;
2251
- return storyTarget ? { ...storyTarget } : MAIN_STORY_TARGET;
2252
- }
2253
-
2254
- function createSecondaryStoryLocalReviewState(
2255
- review: EditorState["document"]["review"],
2256
- storyTarget: EditorStoryTarget,
2257
- ): EditorState["document"]["review"] {
2258
- return {
2259
- comments: {},
2260
- revisions: Object.fromEntries(
2261
- Object.values(review.revisions)
2262
- .filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
2263
- .map((revision) => [
2264
- revision.changeId,
2265
- {
2266
- ...revision,
2267
- metadata: {
2268
- ...revision.metadata,
2269
- storyTarget: createRevisionStoryTargetRecord(storyTarget),
2270
- },
2271
- },
2272
- ]),
2273
- ),
2274
- };
2275
- }
2276
-
2277
- function getStoryPlainText(
2278
- document: CanonicalDocumentEnvelope,
2279
- storyTarget: EditorStoryTarget,
2280
- cache: Map<string, string>,
2281
- ): string {
2282
- const key = storyTargetKey(storyTarget);
2283
- const cached = cache.get(key);
2284
- if (cached !== undefined) {
2285
- return cached;
2286
- }
2287
- const plainText = createEditorSurfaceSnapshot(
2288
- document,
2289
- createSelectionSnapshot(0, 0),
2290
- storyTarget,
2291
- ).plainText;
2292
- cache.set(key, plainText);
2293
- return plainText;
2294
- }
2295
-
2296
- function createRevisionStoryTargetRecord(
2297
- storyTarget: EditorStoryTarget,
2298
- ): NonNullable<NonNullable<EditorState["document"]["review"]["revisions"][string]["metadata"]>["storyTarget"]> {
2299
- return { ...storyTarget };
2300
- }
2301
-
2302
- function listBlockExportReasons(
2303
- report: InternalCompatibilityReport,
2304
- ): string[] {
2305
- return [
2306
- ...report.featureEntries
2307
- .filter((entry) => entry.featureClass === "unsupported-fatal")
2308
- .map((entry) => entry.message),
2309
- ...report.errors
2310
- .filter((error) => error.isFatal)
2311
- .map((error) => error.message),
2312
- ];
2313
- }
2314
-
2315
- function describeRevisionPreview(
2316
- revision: ReturnType<typeof createRevisionSidebarProjection>["revisions"][number],
2317
- anchor: InternalEditorAnchorProjection,
2318
- plainText: string,
2319
- ): { excerpt: string; detail: string } {
2320
- const { from, to } = toAnchorBounds(anchor);
2321
- const excerpt = summarizeRevisionExcerpt(plainText, from, to, revision.label);
2322
-
2323
- if (revision.actionability === "preserve-only") {
2324
- return {
2325
- excerpt,
2326
- detail:
2327
- revision.preserveOnlyReason ??
2328
- "Visible for review, but this change remains preserve-only in the current runtime.",
2329
- };
2330
- }
2331
-
2332
- if (revision.status === "accepted") {
2333
- return {
2334
- excerpt,
2335
- detail: "Accepted in the live review runtime and retained here for audit visibility.",
2336
- };
2337
- }
2338
-
2339
- if (revision.status === "rejected") {
2340
- return {
2341
- excerpt,
2342
- detail: "Rejected in the live review runtime and retained here for audit visibility.",
2343
- };
2344
- }
2345
-
2346
- return {
2347
- excerpt,
2348
- detail:
2349
- revision.kind === "deletion"
2350
- ? "Deleted content stays reviewable here until it is accepted or rejected."
2351
- : "Runtime-backed change. Review it here or reopen the anchor in the canvas.",
2352
- };
2353
- }
2354
-
2355
- function toAnchorBounds(anchor: InternalEditorAnchorProjection): { from: number; to: number } {
2356
- switch (anchor.kind) {
2357
- case "range":
2358
- return {
2359
- from: Math.min(anchor.range.from, anchor.range.to),
2360
- to: Math.max(anchor.range.from, anchor.range.to),
2361
- };
2362
- case "node":
2363
- return {
2364
- from: anchor.at,
2365
- to: anchor.at + 1,
2366
- };
2367
- case "detached":
2368
- return {
2369
- from: Math.min(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
2370
- to: Math.max(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
2371
- };
2372
- }
2373
- }
2374
-
2375
- function summarizeRevisionExcerpt(
2376
- plainText: string,
2377
- from: number,
2378
- to: number,
2379
- fallback: string,
2380
- ): string {
2381
- const normalizedFrom = Math.max(0, Math.min(from, plainText.length));
2382
- const normalizedTo = Math.max(normalizedFrom, Math.min(to, plainText.length));
2383
- const collapsed = plainText
2384
- .slice(normalizedFrom, normalizedTo)
2385
- .replace(/\s+/g, " ")
2386
- .trim();
2387
-
2388
- if (!collapsed) {
2389
- return fallback;
2390
- }
2391
-
2392
- return collapsed.length > 96 ? `${collapsed.slice(0, 93)}...` : collapsed;
2393
- }
2394
-
2395
- function isValidStoryTarget(
2396
- state: EditorState,
2397
- target: EditorStoryTarget,
2398
- ): boolean {
2399
- if (target.kind === "main") return true;
2400
- const subParts = state.document.subParts;
2401
- if (!subParts) return false;
2402
-
2403
- switch (target.kind) {
2404
- case "header":
2405
- return Boolean(normalizeHeaderFooterTarget(state.document, target));
2406
- case "footer":
2407
- return Boolean(normalizeHeaderFooterTarget(state.document, target));
2408
- case "footnote":
2409
- return Boolean(subParts.footnoteCollection?.footnotes?.[target.noteId]);
2410
- case "endnote":
2411
- return Boolean(subParts.footnoteCollection?.endnotes?.[target.noteId]);
2412
- }
2413
- }
2414
-
2415
- function derivePageLayoutSnapshot(
2416
- state: EditorState,
2417
- activeStory: EditorStoryTarget,
2418
- storySelections?: ReadonlyMap<string, EditorState["selection"]>,
2419
- ): PageLayoutSnapshot | null {
2420
- const subParts = state.document.subParts;
2421
- const sections = buildResolvedSections(state.document);
2422
- if (!subParts && sections.length === 0) {
2423
- return null;
2424
- }
2425
-
2426
- const activeSection = resolveActiveSection(
2427
- state,
2428
- activeStory,
2429
- sections,
2430
- storySelections,
2431
- );
2432
- return buildPageLayoutSnapshot(
2433
- activeSection?.index ?? 0,
2434
- activeSection?.properties ?? subParts?.finalSectionProperties,
2435
- subParts,
2436
- );
2437
- }
2438
-
2439
- function isRecord(value: unknown): value is Record<string, unknown> {
2440
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2441
- }
2442
-
2443
- /** Commands that are safe in viewing mode (no document mutation). */
2444
- const NON_MUTATION_COMMANDS = new Set([
2445
- "selection.set",
2446
- "runtime.set-read-only",
2447
- "runtime.focus",
2448
- "warning.add",
2449
- "warning.clear",
2450
- "comment.open",
2451
- ]);
2452
-
2453
- /** Mutation commands that are not yet supported in suggesting mode. */
2454
- const SUGGESTING_UNSUPPORTED_COMMANDS = new Set([
2455
- "paragraph.split",
2456
- ]);
2457
-
2458
- const SUGGESTING_SECONDARY_STORY_UNSUPPORTED_COMMANDS = new Set([
2459
- "text.insert",
2460
- "text.delete-backward",
2461
- "text.delete-forward",
2462
- "text.insert-tab",
2463
- "text.insert-hard-break",
2464
- ]);
2465
-
2466
- function isMutationCommand(command: EditorCommand): boolean {
2467
- return !NON_MUTATION_COMMANDS.has(command.type);
2468
- }
2469
-
2470
- // ── Field snapshot helpers ──────────────────────────────────────────────────
2471
-
2472
- function buildFieldSnapshot(document: CanonicalDocumentEnvelope): FieldSnapshot {
2473
- const entries: FieldEntrySnapshot[] = [];
2474
- let index = 0;
2475
- for (const block of document.content.children) {
2476
- index = collectFieldsFromBlock(block, entries, index);
2477
- }
2478
- index = collectFieldsFromSubParts(document.subParts, entries, index);
2479
- const supportedCount = entries.filter((e) => e.supported).length;
2480
- return {
2481
- totalCount: entries.length,
2482
- supportedCount,
2483
- preserveOnlyCount: entries.length - supportedCount,
2484
- fields: entries,
2485
- };
2486
- }
2487
-
2488
- function collectFieldsFromBlock(
2489
- block: BlockNode,
2490
- entries: FieldEntrySnapshot[],
2491
- index: number,
2492
- ): number {
2493
- if (block.type === "paragraph") {
2494
- for (const child of block.children) {
2495
- index = collectFieldsFromInline(child, entries, index);
2496
- }
2497
- } else if (block.type === "table") {
2498
- for (const row of block.rows) {
2499
- for (const cell of row.cells) {
2500
- for (const child of cell.children) {
2501
- index = collectFieldsFromBlock(child, entries, index);
2502
- }
2503
- }
2504
- }
2505
- } else if (block.type === "sdt" || block.type === "custom_xml") {
2506
- for (const child of block.children) {
2507
- index = collectFieldsFromBlock(child, entries, index);
2508
- }
2509
- }
2510
- return index;
2511
- }
2512
-
2513
- function collectFieldsFromInline(
2514
- node: InlineNode,
2515
- entries: FieldEntrySnapshot[],
2516
- index: number,
2517
- ): number {
2518
- if (node.type === "field") {
2519
- const fieldFamily = node.fieldFamily ?? "UNKNOWN";
2520
- const supported = isSupportedFieldFamily(fieldFamily);
2521
- const displayText = extractFieldDisplayText(node);
2522
- entries.push({
2523
- index,
2524
- fieldFamily,
2525
- supported,
2526
- instruction: node.instruction,
2527
- fieldTarget: node.fieldTarget,
2528
- refreshStatus: node.refreshStatus ?? (supported ? "stale" : "preserve-only"),
2529
- displayText,
2530
- });
2531
- index++;
2532
- // Also walk children — fields can contain nested fields
2533
- for (const child of node.children) {
2534
- index = collectFieldsFromInline(child, entries, index);
2535
- }
2536
- } else if (node.type === "hyperlink") {
2537
- for (const child of node.children) {
2538
- index = collectFieldsFromInline(child, entries, index);
2539
- }
2540
- }
2541
- return index;
2542
- }
2543
-
2544
- function extractFieldDisplayText(field: FieldNode): string {
2545
- return flattenInlineDisplayText(field.children);
2546
- }
2547
-
2548
- function flattenInlineDisplayText(children: readonly InlineNode[]): string {
2549
- return children
2550
- .map((child) => {
2551
- switch (child.type) {
2552
- case "text":
2553
- return child.text;
2554
- case "tab":
2555
- return "\t";
2556
- case "hard_break":
2557
- case "column_break":
2558
- return "\n";
2559
- case "hyperlink":
2560
- case "field":
2561
- return flattenInlineDisplayText(child.children);
2562
- case "footnote_ref":
2563
- return child.noteId;
2564
- default:
2565
- return "";
2566
- }
2567
- })
2568
- .join("");
2569
- }
2570
-
2571
- function refreshDocumentFields(
2572
- document: CanonicalDocumentEnvelope,
2573
- selectionHead: number,
2574
- activeStory: EditorStoryTarget,
2575
- options?: UpdateFieldsOptions,
2576
- ): {
2577
- document: CanonicalDocumentEnvelope;
2578
- updatedCount: number;
2579
- changed: boolean;
2580
- protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
2581
- } {
2582
- const supportedOnly = options?.supportedOnly ?? true;
2583
- const bookmarkMap = buildBookmarkNameMap(document);
2584
- const paragraphs = collectParagraphContexts(document.content.children);
2585
- const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
2586
- let updatedCount = 0;
2587
- let changed = false;
2588
- let changedFrom: number | undefined;
2589
- let changedTo: number | undefined;
2590
-
2591
- const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
2592
- if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
2593
- return field;
2594
- }
2595
- if (supportedOnly && field.fieldFamily === "TOC") {
2596
- return field;
2597
- }
2598
- const display = resolveSupportedFieldDisplay(
2599
- field,
2600
- document,
2601
- bookmarkMap,
2602
- paragraphs,
2603
- navigation,
2604
- );
2605
- if (!display) {
2606
- return field;
2607
- }
2608
- updatedCount += 1;
2609
- const nextField: FieldNode = {
2610
- ...field,
2611
- children: buildInlineNodesFromDisplayText(display.displayText),
2612
- refreshStatus: display.refreshStatus,
2613
- };
2614
- if (
2615
- nextField.refreshStatus !== field.refreshStatus ||
2616
- flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)
2617
- ) {
2618
- changed = true;
2619
- changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
2620
- changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
2621
- }
2622
- return nextField;
2623
- }).blocks;
2624
- if (!changed) {
2625
- return { document, updatedCount, changed: false };
2626
- }
2627
-
2628
- const nextDocument: CanonicalDocumentEnvelope = {
2629
- ...document,
2630
- content: {
2631
- ...document.content,
2632
- children: nextChildren,
2633
- },
2634
- };
2635
- const nextRegistry = buildFieldRegistry({
2636
- content: nextDocument.content,
2637
- styles: nextDocument.styles,
2638
- subParts: nextDocument.subParts,
2639
- });
2640
- nextDocument.fieldRegistry = nextRegistry;
2641
- let protectionSelection:
2642
- | import("../core/state/editor-state.ts").SelectionSnapshot
2643
- | undefined;
2644
- if (changedFrom !== undefined && changedTo !== undefined) {
2645
- protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
2646
- }
2647
- return {
2648
- document: nextDocument,
2649
- updatedCount,
2650
- changed: true,
2651
- ...(protectionSelection ? { protectionSelection } : {}),
2652
- };
2653
- }
2654
-
2655
- function refreshDocumentTableOfContents(
2656
- document: CanonicalDocumentEnvelope,
2657
- selectionHead: number,
2658
- activeStory: EditorStoryTarget,
2659
- options?: TocRefreshOptions,
2660
- ): {
2661
- document: CanonicalDocumentEnvelope;
2662
- result: TocRefreshResult;
2663
- changed: boolean;
2664
- protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
2665
- } {
2666
- const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
2667
- let changed = false;
2668
- let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
2669
- let changedFrom: number | undefined;
2670
- let changedTo: number | undefined;
2671
- const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
2672
- if (field.fieldFamily !== "TOC") {
2673
- return field;
2674
- }
2675
- const levelRange = options?.maxLevel
2676
- ? { from: 1, to: options.maxLevel }
2677
- : parseTocLevelRange(field.instruction);
2678
- const entries = navigation.headings
2679
- .filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
2680
- .map((heading) => ({
2681
- level: heading.level,
2682
- text: heading.text,
2683
- pageIndex: heading.pageIndex,
2684
- }));
2685
- if (resultEntries.length === 0) {
2686
- resultEntries = entries;
2687
- }
2688
- const nextField: FieldNode = {
2689
- ...field,
2690
- children: buildTocInlineNodes(entries),
2691
- refreshStatus: "current",
2692
- };
2693
- if (flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)) {
2694
- changed = true;
2695
- changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
2696
- changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
2697
- }
2698
- return nextField;
2699
- }).blocks;
2700
- if (!changed) {
2701
- return {
2702
- document,
2703
- result: { entryCount: resultEntries.length, entries: resultEntries },
2704
- changed: false,
2705
- };
2706
- }
2707
-
2708
- const nextDocument: CanonicalDocumentEnvelope = {
2709
- ...document,
2710
- content: {
2711
- ...document.content,
2712
- children: nextChildren,
2713
- },
2714
- };
2715
- const nextRegistry = buildFieldRegistry({
2716
- content: nextDocument.content,
2717
- styles: nextDocument.styles,
2718
- subParts: nextDocument.subParts,
2719
- });
2720
- nextDocument.fieldRegistry = nextRegistry.tocStructure
2721
- ? {
2722
- ...nextRegistry,
2723
- tocStructure: {
2724
- ...nextRegistry.tocStructure,
2725
- status: "current",
2726
- },
2727
- }
2728
- : nextRegistry;
2729
- let protectionSelection:
2730
- | import("../core/state/editor-state.ts").SelectionSnapshot
2731
- | undefined;
2732
- if (changedFrom !== undefined && changedTo !== undefined) {
2733
- protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
2734
- }
2735
-
2736
- return {
2737
- document: nextDocument,
2738
- result: { entryCount: resultEntries.length, entries: resultEntries },
2739
- changed: true,
2740
- ...(protectionSelection ? { protectionSelection } : {}),
2741
- };
2742
- }
2743
-
2744
- function refreshBlocksWithCursor(
2745
- blocks: readonly BlockNode[],
2746
- visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
2747
- cursor = 0,
2748
- previousParagraph = false,
2749
- ): {
2750
- blocks: BlockNode[];
2751
- cursor: number;
2752
- previousParagraph: boolean;
2753
- } {
2754
- const nextBlocks = blocks.map((block) => {
2755
- if (block.type === "paragraph") {
2756
- const paragraphStart = previousParagraph ? cursor + 1 : cursor;
2757
- const refreshedChildren = refreshInlineNodesWithCursor(
2758
- block.children,
2759
- visitField,
2760
- paragraphStart,
2761
- );
2762
- cursor = paragraphStart + refreshedChildren.cursor;
2763
- previousParagraph = true;
2764
- return {
2765
- ...block,
2766
- children: refreshedChildren.nodes,
2767
- };
2768
- }
2769
- if (block.type === "table") {
2770
- cursor += 1;
2771
- previousParagraph = false;
2772
- return {
2773
- ...block,
2774
- rows: block.rows.map((row) => ({
2775
- ...row,
2776
- cells: row.cells.map((cell) => ({
2777
- ...cell,
2778
- children: (() => {
2779
- const refreshed = refreshBlocksWithCursor(cell.children, visitField, cursor, false);
2780
- cursor = refreshed.cursor;
2781
- return refreshed.blocks;
2782
- })(),
2783
- })),
2784
- })),
2785
- };
2786
- }
2787
- if (block.type === "sdt" || block.type === "custom_xml") {
2788
- const refreshed = refreshBlocksWithCursor(
2789
- block.children,
2790
- visitField,
2791
- cursor,
2792
- previousParagraph,
2793
- );
2794
- cursor = refreshed.cursor;
2795
- previousParagraph = refreshed.previousParagraph;
2796
- return {
2797
- ...block,
2798
- children: refreshed.blocks,
2799
- };
2800
- }
2801
- cursor += 1;
2802
- previousParagraph = false;
2803
- return block;
2804
- });
2805
- return { blocks: nextBlocks, cursor, previousParagraph };
2806
- }
2807
-
2808
- function refreshInlineNodesWithCursor(
2809
- nodes: readonly InlineNode[],
2810
- visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
2811
- cursor = 0,
2812
- ): {
2813
- nodes: InlineNode[];
2814
- cursor: number;
2815
- } {
2816
- const nextNodes = nodes.map((node) => {
2817
- if (node.type === "field") {
2818
- const fieldStart = cursor;
2819
- const refreshedChildren = refreshInlineNodesWithCursor(node.children, visitField, cursor);
2820
- const fieldLength = measureInlineNodes(node.children);
2821
- cursor = fieldStart + fieldLength;
2822
- return visitField({
2823
- ...node,
2824
- children: refreshedChildren.nodes,
2825
- }, {
2826
- from: fieldStart,
2827
- to: fieldStart + fieldLength,
2828
- });
2829
- }
2830
- if (node.type === "hyperlink") {
2831
- cursor += measureInlineNodes(node.children);
2832
- return {
2833
- ...node,
2834
- // Hyperlinks only contain text-like children in the canonical model.
2835
- children: [...node.children],
2836
- };
2837
- }
2838
- cursor += measureInlineNode(node);
2839
- return node;
2840
- });
2841
- return { nodes: nextNodes, cursor };
2842
- }
2843
-
2844
- function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
2845
- if (text.length === 0) {
2846
- return [];
2847
- }
2848
- const children: InlineNode[] = [];
2849
- let buffer = "";
2850
- const flushBuffer = () => {
2851
- if (buffer.length > 0) {
2852
- children.push({ type: "text", text: buffer });
2853
- buffer = "";
2854
- }
2855
- };
2856
- for (const character of text) {
2857
- if (character === "\t") {
2858
- flushBuffer();
2859
- children.push({ type: "tab" });
2860
- continue;
2861
- }
2862
- if (character === "\n") {
2863
- flushBuffer();
2864
- children.push({ type: "hard_break" });
2865
- continue;
2866
- }
2867
- buffer += character;
2868
- }
2869
- flushBuffer();
2870
- return children;
2871
- }
2872
-
2873
- function buildTocInlineNodes(
2874
- entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
2875
- ): InlineNode[] {
2876
- const children: InlineNode[] = [];
2877
- entries.forEach((entry, index) => {
2878
- children.push({ type: "text", text: entry.text });
2879
- children.push({ type: "tab" });
2880
- children.push({ type: "text", text: String(entry.pageIndex + 1) });
2881
- if (index < entries.length - 1) {
2882
- children.push({ type: "hard_break" });
2883
- }
2884
- });
2885
- return children;
2886
- }
2887
-
2888
- function collectFieldsFromSubParts(
2889
- subParts: SubPartsCatalog | undefined,
2890
- entries: FieldEntrySnapshot[],
2891
- index: number,
2892
- ): number {
2893
- if (!subParts) {
2894
- return index;
2895
- }
2896
- let nextIndex = index;
2897
- for (const header of subParts.headers ?? []) {
2898
- for (const block of header.blocks) {
2899
- nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
2900
- }
2901
- }
2902
- for (const footer of subParts.footers ?? []) {
2903
- for (const block of footer.blocks) {
2904
- nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
2905
- }
2906
- }
2907
- if (subParts.footnoteCollection) {
2908
- for (const note of Object.values(subParts.footnoteCollection.footnotes)) {
2909
- for (const block of note.blocks) {
2910
- nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
2911
- }
2912
- }
2913
- for (const note of Object.values(subParts.footnoteCollection.endnotes)) {
2914
- for (const block of note.blocks) {
2915
- nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
2916
- }
2917
- }
2918
- }
2919
- return nextIndex;
2920
- }
2921
-
2922
- function resolveSupportedFieldDisplay(
2923
- field: FieldNode,
2924
- document: CanonicalDocumentEnvelope,
2925
- bookmarkMap: Map<string, { bookmarkId: string; paragraphIndex: number }>,
2926
- paragraphs: readonly ParagraphContext[],
2927
- navigation: DocumentNavigationSnapshot,
2928
- ): { displayText: string; refreshStatus: FieldRefreshStatus } | undefined {
2929
- if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
2930
- return undefined;
2931
- }
2932
- if (!field.fieldTarget) {
2933
- return field.fieldFamily === "TOC"
2934
- ? undefined
2935
- : { displayText: "", refreshStatus: "unresolvable" };
2936
- }
2937
- if (field.fieldFamily === "REF") {
2938
- const result = resolveRefFieldText(document, bookmarkMap, field.fieldTarget);
2939
- return result
2940
- ? { displayText: result.text, refreshStatus: result.refreshStatus }
2941
- : { displayText: "", refreshStatus: "unresolvable" };
2942
- }
2943
- const bookmark = bookmarkMap.get(field.fieldTarget);
2944
- if (!bookmark) {
2945
- return { displayText: "", refreshStatus: "unresolvable" };
2946
- }
2947
- if (field.fieldFamily === "PAGEREF") {
2948
- const paragraph = paragraphs[bookmark.paragraphIndex];
2949
- if (!paragraph) {
2950
- return { displayText: "", refreshStatus: "unresolvable" };
2951
- }
2952
- const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
2953
- return { displayText: String(pageIndex + 1), refreshStatus: "current" };
2954
- }
2955
- if (field.fieldFamily === "NOTEREF") {
2956
- const paragraph = paragraphs[bookmark.paragraphIndex]?.paragraph;
2957
- if (!paragraph) {
2958
- return { displayText: "", refreshStatus: "unresolvable" };
2959
- }
2960
- const noteText = resolveNoteReferenceText(paragraph, bookmark.bookmarkId);
2961
- return noteText
2962
- ? { displayText: noteText, refreshStatus: "current" }
2963
- : { displayText: "", refreshStatus: "unresolvable" };
2964
- }
2965
- return undefined;
2966
- }
2967
-
2968
- interface ParagraphContext {
2969
- paragraph: ParagraphNode;
2970
- startOffset: number;
2971
- }
2972
-
2973
- function collectParagraphContexts(blocks: readonly BlockNode[]): ParagraphContext[] {
2974
- const paragraphs: ParagraphContext[] = [];
2975
- collectParagraphContextsFromBlocks(blocks, paragraphs, 0, false);
2976
- return paragraphs;
2977
- }
2978
-
2979
- function collectParagraphContextsFromBlocks(
2980
- blocks: readonly BlockNode[],
2981
- paragraphs: ParagraphContext[],
2982
- cursor: number,
2983
- previousParagraph: boolean,
2984
- ): { cursor: number; previousParagraph: boolean } {
2985
- let nextCursor = cursor;
2986
- let nextPreviousParagraph = previousParagraph;
2987
- for (const block of blocks) {
2988
- if (block.type === "paragraph") {
2989
- if (nextPreviousParagraph) {
2990
- nextCursor += 1;
2991
- }
2992
- paragraphs.push({ paragraph: block, startOffset: nextCursor });
2993
- nextCursor += measureInlineNodes(block.children);
2994
- nextPreviousParagraph = true;
2995
- continue;
2996
- }
2997
- if (block.type === "table") {
2998
- nextCursor += 1;
2999
- nextPreviousParagraph = false;
3000
- for (const row of block.rows) {
3001
- for (const cell of row.cells) {
3002
- const result = collectParagraphContextsFromBlocks(
3003
- cell.children,
3004
- paragraphs,
3005
- nextCursor,
3006
- false,
3007
- );
3008
- nextCursor = result.cursor;
3009
- }
3010
- }
3011
- continue;
3012
- }
3013
- if (block.type === "sdt" || block.type === "custom_xml") {
3014
- const result = collectParagraphContextsFromBlocks(
3015
- block.children,
3016
- paragraphs,
3017
- nextCursor,
3018
- nextPreviousParagraph,
3019
- );
3020
- nextCursor = result.cursor;
3021
- nextPreviousParagraph = result.previousParagraph;
3022
- continue;
3023
- }
3024
- nextCursor += 1;
3025
- nextPreviousParagraph = false;
3026
- }
3027
- return { cursor: nextCursor, previousParagraph: nextPreviousParagraph };
3028
- }
3029
-
3030
- function measureInlineNodes(nodes: readonly InlineNode[]): number {
3031
- return nodes.reduce((size, node) => size + measureInlineNode(node), 0);
3032
- }
3033
-
3034
- function measureInlineNode(node: InlineNode): number {
3035
- switch (node.type) {
3036
- case "text":
3037
- return node.text.length;
3038
- case "tab":
3039
- case "hard_break":
3040
- case "column_break":
3041
- case "footnote_ref":
3042
- case "image":
3043
- case "opaque_inline":
3044
- case "bookmark_start":
3045
- case "bookmark_end":
3046
- return 1;
3047
- case "hyperlink":
3048
- case "field":
3049
- return measureInlineNodes(node.children);
3050
- default:
3051
- return 1;
3052
- }
3053
- }
3054
-
3055
- function resolveNoteReferenceText(paragraph: ParagraphNode, bookmarkId: string): string | undefined {
3056
- let inside = false;
3057
- let sawBoundary = false;
3058
- for (const child of paragraph.children) {
3059
- if (child.type === "bookmark_start" && child.bookmarkId === bookmarkId) {
3060
- inside = true;
3061
- sawBoundary = true;
3062
- continue;
3063
- }
3064
- if (child.type === "bookmark_end" && child.bookmarkId === bookmarkId) {
3065
- break;
3066
- }
3067
- if (!inside) {
3068
- continue;
3069
- }
3070
- if (child.type === "footnote_ref") {
3071
- return child.noteId;
3072
- }
3073
- }
3074
- return sawBoundary ? undefined : undefined;
3075
- }
3076
-
3077
- function getCommandSelection(
3078
- command: EditorCommand,
3079
- fallbackSelection: import("../core/state/editor-state.ts").SelectionSnapshot,
3080
- ): import("../core/state/editor-state.ts").SelectionSnapshot {
3081
- if ("protectionSelection" in command && command.protectionSelection) {
3082
- return command.protectionSelection;
3083
- }
3084
- if ("selection" in command && command.selection) {
3085
- return command.selection;
3086
- }
3087
- return fallbackSelection;
3088
- }
3089
-
3090
- function isBlockedByProtection(
3091
- protection: ProtectionSnapshot,
3092
- selection: import("../core/state/editor-state.ts").SelectionSnapshot,
3093
- ): boolean {
3094
- const enforcedRanges = protection.ranges.filter(
3095
- (range): range is typeof range & { start: number; end: number } =>
3096
- range.enforced && typeof range.start === "number" && typeof range.end === "number",
3097
- );
3098
- if (enforcedRanges.length === 0) {
3099
- return false;
3100
- }
3101
- const from = Math.min(selection.anchor, selection.head);
3102
- const to = Math.max(selection.anchor, selection.head);
3103
- return !enforcedRanges.some((range) =>
3104
- from >= range.start && to <= range.end,
3105
- );
3106
- }
3107
-
3108
- function remapProtectionSnapshot(
3109
- protection: ProtectionSnapshot,
3110
- mapping: import("../core/selection/mapping.ts").TransactionMapping,
3111
- ): ProtectionSnapshot {
3112
- if (mapping.steps.length === 0 || protection.ranges.length === 0) {
3113
- return protection;
3114
- }
3115
- let changed = false;
3116
- const nextRanges = protection.ranges.map((range) => {
3117
- if (
3118
- !range.enforced ||
3119
- typeof range.start !== "number" ||
3120
- typeof range.end !== "number"
3121
- ) {
3122
- return range;
3123
- }
3124
- const mapped = mapRange(
3125
- { from: range.start, to: range.end },
3126
- { start: -1, end: 1 },
3127
- mapping,
3128
- );
3129
- if (mapped.kind === "detached") {
3130
- changed = true;
3131
- return {
3132
- ...range,
3133
- start: undefined,
3134
- end: undefined,
3135
- enforced: false,
3136
- enforcementReason:
3137
- "preserve-only: permission range could not be remapped after runtime edits",
3138
- };
3139
- }
3140
- if (mapped.range.from !== range.start || mapped.range.to !== range.end) {
3141
- changed = true;
3142
- return {
3143
- ...range,
3144
- start: mapped.range.from,
3145
- end: mapped.range.to,
3146
- };
3147
- }
3148
- return range;
3149
- });
3150
- if (!changed) {
3151
- return protection;
3152
- }
3153
- return {
3154
- ...protection,
3155
- ranges: nextRanges,
3156
- enforcedRangeCount: nextRanges.filter((range) => range.enforced).length,
3157
- preservedRangeCount: nextRanges.filter((range) => !range.enforced).length,
3158
- };
3159
- }