@beyondwork/docx-react-component 1.0.28 → 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 (354) 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 +61 -41
  156. package/src/README.md +0 -85
  157. package/src/api/README.md +0 -26
  158. package/src/api/public-types.ts +0 -1421
  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/document-layout.ts +0 -332
  270. package/src/runtime/document-navigation.ts +0 -603
  271. package/src/runtime/document-runtime.ts +0 -3159
  272. package/src/runtime/document-search.ts +0 -145
  273. package/src/runtime/numbering-prefix.ts +0 -216
  274. package/src/runtime/page-layout-estimation.ts +0 -212
  275. package/src/runtime/read-only-diagnostics-runtime.ts +0 -241
  276. package/src/runtime/review-runtime.ts +0 -44
  277. package/src/runtime/revision-runtime.ts +0 -107
  278. package/src/runtime/session-capabilities.ts +0 -192
  279. package/src/runtime/story-context.ts +0 -164
  280. package/src/runtime/story-targeting.ts +0 -162
  281. package/src/runtime/surface-projection.ts +0 -1357
  282. package/src/runtime/table-commands.ts +0 -173
  283. package/src/runtime/table-schema.ts +0 -309
  284. package/src/runtime/view-state.ts +0 -477
  285. package/src/runtime/virtualized-rendering.ts +0 -258
  286. package/src/runtime/workflow-markup.ts +0 -353
  287. package/src/ui/README.md +0 -30
  288. package/src/ui/WordReviewEditor.tsx +0 -4086
  289. package/src/ui/browser-export.ts +0 -52
  290. package/src/ui/comments/README.md +0 -3
  291. package/src/ui/compatibility/README.md +0 -3
  292. package/src/ui/editor-command-bag.ts +0 -120
  293. package/src/ui/editor-runtime-boundary.ts +0 -1457
  294. package/src/ui/editor-shell-view.tsx +0 -142
  295. package/src/ui/editor-surface/README.md +0 -3
  296. package/src/ui/editor-surface-controller.tsx +0 -61
  297. package/src/ui/headless/comment-decoration-model.ts +0 -124
  298. package/src/ui/headless/preserve-editor-selection.ts +0 -5
  299. package/src/ui/headless/revision-decoration-model.ts +0 -128
  300. package/src/ui/headless/selection-helpers.ts +0 -54
  301. package/src/ui/headless/selection-toolbar-model.ts +0 -34
  302. package/src/ui/headless/use-editor-keyboard.ts +0 -103
  303. package/src/ui/review/README.md +0 -3
  304. package/src/ui/runtime-snapshot-selectors.ts +0 -197
  305. package/src/ui/shared/revision-filters.ts +0 -31
  306. package/src/ui/status/README.md +0 -3
  307. package/src/ui/theme/README.md +0 -3
  308. package/src/ui/toolbar/README.md +0 -3
  309. package/src/ui/workflow-surface-blocked-rails.ts +0 -94
  310. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +0 -64
  311. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +0 -129
  312. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +0 -114
  313. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +0 -34
  314. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +0 -386
  315. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -186
  316. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +0 -139
  317. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +0 -128
  318. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +0 -58
  319. package/src/ui-tailwind/chrome/use-before-unload.ts +0 -20
  320. package/src/ui-tailwind/editor-surface/perf-probe.ts +0 -179
  321. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +0 -184
  322. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +0 -31
  323. package/src/ui-tailwind/editor-surface/pm-decorations.ts +0 -427
  324. package/src/ui-tailwind/editor-surface/pm-position-map.ts +0 -123
  325. package/src/ui-tailwind/editor-surface/pm-schema.ts +0 -876
  326. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +0 -504
  327. package/src/ui-tailwind/editor-surface/search-plugin.ts +0 -168
  328. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +0 -61
  329. package/src/ui-tailwind/editor-surface/tw-caret.tsx +0 -12
  330. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +0 -150
  331. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +0 -129
  332. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +0 -58
  333. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +0 -151
  334. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +0 -944
  335. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +0 -111
  336. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -436
  337. package/src/ui-tailwind/index.ts +0 -62
  338. package/src/ui-tailwind/page-chrome-model.ts +0 -27
  339. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +0 -406
  340. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -149
  341. package/src/ui-tailwind/review/tw-review-rail.tsx +0 -120
  342. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +0 -164
  343. package/src/ui-tailwind/status/tw-status-bar.tsx +0 -61
  344. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +0 -52
  345. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +0 -1064
  346. package/src/ui-tailwind/tw-review-workspace.tsx +0 -1417
  347. package/src/validation/README.md +0 -3
  348. package/src/validation/compatibility-engine.ts +0 -634
  349. package/src/validation/compatibility-report.ts +0 -161
  350. package/src/validation/diagnostics.ts +0 -204
  351. package/src/validation/docx-comment-proof.ts +0 -707
  352. package/src/validation/import-diagnostics.ts +0 -128
  353. package/src/validation/low-priority-word-surfaces.ts +0 -373
  354. /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
- }