@beyondwork/docx-react-component 1.0.29 → 1.0.30

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 (381) hide show
  1. package/package.json +65 -96
  2. package/src/README.md +85 -0
  3. package/src/api/README.md +26 -0
  4. package/src/api/public-types.ts +1952 -0
  5. package/src/api/session-state.ts +62 -0
  6. package/src/compare/diff-engine.ts +623 -0
  7. package/src/compare/export-redlines.ts +280 -0
  8. package/src/compare/index.ts +25 -0
  9. package/src/compare/snapshot.ts +97 -0
  10. package/src/component-inventory.md +99 -0
  11. package/src/core/README.md +10 -0
  12. package/src/core/commands/README.md +3 -0
  13. package/{dist/chunk-TJBP2K4T.js → src/core/commands/formatting-commands.ts} +536 -196
  14. package/src/core/commands/image-commands.ts +373 -0
  15. package/src/core/commands/index.ts +1879 -0
  16. package/src/core/commands/list-commands.ts +565 -0
  17. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  18. package/src/core/commands/review-commands.ts +108 -0
  19. package/{dist/core/commands/section-layout-commands.cjs → src/core/commands/section-layout-commands.ts} +340 -137
  20. package/src/core/commands/structural-helpers.ts +309 -0
  21. package/{dist/core/commands/style-commands.cjs → src/core/commands/style-commands.ts} +113 -65
  22. package/src/core/commands/table-structure-commands.ts +854 -0
  23. package/{dist/chunk-UZXBISGO.js → src/core/commands/text-commands.ts} +142 -86
  24. package/src/core/schema/README.md +3 -0
  25. package/src/core/schema/text-schema.ts +516 -0
  26. package/src/core/search/search-text.ts +357 -0
  27. package/src/core/selection/README.md +3 -0
  28. package/src/core/selection/mapping.ts +289 -0
  29. package/src/core/selection/review-anchors.ts +183 -0
  30. package/src/core/state/README.md +3 -0
  31. package/src/core/state/editor-state.ts +892 -0
  32. package/src/core/state/text-transaction.ts +869 -0
  33. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  34. package/src/formats/xlsx/io/parse-sheet.ts +459 -0
  35. package/src/formats/xlsx/io/parse-styles.ts +59 -0
  36. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  37. package/src/formats/xlsx/io/serialize-shared-strings.ts +72 -0
  38. package/src/formats/xlsx/io/serialize-sheet.ts +333 -0
  39. package/src/formats/xlsx/io/serialize-styles.ts +98 -0
  40. package/src/formats/xlsx/io/serialize-workbook.ts +429 -0
  41. package/src/formats/xlsx/io/xlsx-session.ts +314 -0
  42. package/src/formats/xlsx/model/cell.ts +189 -0
  43. package/src/formats/xlsx/model/sheet.ts +326 -0
  44. package/src/formats/xlsx/model/styles.ts +118 -0
  45. package/src/formats/xlsx/model/workbook.ts +453 -0
  46. package/src/formats/xlsx/runtime/cell-commands.ts +567 -0
  47. package/src/formats/xlsx/runtime/sheet-commands.ts +206 -0
  48. package/src/formats/xlsx/runtime/workbook-runtime.ts +177 -0
  49. package/src/formats/xlsx/runtime/workbook-transaction.ts +822 -0
  50. package/src/index.ts +142 -0
  51. package/src/io/README.md +10 -0
  52. package/src/io/docx-session.ts +3175 -0
  53. package/src/io/export/README.md +3 -0
  54. package/src/io/export/export-session.ts +220 -0
  55. package/src/io/export/minimal-docx.ts +115 -0
  56. package/src/io/export/reattach-preserved-parts.ts +54 -0
  57. package/src/io/export/serialize-comments.ts +947 -0
  58. package/src/io/export/serialize-footnotes.ts +394 -0
  59. package/src/io/export/serialize-headers-footers.ts +368 -0
  60. package/src/io/export/serialize-main-document.ts +1342 -0
  61. package/src/io/export/serialize-numbering.ts +218 -0
  62. package/src/io/export/serialize-revisions.ts +389 -0
  63. package/src/io/export/serialize-runtime-revisions.ts +463 -0
  64. package/src/io/export/serialize-tables.ts +174 -0
  65. package/src/io/export/split-review-boundaries.ts +356 -0
  66. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  67. package/src/io/export/table-properties-xml.ts +318 -0
  68. package/src/io/normalize/README.md +3 -0
  69. package/src/io/normalize/normalize-text.ts +670 -0
  70. package/src/io/ooxml/README.md +3 -0
  71. package/src/io/ooxml/highlight-colors.ts +39 -0
  72. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  73. package/src/io/ooxml/parse-comments.ts +852 -0
  74. package/src/io/ooxml/parse-complex-content.ts +287 -0
  75. package/src/io/ooxml/parse-fields.ts +834 -0
  76. package/src/io/ooxml/parse-footnotes.ts +952 -0
  77. package/src/io/ooxml/parse-headers-footers.ts +1212 -0
  78. package/src/io/ooxml/parse-inline-media.ts +461 -0
  79. package/src/io/ooxml/parse-main-document.ts +2947 -0
  80. package/src/io/ooxml/parse-numbering.ts +747 -0
  81. package/src/io/ooxml/parse-revisions.ts +1045 -0
  82. package/src/io/ooxml/parse-settings.ts +184 -0
  83. package/src/io/ooxml/parse-shapes.ts +296 -0
  84. package/src/io/ooxml/parse-styles.ts +639 -0
  85. package/src/io/ooxml/parse-tables.ts +627 -0
  86. package/src/io/ooxml/parse-theme.ts +346 -0
  87. package/src/io/ooxml/part-manifest.ts +136 -0
  88. package/src/io/ooxml/revision-boundaries.ts +475 -0
  89. package/src/io/ooxml/workflow-payload.ts +544 -0
  90. package/src/io/opc/README.md +3 -0
  91. package/src/io/opc/corrupt-package.ts +166 -0
  92. package/src/io/opc/docx-package.ts +74 -0
  93. package/src/io/opc/package-reader.ts +325 -0
  94. package/src/io/opc/package-writer.ts +273 -0
  95. package/src/io/source-package-provenance.ts +241 -0
  96. package/{dist/chunk-RMH72RZI.js → src/legal/bookmarks.ts} +130 -44
  97. package/src/legal/cross-references.ts +414 -0
  98. package/src/legal/defined-terms.ts +203 -0
  99. package/src/legal/index.ts +32 -0
  100. package/src/legal/signature-blocks.ts +259 -0
  101. package/src/model/README.md +3 -0
  102. package/src/model/canonical-document.ts +2722 -0
  103. package/src/model/cds-1.0.0.ts +212 -0
  104. package/src/model/snapshot.ts +760 -0
  105. package/src/preservation/README.md +3 -0
  106. package/src/preservation/markup-compatibility.ts +48 -0
  107. package/src/preservation/opaque-fragment-store.ts +89 -0
  108. package/src/preservation/opaque-region.ts +233 -0
  109. package/src/preservation/package-preservation.ts +113 -0
  110. package/src/preservation/preserved-part-manifest.ts +56 -0
  111. package/src/preservation/relationship-retention.ts +57 -0
  112. package/src/preservation/store.ts +255 -0
  113. package/src/review/README.md +16 -0
  114. package/src/review/store/README.md +3 -0
  115. package/src/review/store/comment-anchors.ts +70 -0
  116. package/src/review/store/comment-remapping.ts +154 -0
  117. package/src/review/store/comment-store.ts +349 -0
  118. package/src/review/store/comment-thread.ts +109 -0
  119. package/src/review/store/revision-actions.ts +423 -0
  120. package/src/review/store/revision-store.ts +323 -0
  121. package/src/review/store/revision-types.ts +182 -0
  122. package/src/review/store/runtime-comment-store.ts +43 -0
  123. package/src/runtime/README.md +3 -0
  124. package/src/runtime/ai-action-policy.ts +764 -0
  125. package/src/runtime/context-analytics.ts +824 -0
  126. package/src/runtime/document-layout.ts +332 -0
  127. package/src/runtime/document-locations.ts +521 -0
  128. package/src/runtime/document-navigation.ts +616 -0
  129. package/src/runtime/document-outline.ts +440 -0
  130. package/src/runtime/document-runtime.ts +4055 -0
  131. package/src/runtime/document-search.ts +145 -0
  132. package/src/runtime/event-refresh-hints.ts +137 -0
  133. package/src/runtime/numbering-prefix.ts +244 -0
  134. package/src/runtime/page-layout-estimation.ts +305 -0
  135. package/src/runtime/read-only-diagnostics-runtime.ts +241 -0
  136. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  137. package/src/runtime/review-runtime.ts +44 -0
  138. package/src/runtime/revision-runtime.ts +107 -0
  139. package/src/runtime/session-capabilities.ts +192 -0
  140. package/src/runtime/story-context.ts +164 -0
  141. package/src/runtime/story-targeting.ts +162 -0
  142. package/src/runtime/suggestions-snapshot.ts +137 -0
  143. package/src/runtime/surface-projection.ts +1553 -0
  144. package/src/runtime/table-commands.ts +173 -0
  145. package/src/runtime/table-schema.ts +309 -0
  146. package/src/runtime/table-style-resolver.ts +409 -0
  147. package/src/runtime/view-state.ts +493 -0
  148. package/src/runtime/virtualized-rendering.ts +258 -0
  149. package/src/runtime/workflow-markup.ts +393 -0
  150. package/src/ui/README.md +30 -0
  151. package/src/ui/WordReviewEditor.tsx +5268 -0
  152. package/src/ui/browser-export.ts +52 -0
  153. package/src/ui/comments/README.md +3 -0
  154. package/src/ui/compatibility/README.md +3 -0
  155. package/src/ui/editor-command-bag.ts +127 -0
  156. package/src/ui/editor-runtime-boundary.ts +1558 -0
  157. package/src/ui/editor-shell-view.tsx +144 -0
  158. package/src/ui/editor-surface/README.md +3 -0
  159. package/src/ui/editor-surface-controller.tsx +66 -0
  160. package/src/ui/headless/comment-decoration-model.ts +124 -0
  161. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  162. package/src/ui/headless/revision-decoration-model.ts +128 -0
  163. package/src/ui/headless/selection-helpers.ts +54 -0
  164. package/src/ui/headless/selection-tool-context.ts +19 -0
  165. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  166. package/src/ui/headless/selection-tool-types.ts +129 -0
  167. package/src/ui/headless/selection-toolbar-model.ts +11 -0
  168. package/src/ui/headless/use-editor-keyboard.ts +103 -0
  169. package/src/ui/review/README.md +3 -0
  170. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  171. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  172. package/src/ui/shared/revision-filters.ts +31 -0
  173. package/src/ui/status/README.md +3 -0
  174. package/src/ui/theme/README.md +3 -0
  175. package/src/ui/toolbar/README.md +3 -0
  176. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  177. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  178. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  179. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  180. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +64 -0
  181. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  182. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +121 -0
  183. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  184. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +30 -0
  185. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +365 -0
  186. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  187. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  188. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  189. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  190. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  191. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  192. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  193. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +186 -0
  194. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  195. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +200 -0
  196. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  197. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  198. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  199. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +189 -0
  200. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  201. package/src/ui-tailwind/editor-surface/pm-decorations.ts +411 -0
  202. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  203. package/src/ui-tailwind/editor-surface/pm-schema.ts +927 -0
  204. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +567 -0
  205. package/src/ui-tailwind/editor-surface/search-plugin.ts +168 -0
  206. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +65 -0
  207. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  208. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  209. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +129 -0
  210. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +58 -0
  211. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  212. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +1047 -0
  213. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  214. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +503 -0
  215. package/src/ui-tailwind/index.ts +62 -0
  216. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  217. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +406 -0
  218. package/src/ui-tailwind/review/tw-health-panel.tsx +149 -0
  219. package/src/ui-tailwind/review/tw-review-rail.tsx +122 -0
  220. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  221. package/src/ui-tailwind/status/tw-status-bar.tsx +65 -0
  222. package/{dist → src}/ui-tailwind/theme/editor-theme.css +58 -40
  223. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +52 -0
  224. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +1133 -0
  225. package/src/ui-tailwind/tw-review-workspace.tsx +1460 -0
  226. package/src/validation/README.md +3 -0
  227. package/src/validation/compatibility-engine.ts +878 -0
  228. package/src/validation/compatibility-report.ts +161 -0
  229. package/src/validation/diagnostics.ts +204 -0
  230. package/src/validation/docx-comment-proof.ts +720 -0
  231. package/src/validation/import-diagnostics.ts +128 -0
  232. package/src/validation/low-priority-word-surfaces.ts +373 -0
  233. package/dist/canonical-document-BLEbzL2J.d.cts +0 -844
  234. package/dist/canonical-document-BLEbzL2J.d.ts +0 -844
  235. package/dist/chunk-2FJS5GZM.js +0 -763
  236. package/dist/chunk-2FJS5GZM.js.map +0 -1
  237. package/dist/chunk-2OQBZS3F.js +0 -446
  238. package/dist/chunk-2OQBZS3F.js.map +0 -1
  239. package/dist/chunk-2S7W4KFO.js +0 -127
  240. package/dist/chunk-2S7W4KFO.js.map +0 -1
  241. package/dist/chunk-2TG72QSW.js +0 -3874
  242. package/dist/chunk-2TG72QSW.js.map +0 -1
  243. package/dist/chunk-36QNIZBO.js +0 -532
  244. package/dist/chunk-36QNIZBO.js.map +0 -1
  245. package/dist/chunk-4AQOYAW4.js +0 -3069
  246. package/dist/chunk-4AQOYAW4.js.map +0 -1
  247. package/dist/chunk-4D5EWJ3P.js +0 -77
  248. package/dist/chunk-4D5EWJ3P.js.map +0 -1
  249. package/dist/chunk-5FN54NDH.js +0 -2257
  250. package/dist/chunk-5FN54NDH.js.map +0 -1
  251. package/dist/chunk-BOYGQYRQ.js +0 -7306
  252. package/dist/chunk-BOYGQYRQ.js.map +0 -1
  253. package/dist/chunk-CN3XMECL.js +0 -212
  254. package/dist/chunk-CN3XMECL.js.map +0 -1
  255. package/dist/chunk-EBI3BX6U.js +0 -164
  256. package/dist/chunk-EBI3BX6U.js.map +0 -1
  257. package/dist/chunk-EILUG3VB.js +0 -1275
  258. package/dist/chunk-EILUG3VB.js.map +0 -1
  259. package/dist/chunk-FUDY333O.js +0 -70
  260. package/dist/chunk-FUDY333O.js.map +0 -1
  261. package/dist/chunk-GBVOWFIK.js +0 -1237
  262. package/dist/chunk-GBVOWFIK.js.map +0 -1
  263. package/dist/chunk-H4TQ3H3Y.js +0 -262
  264. package/dist/chunk-H4TQ3H3Y.js.map +0 -1
  265. package/dist/chunk-JGB3IXZO.js +0 -189
  266. package/dist/chunk-JGB3IXZO.js.map +0 -1
  267. package/dist/chunk-KD2QRQPY.js +0 -4342
  268. package/dist/chunk-KD2QRQPY.js.map +0 -1
  269. package/dist/chunk-KLMXQVYK.js +0 -369
  270. package/dist/chunk-KLMXQVYK.js.map +0 -1
  271. package/dist/chunk-KZUG5KFQ.js +0 -214
  272. package/dist/chunk-KZUG5KFQ.js.map +0 -1
  273. package/dist/chunk-QDAQ4CJU.js +0 -345
  274. package/dist/chunk-QDAQ4CJU.js.map +0 -1
  275. package/dist/chunk-RMH72RZI.js.map +0 -1
  276. package/dist/chunk-SWKWQZXM.js +0 -117
  277. package/dist/chunk-SWKWQZXM.js.map +0 -1
  278. package/dist/chunk-TJBP2K4T.js.map +0 -1
  279. package/dist/chunk-TLCEAQDQ.js +0 -542
  280. package/dist/chunk-TLCEAQDQ.js.map +0 -1
  281. package/dist/chunk-UZXBISGO.js.map +0 -1
  282. package/dist/chunk-WGBAKP3Q.js +0 -3220
  283. package/dist/chunk-WGBAKP3Q.js.map +0 -1
  284. package/dist/compare/index.cjs +0 -5475
  285. package/dist/compare/index.cjs.map +0 -1
  286. package/dist/compare/index.d.cts +0 -114
  287. package/dist/compare/index.d.ts +0 -114
  288. package/dist/compare/index.js +0 -731
  289. package/dist/compare/index.js.map +0 -1
  290. package/dist/core/commands/formatting-commands.cjs +0 -828
  291. package/dist/core/commands/formatting-commands.cjs.map +0 -1
  292. package/dist/core/commands/formatting-commands.d.cts +0 -63
  293. package/dist/core/commands/formatting-commands.d.ts +0 -63
  294. package/dist/core/commands/formatting-commands.js +0 -37
  295. package/dist/core/commands/formatting-commands.js.map +0 -1
  296. package/dist/core/commands/image-commands.cjs +0 -2023
  297. package/dist/core/commands/image-commands.cjs.map +0 -1
  298. package/dist/core/commands/image-commands.d.cts +0 -58
  299. package/dist/core/commands/image-commands.d.ts +0 -58
  300. package/dist/core/commands/image-commands.js +0 -18
  301. package/dist/core/commands/image-commands.js.map +0 -1
  302. package/dist/core/commands/section-layout-commands.cjs.map +0 -1
  303. package/dist/core/commands/section-layout-commands.d.cts +0 -62
  304. package/dist/core/commands/section-layout-commands.d.ts +0 -62
  305. package/dist/core/commands/section-layout-commands.js +0 -21
  306. package/dist/core/commands/section-layout-commands.js.map +0 -1
  307. package/dist/core/commands/style-commands.cjs.map +0 -1
  308. package/dist/core/commands/style-commands.d.cts +0 -13
  309. package/dist/core/commands/style-commands.d.ts +0 -13
  310. package/dist/core/commands/style-commands.js +0 -9
  311. package/dist/core/commands/style-commands.js.map +0 -1
  312. package/dist/core/commands/table-structure-commands.cjs +0 -1883
  313. package/dist/core/commands/table-structure-commands.cjs.map +0 -1
  314. package/dist/core/commands/table-structure-commands.d.cts +0 -59
  315. package/dist/core/commands/table-structure-commands.d.ts +0 -59
  316. package/dist/core/commands/table-structure-commands.js +0 -12
  317. package/dist/core/commands/table-structure-commands.js.map +0 -1
  318. package/dist/core/commands/text-commands.cjs +0 -2391
  319. package/dist/core/commands/text-commands.cjs.map +0 -1
  320. package/dist/core/commands/text-commands.d.cts +0 -24
  321. package/dist/core/commands/text-commands.d.ts +0 -24
  322. package/dist/core/commands/text-commands.js +0 -28
  323. package/dist/core/commands/text-commands.js.map +0 -1
  324. package/dist/core/selection/mapping.cjs +0 -200
  325. package/dist/core/selection/mapping.cjs.map +0 -1
  326. package/dist/core/selection/mapping.d.cts +0 -2
  327. package/dist/core/selection/mapping.d.ts +0 -2
  328. package/dist/core/selection/mapping.js +0 -31
  329. package/dist/core/selection/mapping.js.map +0 -1
  330. package/dist/core/state/editor-state.cjs +0 -2278
  331. package/dist/core/state/editor-state.cjs.map +0 -1
  332. package/dist/core/state/editor-state.d.cts +0 -2
  333. package/dist/core/state/editor-state.d.ts +0 -2
  334. package/dist/core/state/editor-state.js +0 -26
  335. package/dist/core/state/editor-state.js.map +0 -1
  336. package/dist/index.cjs +0 -38553
  337. package/dist/index.cjs.map +0 -1
  338. package/dist/index.d.cts +0 -15
  339. package/dist/index.d.ts +0 -15
  340. package/dist/index.js +0 -7856
  341. package/dist/index.js.map +0 -1
  342. package/dist/io/docx-session.cjs +0 -16236
  343. package/dist/io/docx-session.cjs.map +0 -1
  344. package/dist/io/docx-session.d.cts +0 -21
  345. package/dist/io/docx-session.d.ts +0 -21
  346. package/dist/io/docx-session.js +0 -18
  347. package/dist/io/docx-session.js.map +0 -1
  348. package/dist/legal/index.cjs +0 -3900
  349. package/dist/legal/index.cjs.map +0 -1
  350. package/dist/legal/index.d.cts +0 -86
  351. package/dist/legal/index.d.ts +0 -86
  352. package/dist/legal/index.js +0 -616
  353. package/dist/legal/index.js.map +0 -1
  354. package/dist/public-types-7ZL_94cz.d.ts +0 -1573
  355. package/dist/public-types-CeMaDueh.d.cts +0 -1573
  356. package/dist/public-types.cjs +0 -19
  357. package/dist/public-types.cjs.map +0 -1
  358. package/dist/public-types.d.cts +0 -2
  359. package/dist/public-types.d.ts +0 -2
  360. package/dist/public-types.js +0 -1
  361. package/dist/public-types.js.map +0 -1
  362. package/dist/runtime/document-runtime.cjs +0 -11140
  363. package/dist/runtime/document-runtime.cjs.map +0 -1
  364. package/dist/runtime/document-runtime.d.cts +0 -231
  365. package/dist/runtime/document-runtime.d.ts +0 -231
  366. package/dist/runtime/document-runtime.js +0 -21
  367. package/dist/runtime/document-runtime.js.map +0 -1
  368. package/dist/structural-helpers-CilgOVhh.d.cts +0 -10
  369. package/dist/structural-helpers-q0Gd-eBN.d.ts +0 -10
  370. package/dist/ui-tailwind/editor-surface/search-plugin.cjs +0 -313
  371. package/dist/ui-tailwind/editor-surface/search-plugin.cjs.map +0 -1
  372. package/dist/ui-tailwind/editor-surface/search-plugin.d.cts +0 -67
  373. package/dist/ui-tailwind/editor-surface/search-plugin.d.ts +0 -67
  374. package/dist/ui-tailwind/editor-surface/search-plugin.js +0 -23
  375. package/dist/ui-tailwind/editor-surface/search-plugin.js.map +0 -1
  376. package/dist/ui-tailwind/index.cjs +0 -4833
  377. package/dist/ui-tailwind/index.cjs.map +0 -1
  378. package/dist/ui-tailwind/index.d.cts +0 -617
  379. package/dist/ui-tailwind/index.d.ts +0 -617
  380. package/dist/ui-tailwind/index.js +0 -575
  381. package/dist/ui-tailwind/index.js.map +0 -1
@@ -0,0 +1,4055 @@
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
+ DocumentChunkSnapshot,
21
+ DocumentLocationSnapshot,
22
+ DocumentMode,
23
+ DocumentNavigationSnapshot,
24
+ DocumentOutlineSnapshot,
25
+ DocumentSectionSnapshot,
26
+ DocumentTextToken,
27
+ EditorSessionState,
28
+ EditorAnchorProjection,
29
+ EditorError,
30
+ EditorStoryTarget,
31
+ EditorViewStateSnapshot,
32
+ EditorWarning,
33
+ FieldEntrySnapshot,
34
+ FieldSnapshot,
35
+ HeaderFooterLinkPatch,
36
+ ExportDocxOptions,
37
+ ExportResult,
38
+ HostAnnotationOverlay,
39
+ HostAnnotationSnapshot,
40
+ InteractionGuardSnapshot,
41
+ PageLayoutSnapshot,
42
+ PersistedEditorSnapshot,
43
+ ProtectionSnapshot,
44
+ RestorePointSnapshot,
45
+ RestoreResult,
46
+ ReviewWorkSnapshot,
47
+ RuntimeContextAnalyticsQuery,
48
+ RuntimeContextAnalyticsSnapshot,
49
+ RuntimeRenderSnapshot,
50
+ SelectionSnapshot,
51
+ SnapshotRefreshHints,
52
+ SuggestionsSnapshot,
53
+ SurfaceBlockSnapshot,
54
+ SurfaceInlineSegment,
55
+ StoryTextStreamSnapshot,
56
+ TocSnapshot,
57
+ StyleCatalogSnapshot,
58
+ TocRefreshOptions,
59
+ TocRefreshResult,
60
+ TrackedChangeEntrySnapshot,
61
+ TrackedChangesSnapshot,
62
+ UpdateFieldsOptions,
63
+ UpdateFieldsResult,
64
+ ViewMode,
65
+ WorkflowCandidateRange,
66
+ WorkflowCandidateRangeOptions,
67
+ WorkflowBlockedCommandReason,
68
+ WorkflowMetadataDefinition,
69
+ WorkflowMetadataEntry,
70
+ WorkflowMetadataSnapshot,
71
+ WorkflowMarkupSnapshot,
72
+ WorkflowOverlay,
73
+ WorkflowScopeSnapshot,
74
+ WorkspaceMode,
75
+ WordReviewEditorEvent,
76
+ ZoomLevel,
77
+ } from "../api/public-types";
78
+ import {
79
+ editorSessionStateFromPersistedSnapshot,
80
+ persistedSnapshotFromEditorSessionState,
81
+ } from "../api/session-state.ts";
82
+ import {
83
+ executeEditorCommand,
84
+ selectionChanged,
85
+ type CommandOrigin,
86
+ type EditorCommand,
87
+ type EditorTransaction,
88
+ } from "../core/commands/index.ts";
89
+ import {
90
+ createDetachedAnchor,
91
+ createEmptyMapping,
92
+ createNodeAnchor,
93
+ createRangeAnchor,
94
+ mapRange,
95
+ MAIN_STORY_TARGET,
96
+ storyTargetsEqual,
97
+ type EditorAnchorProjection as InternalEditorAnchorProjection,
98
+ } from "../core/selection/mapping.ts";
99
+ import { canCreateDocxCommentAnchor } from "../core/selection/review-anchors.ts";
100
+ import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
101
+ import {
102
+ describeOpaqueFragment,
103
+ findOpaqueFragmentsIntersectingRange,
104
+ isBlockedImportFeatureKey,
105
+ } from "../preservation/store.ts";
106
+ import { createCommentSidebarProjection } from "../review/store/comment-store.ts";
107
+ import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
108
+ import {
109
+ createRevisionSidebarProjection,
110
+ type RevisionStore,
111
+ } from "../review/store/revision-store.ts";
112
+ import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
113
+ import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
114
+ import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
115
+ import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
116
+ import {
117
+ collectWorkflowMarkupSnapshot,
118
+ deriveWorkflowCandidateRangesFromMarkup,
119
+ } from "./workflow-markup.ts";
120
+ import {
121
+ createDocumentNavigationSnapshot,
122
+ findPageForOffset,
123
+ } from "./document-navigation.ts";
124
+ import {
125
+ createDocumentOutlineSnapshot,
126
+ createDocumentSectionSnapshots,
127
+ createSectionLocations,
128
+ createTocSnapshot,
129
+ findDocumentSectionSnapshot,
130
+ } from "./document-outline.ts";
131
+ import {
132
+ createCurrentLocation,
133
+ createDocumentChunks,
134
+ createDocumentLocationSnapshot,
135
+ createDocumentTextStreamSnapshots,
136
+ createLocationFromSelection,
137
+ createRestorePoint,
138
+ createReviewWorkSnapshot,
139
+ createWorkflowChunks,
140
+ } from "./document-locations.ts";
141
+ import { describeEventImpact } from "./event-refresh-hints.ts";
142
+ import {
143
+ createRuntimeContextAnalyticsSnapshot,
144
+ resolveCurrentContextAnalyticsQuery,
145
+ runtimeContextAnalyticsSnapshotsEqual,
146
+ } from "./context-analytics.ts";
147
+ import {
148
+ buildPageLayoutSnapshot,
149
+ buildResolvedSections,
150
+ resolveActiveSection,
151
+ } from "./document-layout.ts";
152
+ import { normalizeHeaderFooterTarget } from "./story-context.ts";
153
+ import {
154
+ getStoryBlocks,
155
+ replaceStoryBlocks,
156
+ storyTargetKey,
157
+ } from "./story-targeting.ts";
158
+ import {
159
+ createViewState,
160
+ setViewMode as applyViewMode,
161
+ setDocumentMode as applyDocumentMode,
162
+ setWorkspaceMode as applyWorkspaceMode,
163
+ setZoomLevel as applyZoomLevel,
164
+ setFocused as applyFocused,
165
+ setCaretAffinity as applyCaretAffinity,
166
+ setActivePageRegion as applyActivePageRegion,
167
+ setActiveObjectFrame as applyActiveObjectFrame,
168
+ createEditorViewStateSnapshot,
169
+ type ViewState,
170
+ } from "./view-state.ts";
171
+ import type {
172
+ BlockNode,
173
+ FieldNode,
174
+ FieldRefreshStatus,
175
+ InlineNode,
176
+ PageMargins,
177
+ ParagraphNode,
178
+ SubPartsCatalog,
179
+ } from "../model/canonical-document.ts";
180
+ import {
181
+ buildFieldRegistry,
182
+ isSupportedFieldFamily,
183
+ parseTocLevelRange,
184
+ resolveRefFieldText,
185
+ } from "../io/ooxml/parse-fields.ts";
186
+ import {
187
+ incrementInvalidationCounter,
188
+ recordPerfSample,
189
+ } from "../ui-tailwind/editor-surface/perf-probe.ts";
190
+
191
+ export type Unsubscribe = () => void;
192
+
193
+ type RuntimeReadySource = "docx" | "session" | "snapshot" | "datastore" | "canonical";
194
+
195
+ export type DocumentRuntimeEvent =
196
+ | (Omit<Extract<WordReviewEditorEvent, { type: "ready" }>, "source"> & {
197
+ source: RuntimeReadySource;
198
+ })
199
+ | Exclude<WordReviewEditorEvent, { type: "ready" }>;
200
+
201
+ export type ActiveStoryTextCommand =
202
+ | Extract<EditorCommand, { type: "text.insert" }>
203
+ | Extract<EditorCommand, { type: "text.delete-backward" }>
204
+ | Extract<EditorCommand, { type: "text.delete-forward" }>
205
+ | Extract<EditorCommand, { type: "text.insert-tab" }>
206
+ | Extract<EditorCommand, { type: "text.insert-hard-break" }>
207
+ | Extract<EditorCommand, { type: "paragraph.split" }>;
208
+
209
+ export interface DocumentRuntime {
210
+ subscribe(listener: () => void): Unsubscribe;
211
+ subscribeToEvents(listener: (event: DocumentRuntimeEvent) => void): Unsubscribe;
212
+ getRenderSnapshot(): RuntimeRenderSnapshot;
213
+ getCanonicalDocument(): CanonicalDocumentEnvelope;
214
+ getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
215
+ replaceText(text: string, target?: EditorAnchorProjection): void;
216
+ applyActiveStoryTextCommand(command: ActiveStoryTextCommand): void;
217
+ dispatch(command: EditorCommand): void;
218
+ emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
219
+ undo(): void;
220
+ redo(): void;
221
+ focus(): void;
222
+ blur(): void;
223
+ setDefaultAuthorId?(authorId?: string): void;
224
+ getDefaultAuthorId?(): string | undefined;
225
+ addComment(params: AddCommentParams): string;
226
+ openComment(commentId: string): void;
227
+ resolveComment(commentId: string): void;
228
+ reopenComment(commentId: string): void;
229
+ addCommentReply(commentId: string, body: string, authorId?: string): void;
230
+ editCommentBody(commentId: string, body: string): void;
231
+ acceptChange(changeId: string): void;
232
+ rejectChange(changeId: string): void;
233
+ acceptAllChanges(): void;
234
+ rejectAllChanges(): void;
235
+ openStory(target: EditorStoryTarget): boolean;
236
+ closeStory(): void;
237
+ getActiveStory(): EditorStoryTarget;
238
+ getViewState(): EditorViewStateSnapshot;
239
+ setViewMode(mode: ViewMode): void;
240
+ setDocumentMode(mode: DocumentMode): void;
241
+ getProtectionSnapshot(): ProtectionSnapshot;
242
+ setWorkspaceMode(mode: WorkspaceMode): void;
243
+ setZoom(level: ZoomLevel): void;
244
+ getPageLayoutSnapshot(): PageLayoutSnapshot | null;
245
+ getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
246
+ getCurrentLocation(): DocumentLocationSnapshot | null;
247
+ getLocationForSelection(selection: SelectionSnapshot): DocumentLocationSnapshot | null;
248
+ getLocationForAnchor(
249
+ anchor: EditorAnchorProjection,
250
+ storyTarget?: EditorStoryTarget,
251
+ ): DocumentLocationSnapshot | null;
252
+ captureRestorePoint(
253
+ input?: SelectionSnapshot | EditorAnchorProjection,
254
+ ): RestorePointSnapshot | null;
255
+ restoreToPoint(
256
+ point: RestorePointSnapshot,
257
+ options?: { behavior?: "exact" | "semantic"; scroll?: boolean },
258
+ ): RestoreResult;
259
+ getOutlineSnapshot(): DocumentOutlineSnapshot;
260
+ getTocSnapshot(): TocSnapshot | null;
261
+ getSections(): DocumentSectionSnapshot[];
262
+ getSectionSnapshot(input: {
263
+ sectionIndex?: number;
264
+ headingId?: string;
265
+ bookmarkName?: string;
266
+ }): DocumentSectionSnapshot | null;
267
+ describeEventImpact(event: WordReviewEditorEvent): SnapshotRefreshHints;
268
+ getFieldSnapshot(): FieldSnapshot;
269
+ updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult;
270
+ updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult;
271
+ getSessionState(): EditorSessionState;
272
+ getPersistedSnapshot(): PersistedEditorSnapshot;
273
+ getCompatibilityReport(): CompatibilityReport;
274
+ getWarnings(): EditorWarning[];
275
+ exportDocx(options?: ExportDocxOptions): Promise<ExportResult>;
276
+ getSuggestionsSnapshot(): SuggestionsSnapshot;
277
+ setWorkflowOverlay(overlay: WorkflowOverlay): void;
278
+ clearWorkflowOverlay(): void;
279
+ getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
280
+ getInteractionGuardSnapshot(): InteractionGuardSnapshot;
281
+ getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
282
+ setWorkflowMetadataDefinitions(definitions: WorkflowMetadataDefinition[]): void;
283
+ clearWorkflowMetadataDefinitions(): void;
284
+ setWorkflowMetadataEntries(entries: WorkflowMetadataEntry[]): void;
285
+ clearWorkflowMetadataEntries(): void;
286
+ getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
287
+ setHostAnnotationOverlay(overlay: HostAnnotationOverlay): void;
288
+ clearHostAnnotationOverlay(): void;
289
+ getHostAnnotationSnapshot(): HostAnnotationSnapshot;
290
+ getWorkflowCandidateRanges(options?: WorkflowCandidateRangeOptions): WorkflowCandidateRange[];
291
+ replaceWorkflowMarkupText(markupId: string, text: string): void;
292
+ getDocumentTextStream(): StoryTextStreamSnapshot[];
293
+ getStoryTextStream(target: EditorStoryTarget): StoryTextStreamSnapshot | null;
294
+ getDocumentChunks(): DocumentChunkSnapshot[];
295
+ getWorkflowChunks(): DocumentChunkSnapshot[];
296
+ getReviewWorkSnapshot(): ReviewWorkSnapshot;
297
+ getRuntimeContextAnalytics(
298
+ query?: RuntimeContextAnalyticsQuery,
299
+ ): RuntimeContextAnalyticsSnapshot | null;
300
+ }
301
+
302
+ export interface CreateDocumentRuntimeOptions {
303
+ documentId: string;
304
+ initialSessionState?: EditorSessionState;
305
+ initialSnapshot?: PersistedEditorSnapshot;
306
+ initialCanonicalDocument?: CanonicalDocumentEnvelope;
307
+ sourceLabel?: string;
308
+ sourceKind?: RuntimeReadySource;
309
+ readOnly?: boolean;
310
+ editorBuild?: string;
311
+ defaultAuthorId?: string;
312
+ fatalError?: EditorError;
313
+ clock?: () => string;
314
+ exportDocx?: (
315
+ sessionState: EditorSessionState,
316
+ options?: ExportDocxOptions,
317
+ ) => Promise<ExportResult>;
318
+ onEvent?: (event: DocumentRuntimeEvent) => void;
319
+ onWarning?: (warning: EditorWarning) => void;
320
+ onError?: (error: EditorError) => void;
321
+ initialViewState?: Partial<ViewState>;
322
+ protectionSnapshot?: ProtectionSnapshot;
323
+ }
324
+
325
+ interface HistoryState {
326
+ past: EditorState[];
327
+ future: EditorState[];
328
+ }
329
+
330
+ export function createDocumentRuntime(
331
+ options: CreateDocumentRuntimeOptions,
332
+ ): DocumentRuntime {
333
+ const clock = options.clock ?? (() => new Date().toISOString());
334
+ const editorBuild = options.editorBuild ?? "dev";
335
+ let defaultAuthorId = options.defaultAuthorId;
336
+ const sessionId = createSessionId(options.documentId, clock());
337
+ const listeners = new Set<() => void>();
338
+ const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
339
+ const history: HistoryState = {
340
+ past: [],
341
+ future: [],
342
+ };
343
+
344
+ let activeStory: EditorStoryTarget = MAIN_STORY_TARGET;
345
+ const storySelections = new Map<string, EditorState["selection"]>();
346
+ let viewState: ViewState = createViewState(options.initialViewState);
347
+ let protectionSnapshot: ProtectionSnapshot =
348
+ options.protectionSnapshot ??
349
+ options.initialSessionState?.protectionSnapshot ??
350
+ options.initialSnapshot?.protectionSnapshot ?? {
351
+ hasDocumentProtection: false,
352
+ enforcementActive: false,
353
+ ranges: [],
354
+ enforcedRangeCount: 0,
355
+ preservedRangeCount: 0,
356
+ };
357
+ let workflowOverlay: WorkflowOverlay | null = null;
358
+ let workflowMetadataDefinitions: WorkflowMetadataDefinition[] =
359
+ options.initialSessionState?.workflowMetadata?.definitions
360
+ ?? options.initialSnapshot?.workflowMetadata?.definitions
361
+ ?? [];
362
+ let workflowMetadataEntries: WorkflowMetadataEntry[] =
363
+ options.initialSessionState?.workflowMetadata?.entries
364
+ ?? options.initialSnapshot?.workflowMetadata?.entries
365
+ ?? [];
366
+ let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
367
+ const initialPersistedSnapshot = options.initialSessionState
368
+ ? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
369
+ savedAt: options.initialSessionState.updatedAt,
370
+ })
371
+ : options.initialSnapshot;
372
+
373
+ let state = createEditorState({
374
+ documentId: options.documentId,
375
+ sessionId,
376
+ sourceLabel: options.sourceLabel,
377
+ readOnly: options.readOnly,
378
+ persistedSnapshot: initialPersistedSnapshot as never,
379
+ canonicalDocument: options.initialCanonicalDocument,
380
+ fatalError: options.fatalError as never,
381
+ });
382
+ storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
383
+ let cachedSurface:
384
+ | {
385
+ revisionToken: string;
386
+ activeStoryKey: string;
387
+ snapshot: RuntimeRenderSnapshot["surface"];
388
+ }
389
+ | undefined;
390
+ let cachedCompatibility:
391
+ | {
392
+ revisionToken: string;
393
+ warnings: EditorState["warnings"];
394
+ fatalError: EditorState["fatalError"];
395
+ report: RuntimeRenderSnapshot["compatibility"];
396
+ }
397
+ | undefined;
398
+ let cachedComments:
399
+ | {
400
+ comments: CanonicalDocumentEnvelope["review"]["comments"];
401
+ activeCommentId: EditorState["runtime"]["activeCommentId"];
402
+ snapshot: CommentSidebarSnapshot;
403
+ }
404
+ | undefined;
405
+ let cachedTrackedChanges:
406
+ | {
407
+ revisions: CanonicalDocumentEnvelope["review"]["revisions"];
408
+ revisionToken: string;
409
+ snapshot: TrackedChangesSnapshot;
410
+ }
411
+ | undefined;
412
+ let cachedSuggestions:
413
+ | {
414
+ trackedChanges: TrackedChangesSnapshot;
415
+ snapshot: SuggestionsSnapshot;
416
+ }
417
+ | undefined;
418
+ let cachedPageLayout:
419
+ | {
420
+ revisionToken: string;
421
+ activeStoryKey: string;
422
+ activeSectionIndex: number | string;
423
+ snapshot: PageLayoutSnapshot | null;
424
+ }
425
+ | undefined;
426
+ let cachedNavigation:
427
+ | {
428
+ revisionToken: string;
429
+ activeStoryKey: string;
430
+ selectionHead: number;
431
+ snapshot: DocumentNavigationSnapshot;
432
+ }
433
+ | undefined;
434
+ let cachedViewStateSnapshot:
435
+ | {
436
+ revisionToken: string;
437
+ activeStoryKey: string;
438
+ selection: EditorState["selection"];
439
+ viewStateRef: ViewState;
440
+ pageLayout: PageLayoutSnapshot | null | undefined;
441
+ snapshot: EditorViewStateSnapshot;
442
+ }
443
+ | undefined;
444
+ let cachedInteractionGuardSnapshot:
445
+ | {
446
+ revisionToken: string;
447
+ activeStoryKey: string;
448
+ selection: EditorState["selection"];
449
+ readOnly: boolean;
450
+ documentMode: DocumentMode;
451
+ protectionSnapshot: ProtectionSnapshot;
452
+ workflowOverlay: WorkflowOverlay | null;
453
+ snapshot: InteractionGuardSnapshot;
454
+ }
455
+ | undefined;
456
+ let cachedWorkflowScopeSnapshot:
457
+ | {
458
+ workflowOverlay: WorkflowOverlay;
459
+ interactionGuardSnapshot: InteractionGuardSnapshot;
460
+ snapshot: WorkflowScopeSnapshot;
461
+ }
462
+ | undefined;
463
+ let cachedWorkflowMarkupSnapshot:
464
+ | {
465
+ revisionToken: string;
466
+ activeStoryKey: string;
467
+ protectionSnapshot: ProtectionSnapshot;
468
+ preservation: CanonicalDocumentEnvelope["preservation"];
469
+ workflowOverlay: WorkflowOverlay | null;
470
+ workflowMetadataDefinitions: WorkflowMetadataDefinition[];
471
+ workflowMetadataEntries: WorkflowMetadataEntry[];
472
+ snapshot: WorkflowMarkupSnapshot;
473
+ }
474
+ | undefined;
475
+ const cachedContextAnalyticsSnapshots = new Map<
476
+ string,
477
+ {
478
+ revisionToken: string;
479
+ activeStoryKey: string;
480
+ selection: EditorState["selection"];
481
+ readOnly: boolean;
482
+ documentMode: DocumentMode;
483
+ workflowOverlay: WorkflowOverlay | null;
484
+ protectionSnapshot: ProtectionSnapshot;
485
+ warnings: EditorState["warnings"];
486
+ fatalError: EditorState["fatalError"];
487
+ snapshot: RuntimeContextAnalyticsSnapshot | null;
488
+ }
489
+ >();
490
+ let lastEmittedContextAnalyticsSnapshots:
491
+ | Map<string, RuntimeContextAnalyticsSnapshot | null>
492
+ | undefined;
493
+
494
+ function getCachedSurface(
495
+ document: CanonicalDocumentEnvelope,
496
+ nextActiveStory: EditorStoryTarget,
497
+ ): RuntimeRenderSnapshot["surface"] {
498
+ const activeStoryKey = storyTargetKey(nextActiveStory);
499
+ if (
500
+ cachedSurface &&
501
+ cachedSurface.revisionToken === state.revisionToken &&
502
+ cachedSurface.activeStoryKey === activeStoryKey
503
+ ) {
504
+ return cachedSurface.snapshot;
505
+ }
506
+
507
+ const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory);
508
+ recordPerfSample("snapshot.surface");
509
+ incrementInvalidationCounter("runtime.snapshot.surfaceMisses");
510
+ cachedSurface = {
511
+ revisionToken: state.revisionToken,
512
+ activeStoryKey,
513
+ snapshot,
514
+ };
515
+ return snapshot;
516
+ }
517
+
518
+ function getCachedCompatibilityReport(
519
+ nextState: EditorState,
520
+ ): RuntimeRenderSnapshot["compatibility"] {
521
+ if (
522
+ cachedCompatibility &&
523
+ cachedCompatibility.revisionToken === nextState.revisionToken &&
524
+ cachedCompatibility.warnings === nextState.warnings &&
525
+ cachedCompatibility.fatalError === nextState.fatalError
526
+ ) {
527
+ return cachedCompatibility.report;
528
+ }
529
+
530
+ const derived = createDerivedCompatibility(nextState);
531
+ recordPerfSample("snapshot.compatibility");
532
+ incrementInvalidationCounter("runtime.snapshot.compatibilityMisses");
533
+ const report = {
534
+ blockExport: derived.blockExport,
535
+ blockExportReasons: listBlockExportReasons(derived),
536
+ warningCount: derived.warnings.length,
537
+ errorCount: derived.errors.length,
538
+ featureEntries: derived.featureEntries.map((entry) =>
539
+ toPublicCompatibilityFeatureEntry(entry),
540
+ ),
541
+ };
542
+ cachedCompatibility = {
543
+ revisionToken: nextState.revisionToken,
544
+ warnings: nextState.warnings,
545
+ fatalError: nextState.fatalError,
546
+ report,
547
+ };
548
+ return report;
549
+ }
550
+
551
+ function getCachedCommentSidebarSnapshot(nextState: EditorState): CommentSidebarSnapshot {
552
+ if (
553
+ cachedComments &&
554
+ cachedComments.comments === nextState.document.review.comments &&
555
+ cachedComments.activeCommentId === nextState.runtime.activeCommentId
556
+ ) {
557
+ return cachedComments.snapshot;
558
+ }
559
+
560
+ const snapshot = toPublicCommentSidebarSnapshot(nextState);
561
+ cachedComments = {
562
+ comments: nextState.document.review.comments,
563
+ activeCommentId: nextState.runtime.activeCommentId,
564
+ snapshot,
565
+ };
566
+ return snapshot;
567
+ }
568
+
569
+ function getCachedTrackedChangesSnapshot(
570
+ nextState: EditorState,
571
+ _surface: RuntimeRenderSnapshot["surface"],
572
+ ): TrackedChangesSnapshot {
573
+ if (
574
+ cachedTrackedChanges &&
575
+ cachedTrackedChanges.revisions === nextState.document.review.revisions &&
576
+ cachedTrackedChanges.revisionToken === nextState.revisionToken
577
+ ) {
578
+ return cachedTrackedChanges.snapshot;
579
+ }
580
+
581
+ const snapshot = toPublicTrackedChangesSnapshot(nextState);
582
+ cachedTrackedChanges = {
583
+ revisions: nextState.document.review.revisions,
584
+ revisionToken: nextState.revisionToken,
585
+ snapshot,
586
+ };
587
+ return snapshot;
588
+ }
589
+
590
+ function getCachedSuggestionsSnapshot(nextState: EditorState): SuggestionsSnapshot {
591
+ const trackedChanges = getCachedTrackedChangesSnapshot(nextState, undefined);
592
+ if (
593
+ cachedSuggestions &&
594
+ cachedSuggestions.trackedChanges === trackedChanges
595
+ ) {
596
+ return cachedSuggestions.snapshot;
597
+ }
598
+
599
+ const snapshot = createSuggestionsSnapshot(trackedChanges);
600
+ cachedSuggestions = {
601
+ trackedChanges,
602
+ snapshot,
603
+ };
604
+ return snapshot;
605
+ }
606
+
607
+ function findSuggestionByChangeId(
608
+ snapshot: SuggestionsSnapshot,
609
+ changeId: string,
610
+ ) {
611
+ return snapshot.suggestions.find((suggestion) => suggestion.changeIds.includes(changeId));
612
+ }
613
+
614
+ function getCachedDocumentNavigationSnapshot(
615
+ nextState: EditorState,
616
+ nextActiveStory: EditorStoryTarget,
617
+ ): DocumentNavigationSnapshot {
618
+ const activeStoryKey = storyTargetKey(nextActiveStory);
619
+ if (
620
+ cachedNavigation &&
621
+ cachedNavigation.revisionToken === nextState.revisionToken &&
622
+ cachedNavigation.activeStoryKey === activeStoryKey
623
+ ) {
624
+ if (cachedNavigation.selectionHead === nextState.selection.head) {
625
+ return cachedNavigation.snapshot;
626
+ }
627
+
628
+ const snapshot = createDocumentNavigationSnapshot(
629
+ nextState.document,
630
+ nextState.selection.head,
631
+ nextActiveStory,
632
+ );
633
+ if (
634
+ snapshot.activePageIndex === cachedNavigation.snapshot.activePageIndex &&
635
+ snapshot.activeSectionIndex === cachedNavigation.snapshot.activeSectionIndex
636
+ ) {
637
+ cachedNavigation = {
638
+ revisionToken: nextState.revisionToken,
639
+ activeStoryKey,
640
+ selectionHead: nextState.selection.head,
641
+ snapshot: cachedNavigation.snapshot,
642
+ };
643
+ return cachedNavigation.snapshot;
644
+ }
645
+ cachedNavigation = {
646
+ revisionToken: nextState.revisionToken,
647
+ activeStoryKey,
648
+ selectionHead: nextState.selection.head,
649
+ snapshot,
650
+ };
651
+ return snapshot;
652
+ }
653
+
654
+ const snapshot = createDocumentNavigationSnapshot(
655
+ nextState.document,
656
+ nextState.selection.head,
657
+ nextActiveStory,
658
+ );
659
+ recordPerfSample("snapshot.navigation");
660
+ incrementInvalidationCounter("runtime.snapshot.navigationMisses");
661
+ cachedNavigation = {
662
+ revisionToken: nextState.revisionToken,
663
+ activeStoryKey,
664
+ selectionHead: nextState.selection.head,
665
+ snapshot,
666
+ };
667
+ return snapshot;
668
+ }
669
+
670
+ function resolvePageLayoutActiveSectionIndex(
671
+ nextState: EditorState,
672
+ nextActiveStory: EditorStoryTarget,
673
+ ): number | string {
674
+ if (nextActiveStory.kind === "main") {
675
+ return getCachedDocumentNavigationSnapshot(nextState, nextActiveStory).activeSectionIndex;
676
+ }
677
+
678
+ if ("sectionIndex" in nextActiveStory && typeof nextActiveStory.sectionIndex === "number") {
679
+ return nextActiveStory.sectionIndex;
680
+ }
681
+
682
+ return storyTargetKey(nextActiveStory);
683
+ }
684
+
685
+ function getCachedPageLayoutSnapshot(
686
+ nextState: EditorState,
687
+ nextActiveStory: EditorStoryTarget,
688
+ ): PageLayoutSnapshot | null {
689
+ const activeStoryKey = storyTargetKey(nextActiveStory);
690
+ const activeSectionIndex = resolvePageLayoutActiveSectionIndex(
691
+ nextState,
692
+ nextActiveStory,
693
+ );
694
+ if (
695
+ cachedPageLayout &&
696
+ cachedPageLayout.revisionToken === nextState.revisionToken &&
697
+ cachedPageLayout.activeStoryKey === activeStoryKey &&
698
+ cachedPageLayout.activeSectionIndex === activeSectionIndex
699
+ ) {
700
+ return cachedPageLayout.snapshot;
701
+ }
702
+
703
+ const snapshot = derivePageLayoutSnapshot(nextState, nextActiveStory, storySelections);
704
+ cachedPageLayout = {
705
+ revisionToken: nextState.revisionToken,
706
+ activeStoryKey,
707
+ activeSectionIndex,
708
+ snapshot,
709
+ };
710
+ return snapshot;
711
+ }
712
+
713
+ function evaluateWorkflowBlockedReasons(
714
+ selection: EditorState["selection"],
715
+ commandType?: string,
716
+ ): WorkflowBlockedCommandReason[] {
717
+ const reasons: WorkflowBlockedCommandReason[] = [];
718
+ const selectionBounds = {
719
+ from: Math.min(selection.anchor, selection.head),
720
+ to: Math.max(selection.anchor, selection.head),
721
+ };
722
+ const selectionRange = expandSelectionRange(selectionBounds);
723
+ const opaqueReason = deriveOpaqueWorkflowBlockedReason(selectionRange);
724
+
725
+ if (opaqueReason) {
726
+ reasons.push(opaqueReason);
727
+ }
728
+
729
+ if (state.readOnly) {
730
+ reasons.push({
731
+ code: "document_read_only",
732
+ message: "Document is in read-only mode.",
733
+ });
734
+ }
735
+
736
+ if (viewState.documentMode === "viewing") {
737
+ reasons.push({
738
+ code: "document_viewing_mode",
739
+ message: "Document is in viewing mode.",
740
+ });
741
+ }
742
+
743
+ if (
744
+ isBlockedByProtection(protectionSnapshot, selection)
745
+ ) {
746
+ reasons.push({
747
+ code: "protected_range",
748
+ message: "Selection falls within a protected range.",
749
+ });
750
+ }
751
+
752
+ const effectiveDocumentMode = getEffectiveDocumentMode(selection);
753
+
754
+ if (effectiveDocumentMode === "suggesting" && commandType) {
755
+ if (SUGGESTING_UNSUPPORTED_COMMANDS.has(commandType)) {
756
+ reasons.push({
757
+ code: "suggesting_unsupported",
758
+ message: `"${commandType}" is not supported in suggesting mode.`,
759
+ });
760
+ }
761
+ }
762
+
763
+ if (workflowOverlay) {
764
+ const matchingScope = getMatchingWorkflowScope(selection);
765
+
766
+ if (!matchingScope && workflowOverlay.scopes.length > 0) {
767
+ reasons.push({
768
+ code: "outside_workflow_scope",
769
+ message: "Selection is outside any active workflow scope.",
770
+ });
771
+ } else if (matchingScope) {
772
+ if (matchingScope.mode === "comment") {
773
+ const isCommentCommand =
774
+ commandType?.startsWith("comment.") ?? false;
775
+ if (!isCommentCommand) {
776
+ reasons.push({
777
+ code: "workflow_comment_only",
778
+ message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
779
+ scopeId: matchingScope.scopeId,
780
+ workItemId: matchingScope.workItemId,
781
+ });
782
+ }
783
+ } else if (matchingScope.mode === "view") {
784
+ reasons.push({
785
+ code: "workflow_view_only",
786
+ message: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
787
+ scopeId: matchingScope.scopeId,
788
+ workItemId: matchingScope.workItemId,
789
+ });
790
+ }
791
+ }
792
+ }
793
+
794
+ return reasons;
795
+ }
796
+
797
+ function getMatchingWorkflowScope(
798
+ selection: EditorState["selection"],
799
+ ): WorkflowOverlay["scopes"][number] | null {
800
+ if (!workflowOverlay) {
801
+ return null;
802
+ }
803
+
804
+ const selectionBounds = {
805
+ from: Math.min(selection.anchor, selection.head),
806
+ to: Math.max(selection.anchor, selection.head),
807
+ };
808
+ const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
809
+ return activeScopes.find((scope) => {
810
+ if (scope.anchor.kind === "detached") return false;
811
+ const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
812
+ const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
813
+ return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
814
+ }) ?? null;
815
+ }
816
+
817
+ function getEffectiveDocumentMode(
818
+ selection: EditorState["selection"],
819
+ ): DocumentMode {
820
+ if (viewState.documentMode === "viewing") {
821
+ return "viewing";
822
+ }
823
+ const matchingScope = getMatchingWorkflowScope(selection);
824
+ if (matchingScope?.mode === "suggest") {
825
+ return "suggesting";
826
+ }
827
+ return viewState.documentMode;
828
+ }
829
+
830
+ function expandSelectionRange(
831
+ range: { from: number; to: number },
832
+ ): { from: number; to: number } {
833
+ return {
834
+ from: range.from,
835
+ to: range.to > range.from ? range.to : range.from + 1,
836
+ };
837
+ }
838
+
839
+ function deriveOpaqueWorkflowBlockedReason(
840
+ range: { from: number; to: number },
841
+ ): WorkflowBlockedCommandReason | null {
842
+ const surfaceReason = deriveSurfaceOpaqueWorkflowBlockedReason(range);
843
+ if (surfaceReason) {
844
+ return surfaceReason;
845
+ }
846
+
847
+ const targetPartPath = getStoryTargetOpaquePartPath(activeStory);
848
+ if (!targetPartPath) {
849
+ return null;
850
+ }
851
+ const fragments = findOpaqueFragmentsIntersectingRange(
852
+ state.document.preservation,
853
+ range,
854
+ ).filter((fragment) => fragment.packagePartName === targetPartPath);
855
+
856
+ if (fragments.length === 0) {
857
+ return null;
858
+ }
859
+
860
+ const blockedImportFragment =
861
+ fragments.find((fragment) =>
862
+ isBlockedImportFeatureKey(describeOpaqueFragment(fragment).featureKey),
863
+ ) ?? null;
864
+ const fragment = blockedImportFragment ?? fragments[0]!;
865
+ const descriptor = describeOpaqueFragment(fragment);
866
+ const isBlockedImport = blockedImportFragment !== null;
867
+
868
+ return {
869
+ code: isBlockedImport ? "workflow_blocked_import" : "workflow_preserve_only",
870
+ message: isBlockedImport
871
+ ? `${descriptor.label} remains a blocked import and cannot be edited.`
872
+ : `${descriptor.label} remains preserve-only and cannot be edited.`,
873
+ anchor: toPublicAnchorProjection(
874
+ createRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to, {
875
+ start: -1,
876
+ end: 1,
877
+ }),
878
+ ),
879
+ storyTarget: activeStory,
880
+ };
881
+ }
882
+
883
+ function deriveSurfaceOpaqueWorkflowBlockedReason(
884
+ range: { from: number; to: number },
885
+ ): WorkflowBlockedCommandReason | null {
886
+ const blocks = getActiveStorySurfaceBlocks();
887
+ if (!blocks) {
888
+ return null;
889
+ }
890
+
891
+ const opaqueTarget = findSurfaceOpaqueTargetIntersectingRange(blocks, range);
892
+ if (!opaqueTarget) {
893
+ return null;
894
+ }
895
+
896
+ const code = opaqueTarget.blockedReasonCode ?? "workflow_preserve_only";
897
+ return {
898
+ code,
899
+ message:
900
+ code === "workflow_blocked_import"
901
+ ? `${opaqueTarget.label} remains a blocked import and cannot be edited.`
902
+ : `${opaqueTarget.label} remains preserve-only and cannot be edited.`,
903
+ anchor: toPublicAnchorProjection(
904
+ createRangeAnchor(opaqueTarget.from, opaqueTarget.to, {
905
+ start: -1,
906
+ end: 1,
907
+ }),
908
+ ),
909
+ storyTarget: activeStory,
910
+ };
911
+ }
912
+
913
+ function getActiveStorySurfaceBlocks(): readonly SurfaceBlockSnapshot[] | null {
914
+ const surface = cachedRenderSnapshot.surface;
915
+ if (!surface) {
916
+ return null;
917
+ }
918
+
919
+ if (activeStory.kind === "main") {
920
+ return surface.blocks;
921
+ }
922
+
923
+ const activeStoryKey = storyTargetKey(activeStory);
924
+ return (
925
+ surface.secondaryStories.find((story) => storyTargetKey(story.target) === activeStoryKey)?.blocks ??
926
+ null
927
+ );
928
+ }
929
+
930
+ function findSurfaceOpaqueTargetIntersectingRange(
931
+ blocks: readonly SurfaceBlockSnapshot[],
932
+ range: { from: number; to: number },
933
+ ): {
934
+ from: number;
935
+ to: number;
936
+ label: string;
937
+ blockedReasonCode: "workflow_preserve_only" | "workflow_blocked_import";
938
+ } | null {
939
+ for (const block of blocks) {
940
+ if (block.kind === "paragraph") {
941
+ for (const segment of block.segments) {
942
+ const match = matchOpaqueInlineSegment(segment, range);
943
+ if (match) {
944
+ return match;
945
+ }
946
+ }
947
+ continue;
948
+ }
949
+
950
+ if (block.kind === "table") {
951
+ for (const row of block.rows) {
952
+ for (const cell of row.cells) {
953
+ const match = findSurfaceOpaqueTargetIntersectingRange(cell.content, range);
954
+ if (match) {
955
+ return match;
956
+ }
957
+ }
958
+ }
959
+ continue;
960
+ }
961
+
962
+ if (block.kind === "sdt_block") {
963
+ const match = findSurfaceOpaqueTargetIntersectingRange(block.children, range);
964
+ if (match) {
965
+ return match;
966
+ }
967
+ continue;
968
+ }
969
+
970
+ if (block.kind !== "opaque_block") {
971
+ continue;
972
+ }
973
+
974
+ const blockRange = {
975
+ from: block.from,
976
+ to: block.to > block.from ? block.to : block.from + 1,
977
+ };
978
+ if (rangesIntersect(blockRange, range)) {
979
+ if (!block.blockedReasonCode) {
980
+ continue;
981
+ }
982
+ return {
983
+ from: blockRange.from,
984
+ to: blockRange.to,
985
+ label: block.label,
986
+ blockedReasonCode: block.blockedReasonCode,
987
+ };
988
+ }
989
+ }
990
+
991
+ return null;
992
+ }
993
+
994
+ function matchOpaqueInlineSegment(
995
+ segment: SurfaceInlineSegment,
996
+ range: { from: number; to: number },
997
+ ): {
998
+ from: number;
999
+ to: number;
1000
+ label: string;
1001
+ blockedReasonCode: "workflow_preserve_only" | "workflow_blocked_import";
1002
+ } | null {
1003
+ if (segment.kind !== "opaque_inline") {
1004
+ return null;
1005
+ }
1006
+ if (!segment.blockedReasonCode) {
1007
+ return null;
1008
+ }
1009
+
1010
+ const segmentRange = {
1011
+ from: segment.from,
1012
+ to: segment.to > segment.from ? segment.to : segment.from + 1,
1013
+ };
1014
+ if (!rangesIntersect(segmentRange, range)) {
1015
+ return null;
1016
+ }
1017
+
1018
+ return {
1019
+ from: segmentRange.from,
1020
+ to: segmentRange.to,
1021
+ label: segment.label,
1022
+ blockedReasonCode: segment.blockedReasonCode,
1023
+ };
1024
+ }
1025
+
1026
+ function getStoryTargetOpaquePartPath(storyTarget: EditorStoryTarget): string | null {
1027
+ if (storyTarget.kind === "main") {
1028
+ return "/word/document.xml";
1029
+ }
1030
+ if (storyTarget.kind === "header") {
1031
+ return state.document.subParts?.headers.find(
1032
+ (header) =>
1033
+ header.relationshipId === storyTarget.relationshipId
1034
+ && header.variant === storyTarget.variant
1035
+ && header.sectionIndex === storyTarget.sectionIndex,
1036
+ )?.partPath ?? null;
1037
+ }
1038
+ if (storyTarget.kind === "footer") {
1039
+ return state.document.subParts?.footers.find(
1040
+ (footer) =>
1041
+ footer.relationshipId === storyTarget.relationshipId
1042
+ && footer.variant === storyTarget.variant
1043
+ && footer.sectionIndex === storyTarget.sectionIndex,
1044
+ )?.partPath ?? null;
1045
+ }
1046
+ if (storyTarget.kind === "footnote") {
1047
+ return "/word/footnotes.xml";
1048
+ }
1049
+ if (storyTarget.kind === "endnote") {
1050
+ return "/word/endnotes.xml";
1051
+ }
1052
+ return null;
1053
+ }
1054
+
1055
+ function rangesIntersect(
1056
+ left: { from: number; to: number },
1057
+ right: { from: number; to: number },
1058
+ ): boolean {
1059
+ return left.from < right.to && right.from < left.to;
1060
+ }
1061
+
1062
+ function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
1063
+ if (!workflowOverlay) return null;
1064
+ const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
1065
+ const activeItem = workflowOverlay.activeWorkItemId
1066
+ ? workflowOverlay.workItems?.find(
1067
+ (item) => item.workItemId === workflowOverlay!.activeWorkItemId,
1068
+ )
1069
+ : undefined;
1070
+ return {
1071
+ overlayPresent: true,
1072
+ activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
1073
+ activeWorkItem: activeItem,
1074
+ scopes: workflowOverlay.scopes,
1075
+ candidates: workflowOverlay.candidates ?? [],
1076
+ blockedReasons,
1077
+ };
1078
+ }
1079
+
1080
+ function deriveHostAnnotationSnapshot(): HostAnnotationSnapshot {
1081
+ return {
1082
+ totalCount: hostAnnotationOverlay?.annotations.length ?? 0,
1083
+ annotations: structuredClone(hostAnnotationOverlay?.annotations ?? []),
1084
+ };
1085
+ }
1086
+
1087
+ function deriveWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot {
1088
+ return {
1089
+ definitions: structuredClone(workflowMetadataDefinitions),
1090
+ entries: structuredClone(workflowMetadataEntries),
1091
+ };
1092
+ }
1093
+
1094
+ function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
1095
+ const activeWorkItemId = overlay.activeWorkItemId ?? null;
1096
+ const activeWorkItemScopeIds =
1097
+ activeWorkItemId === null
1098
+ ? null
1099
+ : new Set(
1100
+ overlay.workItems?.find((item) => item.workItemId === activeWorkItemId)?.scopeIds ?? [],
1101
+ );
1102
+
1103
+ return overlay.scopes.filter((scope) => {
1104
+ const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
1105
+ if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
1106
+ return false;
1107
+ }
1108
+
1109
+ if (activeWorkItemId === null) {
1110
+ return true;
1111
+ }
1112
+
1113
+ return (
1114
+ scope.workItemId === activeWorkItemId ||
1115
+ activeWorkItemScopeIds?.has(scope.scopeId) === true
1116
+ );
1117
+ });
1118
+ }
1119
+
1120
+ function getCachedInteractionGuardSnapshot(): InteractionGuardSnapshot {
1121
+ const activeStoryKey = storyTargetKey(activeStory);
1122
+ if (
1123
+ cachedInteractionGuardSnapshot &&
1124
+ cachedInteractionGuardSnapshot.revisionToken === state.revisionToken &&
1125
+ cachedInteractionGuardSnapshot.activeStoryKey === activeStoryKey &&
1126
+ cachedInteractionGuardSnapshot.selection === state.selection &&
1127
+ cachedInteractionGuardSnapshot.readOnly === state.readOnly &&
1128
+ cachedInteractionGuardSnapshot.documentMode === viewState.documentMode &&
1129
+ cachedInteractionGuardSnapshot.protectionSnapshot === protectionSnapshot &&
1130
+ cachedInteractionGuardSnapshot.workflowOverlay === workflowOverlay
1131
+ ) {
1132
+ return cachedInteractionGuardSnapshot.snapshot;
1133
+ }
1134
+
1135
+ const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
1136
+ const matchingScope = getMatchingWorkflowScope(state.selection);
1137
+ const primaryBlockedReason = blockedReasons[0];
1138
+ const effectiveMode = primaryBlockedReason
1139
+ ? (
1140
+ primaryBlockedReason.code === "workflow_comment_only"
1141
+ ? "comment"
1142
+ : primaryBlockedReason.code === "workflow_view_only"
1143
+ ? "view"
1144
+ : "blocked"
1145
+ )
1146
+ : getEffectiveDocumentMode(state.selection) === "suggesting"
1147
+ ? "suggest"
1148
+ : matchingScope?.mode ?? "edit";
1149
+ const snapshot: InteractionGuardSnapshot = {
1150
+ effectiveMode,
1151
+ ...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
1152
+ ...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
1153
+ targetAccess:
1154
+ effectiveMode === "edit"
1155
+ ? "direct-edit"
1156
+ : effectiveMode === "suggest"
1157
+ ? "suggest"
1158
+ : effectiveMode === "comment"
1159
+ ? "comment-only"
1160
+ : effectiveMode === "view"
1161
+ ? "view-only"
1162
+ : "blocked",
1163
+ commandCapabilities: [
1164
+ {
1165
+ family: "text",
1166
+ supported: evaluateWorkflowBlockedReasons(state.selection, "text.insert").length === 0,
1167
+ blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "text.insert"),
1168
+ },
1169
+ {
1170
+ family: "formatting",
1171
+ supported: evaluateWorkflowBlockedReasons(state.selection, "toggleBold").length === 0,
1172
+ blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "toggleBold"),
1173
+ },
1174
+ {
1175
+ family: "structure",
1176
+ supported: evaluateWorkflowBlockedReasons(state.selection, "insertTable").length === 0,
1177
+ blockedReasons: evaluateWorkflowBlockedReasons(state.selection, "insertTable"),
1178
+ },
1179
+ ],
1180
+ ...(primaryBlockedReason ? { disabledReason: primaryBlockedReason.message } : {}),
1181
+ blockedReasons,
1182
+ };
1183
+ cachedInteractionGuardSnapshot = {
1184
+ revisionToken: state.revisionToken,
1185
+ activeStoryKey,
1186
+ selection: state.selection,
1187
+ readOnly: state.readOnly,
1188
+ documentMode: viewState.documentMode,
1189
+ protectionSnapshot,
1190
+ workflowOverlay,
1191
+ snapshot,
1192
+ };
1193
+ return snapshot;
1194
+ }
1195
+
1196
+ function getCachedWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
1197
+ if (!workflowOverlay) {
1198
+ return null;
1199
+ }
1200
+
1201
+ const interactionGuardSnapshot = getCachedInteractionGuardSnapshot();
1202
+ if (
1203
+ cachedWorkflowScopeSnapshot &&
1204
+ cachedWorkflowScopeSnapshot.workflowOverlay === workflowOverlay &&
1205
+ cachedWorkflowScopeSnapshot.interactionGuardSnapshot === interactionGuardSnapshot
1206
+ ) {
1207
+ return cachedWorkflowScopeSnapshot.snapshot;
1208
+ }
1209
+
1210
+ const snapshot = deriveWorkflowScopeSnapshot()!;
1211
+ cachedWorkflowScopeSnapshot = {
1212
+ workflowOverlay,
1213
+ interactionGuardSnapshot,
1214
+ snapshot,
1215
+ };
1216
+ return snapshot;
1217
+ }
1218
+
1219
+ function getCachedWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot {
1220
+ const activeStoryKey = storyTargetKey(activeStory);
1221
+ if (
1222
+ cachedWorkflowMarkupSnapshot &&
1223
+ cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
1224
+ cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
1225
+ cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
1226
+ cachedWorkflowMarkupSnapshot.preservation === state.document.preservation &&
1227
+ cachedWorkflowMarkupSnapshot.workflowOverlay === workflowOverlay &&
1228
+ cachedWorkflowMarkupSnapshot.workflowMetadataDefinitions === workflowMetadataDefinitions &&
1229
+ cachedWorkflowMarkupSnapshot.workflowMetadataEntries === workflowMetadataEntries
1230
+ ) {
1231
+ return cachedWorkflowMarkupSnapshot.snapshot;
1232
+ }
1233
+
1234
+ const snapshot = collectWorkflowMarkupSnapshot({
1235
+ renderSnapshot: cachedRenderSnapshot,
1236
+ fieldSnapshot: buildFieldSnapshot(state.document),
1237
+ protectionSnapshot,
1238
+ preservation: state.document.preservation,
1239
+ workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
1240
+ });
1241
+ cachedWorkflowMarkupSnapshot = {
1242
+ revisionToken: state.revisionToken,
1243
+ activeStoryKey,
1244
+ protectionSnapshot,
1245
+ preservation: state.document.preservation,
1246
+ workflowOverlay,
1247
+ workflowMetadataDefinitions,
1248
+ workflowMetadataEntries,
1249
+ snapshot,
1250
+ };
1251
+ return snapshot;
1252
+ }
1253
+
1254
+ function getCachedRuntimeContextAnalytics(
1255
+ query?: RuntimeContextAnalyticsQuery,
1256
+ ): RuntimeContextAnalyticsSnapshot | null {
1257
+ const activeStoryKey = storyTargetKey(activeStory);
1258
+ const queryKey = getRuntimeContextAnalyticsQueryKey(query);
1259
+ const cachedEntry = cachedContextAnalyticsSnapshots.get(queryKey);
1260
+ if (
1261
+ cachedEntry &&
1262
+ cachedEntry.revisionToken === state.revisionToken &&
1263
+ cachedEntry.activeStoryKey === activeStoryKey &&
1264
+ cachedEntry.selection === state.selection &&
1265
+ cachedEntry.readOnly === state.readOnly &&
1266
+ cachedEntry.documentMode === viewState.documentMode &&
1267
+ cachedEntry.workflowOverlay === workflowOverlay &&
1268
+ cachedEntry.protectionSnapshot === protectionSnapshot &&
1269
+ cachedEntry.warnings === state.warnings &&
1270
+ cachedEntry.fatalError === state.fatalError
1271
+ ) {
1272
+ return cachedEntry.snapshot;
1273
+ }
1274
+
1275
+ const snapshot = createRuntimeContextAnalyticsSnapshot({
1276
+ query,
1277
+ renderSnapshot: cachedRenderSnapshot,
1278
+ workflowOverlay,
1279
+ workflowScopeSnapshot: getCachedWorkflowScopeSnapshot(),
1280
+ interactionGuardSnapshot: getCachedInteractionGuardSnapshot(),
1281
+ workflowMarkupSnapshot: getCachedWorkflowMarkupSnapshot(),
1282
+ suggestionsSnapshot: getCachedSuggestionsSnapshot(state),
1283
+ reviewWorkSnapshot: createReviewWorkSnapshot({
1284
+ comments: cachedRenderSnapshot.comments,
1285
+ trackedChanges: cachedRenderSnapshot.trackedChanges,
1286
+ workflowMarkup: getCachedWorkflowMarkupSnapshot(),
1287
+ document: state.document,
1288
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
1289
+ }),
1290
+ warnings: state.warnings.map(toPublicWarning),
1291
+ compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
1292
+ });
1293
+ cachedContextAnalyticsSnapshots.set(queryKey, {
1294
+ revisionToken: state.revisionToken,
1295
+ activeStoryKey,
1296
+ selection: state.selection,
1297
+ readOnly: state.readOnly,
1298
+ documentMode: viewState.documentMode,
1299
+ workflowOverlay,
1300
+ protectionSnapshot,
1301
+ warnings: state.warnings,
1302
+ fatalError: state.fatalError,
1303
+ snapshot,
1304
+ });
1305
+ return snapshot;
1306
+ }
1307
+
1308
+ function getRuntimeContextAnalyticsQueryKey(query?: RuntimeContextAnalyticsQuery): string {
1309
+ return JSON.stringify({
1310
+ scopeKind: query?.scopeKind ?? "selection",
1311
+ scopeId: query?.scopeId ?? null,
1312
+ workItemId: query?.workItemId ?? null,
1313
+ });
1314
+ }
1315
+
1316
+ function getTrackedContextAnalyticsQueries(): Array<RuntimeContextAnalyticsQuery | undefined> {
1317
+ const queries: Array<RuntimeContextAnalyticsQuery | undefined> = [
1318
+ undefined,
1319
+ { scopeKind: "document" },
1320
+ ];
1321
+ const workflowScopeSnapshot = getCachedWorkflowScopeSnapshot();
1322
+ const seenScopeIds = new Set<string>();
1323
+ for (const scope of workflowScopeSnapshot?.scopes ?? []) {
1324
+ if (seenScopeIds.has(scope.scopeId)) {
1325
+ continue;
1326
+ }
1327
+ seenScopeIds.add(scope.scopeId);
1328
+ queries.push({
1329
+ scopeKind: "workflow_scope",
1330
+ scopeId: scope.scopeId,
1331
+ });
1332
+ }
1333
+ const seenWorkItemIds = new Set<string>();
1334
+ for (const workItem of workflowOverlay?.workItems ?? []) {
1335
+ if (seenWorkItemIds.has(workItem.workItemId)) {
1336
+ continue;
1337
+ }
1338
+ seenWorkItemIds.add(workItem.workItemId);
1339
+ queries.push({
1340
+ scopeKind: "work_item",
1341
+ workItemId: workItem.workItemId,
1342
+ });
1343
+ }
1344
+ const activeWorkItemId = workflowScopeSnapshot?.activeWorkItemId ?? null;
1345
+ if (activeWorkItemId && !seenWorkItemIds.has(activeWorkItemId)) {
1346
+ queries.push({
1347
+ scopeKind: "work_item",
1348
+ workItemId: activeWorkItemId,
1349
+ });
1350
+ }
1351
+ return queries;
1352
+ }
1353
+
1354
+ function collectTrackedContextAnalyticsSnapshots(): Map<string, RuntimeContextAnalyticsSnapshot | null> {
1355
+ const snapshots = new Map<string, RuntimeContextAnalyticsSnapshot | null>();
1356
+ for (const query of getTrackedContextAnalyticsQueries()) {
1357
+ snapshots.set(
1358
+ getRuntimeContextAnalyticsQueryKey(query),
1359
+ getCachedRuntimeContextAnalytics(query),
1360
+ );
1361
+ }
1362
+ return snapshots;
1363
+ }
1364
+
1365
+ function trackedContextAnalyticsSnapshotsEqual(
1366
+ left: Map<string, RuntimeContextAnalyticsSnapshot | null>,
1367
+ right: Map<string, RuntimeContextAnalyticsSnapshot | null>,
1368
+ ): boolean {
1369
+ if (left.size !== right.size) {
1370
+ return false;
1371
+ }
1372
+ for (const [key, snapshot] of left.entries()) {
1373
+ if (!right.has(key)) {
1374
+ return false;
1375
+ }
1376
+ if (!runtimeContextAnalyticsSnapshotsEqual(snapshot, right.get(key) ?? null)) {
1377
+ return false;
1378
+ }
1379
+ }
1380
+ return true;
1381
+ }
1382
+
1383
+ function refreshRenderSnapshot(): RuntimeRenderSnapshot {
1384
+ const surface = getCachedSurface(state.document, activeStory);
1385
+ return {
1386
+ documentId: state.documentId,
1387
+ sessionId: state.sessionId,
1388
+ sourceLabel: state.sourceLabel,
1389
+ revisionToken: state.revisionToken,
1390
+ isReady: state.phase === "ready",
1391
+ isDirty: state.isDirty,
1392
+ readOnly: state.readOnly,
1393
+ documentMode: viewState.documentMode,
1394
+ selection: toPublicSelectionSnapshot(state.selection, activeStory),
1395
+ activeStory,
1396
+ pageLayout: getCachedPageLayoutSnapshot(state, activeStory) ?? undefined,
1397
+ documentStats: toPublicDocumentStats(state),
1398
+ comments: getCachedCommentSidebarSnapshot(state),
1399
+ trackedChanges: getCachedTrackedChangesSnapshot(state, surface),
1400
+ compatibility: getCachedCompatibilityReport(state),
1401
+ warnings: state.warnings.map((warning) => toPublicWarning(warning)),
1402
+ fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
1403
+ commandState: {
1404
+ canUndo: history.past.length > 0,
1405
+ canRedo: history.future.length > 0,
1406
+ readOnly: state.readOnly,
1407
+ },
1408
+ surface,
1409
+ protectionSnapshot,
1410
+ };
1411
+ }
1412
+
1413
+ function getCachedViewStateSnapshot(): EditorViewStateSnapshot {
1414
+ const activeStoryKey = storyTargetKey(activeStory);
1415
+ const pageLayout = cachedRenderSnapshot.pageLayout;
1416
+ if (
1417
+ cachedViewStateSnapshot &&
1418
+ cachedViewStateSnapshot.revisionToken === state.revisionToken &&
1419
+ cachedViewStateSnapshot.activeStoryKey === activeStoryKey &&
1420
+ cachedViewStateSnapshot.selection === state.selection &&
1421
+ cachedViewStateSnapshot.viewStateRef === viewState &&
1422
+ cachedViewStateSnapshot.pageLayout === pageLayout
1423
+ ) {
1424
+ return cachedViewStateSnapshot.snapshot;
1425
+ }
1426
+
1427
+ const surface = cachedRenderSnapshot.surface;
1428
+ const mainSurface =
1429
+ activeStory.kind === "main"
1430
+ ? surface
1431
+ : getCachedSurface(state.document, MAIN_STORY_TARGET);
1432
+ const snapshot = createEditorViewStateSnapshot(
1433
+ viewState,
1434
+ activeStory,
1435
+ toPublicSelectionSnapshot(state.selection, activeStory),
1436
+ surface,
1437
+ mainSurface,
1438
+ pageLayout,
1439
+ state.document.numbering,
1440
+ );
1441
+ cachedViewStateSnapshot = {
1442
+ revisionToken: state.revisionToken,
1443
+ activeStoryKey,
1444
+ selection: state.selection,
1445
+ viewStateRef: viewState,
1446
+ pageLayout,
1447
+ snapshot,
1448
+ };
1449
+ return snapshot;
1450
+ }
1451
+
1452
+ let cachedRenderSnapshot = refreshRenderSnapshot();
1453
+
1454
+ emit({
1455
+ type: "ready",
1456
+ documentId: state.documentId,
1457
+ sessionId: state.sessionId,
1458
+ source:
1459
+ options.sourceKind ??
1460
+ (options.initialSessionState
1461
+ ? "session"
1462
+ : options.initialSnapshot
1463
+ ? "snapshot"
1464
+ : "canonical"),
1465
+ stats: toPublicDocumentStats(state),
1466
+ compatibility: toPublicCompatibilityReport(createDerivedCompatibility(state)),
1467
+ comments: cachedRenderSnapshot.comments,
1468
+ trackedChanges: cachedRenderSnapshot.trackedChanges,
1469
+ });
1470
+ if (options.fatalError) {
1471
+ emit({
1472
+ type: "error",
1473
+ documentId: state.documentId,
1474
+ error: options.fatalError,
1475
+ });
1476
+ }
1477
+
1478
+ return {
1479
+ subscribe(listener) {
1480
+ listeners.add(listener);
1481
+ return () => {
1482
+ listeners.delete(listener);
1483
+ };
1484
+ },
1485
+ subscribeToEvents(listener) {
1486
+ eventListeners.add(listener);
1487
+ return () => {
1488
+ eventListeners.delete(listener);
1489
+ };
1490
+ },
1491
+ getRenderSnapshot() {
1492
+ return cachedRenderSnapshot;
1493
+ },
1494
+ getCanonicalDocument() {
1495
+ return state.document;
1496
+ },
1497
+ getSourcePackage() {
1498
+ return state.sourcePackage;
1499
+ },
1500
+ emitBlockedCommand(command, reasons) {
1501
+ emit({
1502
+ type: "command_blocked",
1503
+ documentId: state.documentId,
1504
+ command,
1505
+ reasons,
1506
+ });
1507
+ },
1508
+ dispatch(command) {
1509
+ const commandSelection = getCommandSelection(command, state.selection);
1510
+ if (isMutationCommand(command)) {
1511
+ const blockedReasons = evaluateWorkflowBlockedReasons(
1512
+ commandSelection,
1513
+ command.type,
1514
+ );
1515
+ if (blockedReasons.length > 0) {
1516
+ emit({
1517
+ type: "command_blocked",
1518
+ documentId: state.documentId,
1519
+ command: command.type,
1520
+ reasons: blockedReasons,
1521
+ });
1522
+ return;
1523
+ }
1524
+ }
1525
+
1526
+ if (command.type === "history.undo") {
1527
+ if (viewState.documentMode === "viewing") return;
1528
+ applyHistory("undo");
1529
+ return;
1530
+ }
1531
+
1532
+ if (command.type === "history.redo") {
1533
+ if (viewState.documentMode === "viewing") return;
1534
+ applyHistory("redo");
1535
+ return;
1536
+ }
1537
+ try {
1538
+ const transaction = executeEditorCommand(state, command, {
1539
+ timestamp: command.origin?.timestamp ?? clock(),
1540
+ documentMode: getEffectiveDocumentMode(commandSelection),
1541
+ defaultAuthorId: defaultAuthorId ?? undefined,
1542
+ });
1543
+ commit(transaction);
1544
+ } catch (error) {
1545
+ emitError(toRuntimeError(error));
1546
+ }
1547
+ },
1548
+ undo() {
1549
+ this.dispatch({
1550
+ type: "history.undo",
1551
+ origin: createOrigin("runtime", clock()),
1552
+ });
1553
+ },
1554
+ redo() {
1555
+ this.dispatch({
1556
+ type: "history.redo",
1557
+ origin: createOrigin("runtime", clock()),
1558
+ });
1559
+ },
1560
+ focus() {
1561
+ viewState = applyFocused(viewState, true);
1562
+ this.dispatch({
1563
+ type: "runtime.focus",
1564
+ focused: true,
1565
+ origin: createOrigin("api", clock()),
1566
+ });
1567
+ },
1568
+ blur() {
1569
+ viewState = applyFocused(viewState, false);
1570
+ this.dispatch({
1571
+ type: "runtime.focus",
1572
+ focused: false,
1573
+ origin: createOrigin("api", clock()),
1574
+ });
1575
+ },
1576
+ setDefaultAuthorId(authorId) {
1577
+ defaultAuthorId = authorId;
1578
+ },
1579
+ getDefaultAuthorId() {
1580
+ return defaultAuthorId;
1581
+ },
1582
+ replaceText(text, target) {
1583
+ try {
1584
+ const timestamp = clock();
1585
+ applyTextCommandInActiveStory(
1586
+ {
1587
+ type: "text.insert",
1588
+ text,
1589
+ origin: createOrigin("api", timestamp),
1590
+ },
1591
+ {
1592
+ selection: target ? createSelectionFromPublicAnchor(target) : state.selection,
1593
+ blockedCommandName: "replaceText",
1594
+ },
1595
+ );
1596
+ } catch (error) {
1597
+ emitError(toRuntimeError(error));
1598
+ }
1599
+ },
1600
+ applyActiveStoryTextCommand(command) {
1601
+ try {
1602
+ applyTextCommandInActiveStory(command);
1603
+ } catch (error) {
1604
+ emitError(toRuntimeError(error));
1605
+ }
1606
+ },
1607
+ addComment(params) {
1608
+ if (viewState.documentMode === "viewing") {
1609
+ const error: InternalEditorError = {
1610
+ errorId: createSessionId("comment-viewing-mode", clock()),
1611
+ code: "validation_failed",
1612
+ isFatal: false,
1613
+ message: "Cannot add comments in viewing mode.",
1614
+ source: "runtime",
1615
+ details: {
1616
+ reason: "viewing_mode",
1617
+ },
1618
+ };
1619
+ emitError(error);
1620
+ throw toStructuredRuntimeException(error);
1621
+ }
1622
+ const commentId = createEntityId("comment", state.document.review.comments, clock());
1623
+ const anchor = params.anchor
1624
+ ? toInternalAnchorProjection(params.anchor)
1625
+ : state.selection.activeRange;
1626
+ const selection = params.anchor
1627
+ ? createSelectionFromPublicAnchor(params.anchor)
1628
+ : state.selection;
1629
+ if (!canCreateDocxCommentAnchor(state.document.content, anchor)) {
1630
+ const error: InternalEditorError = {
1631
+ errorId: createSessionId("comment-anchor", clock()),
1632
+ code: "validation_failed",
1633
+ isFatal: false,
1634
+ message:
1635
+ "DOCX comments must use a non-empty range that stays within a single paragraph.",
1636
+ source: "runtime",
1637
+ details: {
1638
+ reason: "invalid_comment_anchor",
1639
+ },
1640
+ };
1641
+ emitError(error);
1642
+ throw toStructuredRuntimeException(error);
1643
+ }
1644
+ const authorId = params.authorId ?? defaultAuthorId ?? "unknown";
1645
+ const createdAt = clock();
1646
+ const entries: CommentEntryRecord[] = [
1647
+ {
1648
+ entryId: `${commentId}-entry-1`,
1649
+ authorId,
1650
+ body: params.body ?? "",
1651
+ createdAt,
1652
+ },
1653
+ ];
1654
+ const comment: CommentThreadRecord = {
1655
+ commentId,
1656
+ anchor,
1657
+ createdAt,
1658
+ createdBy: authorId,
1659
+ authorId,
1660
+ body: params.body ?? "",
1661
+ entries,
1662
+ status: anchor.kind === "detached" ? "detached" : "open",
1663
+ warningIds: [],
1664
+ isResolved: false,
1665
+ metadata: {
1666
+ source: "runtime",
1667
+ },
1668
+ };
1669
+
1670
+ this.dispatch({
1671
+ type: "comment.add",
1672
+ comment,
1673
+ selection,
1674
+ origin: createOrigin("api", clock()),
1675
+ });
1676
+
1677
+ return commentId;
1678
+ },
1679
+ openComment(commentId) {
1680
+ this.dispatch({
1681
+ type: "comment.open",
1682
+ commentId,
1683
+ origin: createOrigin("api", clock()),
1684
+ });
1685
+ },
1686
+ resolveComment(commentId) {
1687
+ this.dispatch({
1688
+ type: "comment.resolve",
1689
+ commentId,
1690
+ resolvedBy: defaultAuthorId ?? "unknown",
1691
+ origin: createOrigin("api", clock()),
1692
+ });
1693
+ },
1694
+ reopenComment(commentId) {
1695
+ this.dispatch({
1696
+ type: "comment.reopen",
1697
+ commentId,
1698
+ origin: createOrigin("api", clock()),
1699
+ });
1700
+ },
1701
+ addCommentReply(commentId, body, authorId) {
1702
+ this.dispatch({
1703
+ type: "comment.add-reply",
1704
+ commentId,
1705
+ body,
1706
+ authorId: authorId ?? defaultAuthorId,
1707
+ origin: createOrigin("api", clock()),
1708
+ });
1709
+ },
1710
+ editCommentBody(commentId, body) {
1711
+ this.dispatch({
1712
+ type: "comment.edit-body",
1713
+ commentId,
1714
+ body,
1715
+ origin: createOrigin("api", clock()),
1716
+ });
1717
+ },
1718
+ acceptChange(changeId) {
1719
+ this.dispatch({
1720
+ type: "change.accept",
1721
+ changeId,
1722
+ origin: createOrigin("api", clock()),
1723
+ });
1724
+ },
1725
+ rejectChange(changeId) {
1726
+ this.dispatch({
1727
+ type: "change.reject",
1728
+ changeId,
1729
+ origin: createOrigin("api", clock()),
1730
+ });
1731
+ },
1732
+ acceptAllChanges() {
1733
+ this.dispatch({
1734
+ type: "change.accept-all",
1735
+ origin: createOrigin("api", clock()),
1736
+ });
1737
+ },
1738
+ rejectAllChanges() {
1739
+ this.dispatch({
1740
+ type: "change.reject-all",
1741
+ origin: createOrigin("api", clock()),
1742
+ });
1743
+ },
1744
+ openStory(target) {
1745
+ const normalizedTarget =
1746
+ target.kind === "header" || target.kind === "footer"
1747
+ ? normalizeHeaderFooterTarget(
1748
+ state.document,
1749
+ target,
1750
+ cachedRenderSnapshot.pageLayout?.sectionIndex,
1751
+ ) ?? target
1752
+ : target;
1753
+ if (storyTargetsEqual(activeStory, normalizedTarget)) {
1754
+ return true;
1755
+ }
1756
+ if (!isValidStoryTarget(state, normalizedTarget)) {
1757
+ return false;
1758
+ }
1759
+ switchActiveStory(normalizedTarget);
1760
+ return true;
1761
+ },
1762
+ closeStory() {
1763
+ if (activeStory.kind === "main") {
1764
+ return;
1765
+ }
1766
+ switchActiveStory(MAIN_STORY_TARGET);
1767
+ },
1768
+ getActiveStory() {
1769
+ return activeStory;
1770
+ },
1771
+ getViewState() {
1772
+ return getCachedViewStateSnapshot();
1773
+ },
1774
+ setViewMode(mode) {
1775
+ viewState = applyViewMode(viewState, mode);
1776
+ cachedRenderSnapshot = refreshRenderSnapshot();
1777
+ for (const listener of listeners) {
1778
+ listener();
1779
+ }
1780
+ },
1781
+ setDocumentMode(mode) {
1782
+ viewState = applyDocumentMode(viewState, mode);
1783
+ cachedRenderSnapshot = refreshRenderSnapshot();
1784
+ for (const listener of listeners) {
1785
+ listener();
1786
+ }
1787
+ },
1788
+ getProtectionSnapshot() {
1789
+ return cachedRenderSnapshot.protectionSnapshot;
1790
+ },
1791
+ setWorkspaceMode(mode) {
1792
+ viewState = applyWorkspaceMode(viewState, mode);
1793
+ cachedRenderSnapshot = refreshRenderSnapshot();
1794
+ for (const listener of listeners) {
1795
+ listener();
1796
+ }
1797
+ },
1798
+ setZoom(level) {
1799
+ viewState = applyZoomLevel(viewState, level);
1800
+ cachedRenderSnapshot = refreshRenderSnapshot();
1801
+ for (const listener of listeners) {
1802
+ listener();
1803
+ }
1804
+ },
1805
+ getPageLayoutSnapshot() {
1806
+ return getCachedPageLayoutSnapshot(state, activeStory);
1807
+ },
1808
+ getDocumentNavigationSnapshot() {
1809
+ return getCachedDocumentNavigationSnapshot(state, activeStory);
1810
+ },
1811
+ getCurrentLocation() {
1812
+ const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
1813
+ return createCurrentLocation({
1814
+ document: state.document,
1815
+ renderSnapshot: cachedRenderSnapshot,
1816
+ navigation,
1817
+ sections: createDocumentSectionSnapshots(state.document, navigation),
1818
+ });
1819
+ },
1820
+ getLocationForSelection(selection: SelectionSnapshot) {
1821
+ const selectionStory = selection.storyTarget ?? activeStory;
1822
+ const navigation = getCachedDocumentNavigationSnapshot(state, selectionStory);
1823
+ return createLocationFromSelection({
1824
+ document: state.document,
1825
+ navigation,
1826
+ sections: createDocumentSectionSnapshots(state.document, navigation),
1827
+ selection,
1828
+ });
1829
+ },
1830
+ getLocationForAnchor(anchor: EditorAnchorProjection, storyTarget?: EditorStoryTarget) {
1831
+ const resolvedStory = storyTarget ?? activeStory;
1832
+ const navigation = getCachedDocumentNavigationSnapshot(state, resolvedStory);
1833
+ return createDocumentLocationSnapshot({
1834
+ document: state.document,
1835
+ navigation,
1836
+ sections: createDocumentSectionSnapshots(state.document, navigation),
1837
+ anchor,
1838
+ storyTarget: resolvedStory,
1839
+ source: { kind: "navigation" },
1840
+ });
1841
+ },
1842
+ captureRestorePoint(input?: SelectionSnapshot | EditorAnchorProjection) {
1843
+ const location =
1844
+ input === undefined
1845
+ ? this.getCurrentLocation()
1846
+ : "activeRange" in input
1847
+ ? this.getLocationForSelection(input)
1848
+ : this.getLocationForAnchor(input, activeStory);
1849
+ if (!location) {
1850
+ return null;
1851
+ }
1852
+ return createRestorePoint({
1853
+ location,
1854
+ revisionToken: state.revisionToken,
1855
+ createdAt: clock(),
1856
+ checkpointType:
1857
+ input === undefined
1858
+ ? "selection"
1859
+ : "activeRange" in input
1860
+ ? "selection"
1861
+ : "manual",
1862
+ });
1863
+ },
1864
+ restoreToPoint(point, options) {
1865
+ if (point.location.anchor.kind === "detached") {
1866
+ return {
1867
+ status: "detached",
1868
+ reasons: [point.location.anchor.reason],
1869
+ location: point.location,
1870
+ };
1871
+ }
1872
+
1873
+ const behavior = options?.behavior ?? "semantic";
1874
+ if (behavior === "exact" && point.revisionToken !== state.revisionToken) {
1875
+ return {
1876
+ status: "blocked",
1877
+ reasons: [
1878
+ "restore point no longer matches the current revision; use semantic replay after content changes",
1879
+ ],
1880
+ location: point.location,
1881
+ };
1882
+ }
1883
+
1884
+ if (
1885
+ point.location.storyTarget &&
1886
+ !storyTargetsEqual(activeStory, point.location.storyTarget)
1887
+ ) {
1888
+ if (point.location.storyTarget.kind === "main") {
1889
+ this.closeStory();
1890
+ } else {
1891
+ this.openStory(point.location.storyTarget);
1892
+ }
1893
+ }
1894
+ const selection = createSelectionFromPublicAnchor(point.location.anchor);
1895
+ this.dispatch({
1896
+ type: "selection.set",
1897
+ selection,
1898
+ origin: createOrigin("api", clock()),
1899
+ });
1900
+ const location = this.getCurrentLocation() ?? point.location;
1901
+ return {
1902
+ status: point.revisionToken === state.revisionToken ? "restored" : "remapped",
1903
+ selection: cachedRenderSnapshot.selection,
1904
+ location,
1905
+ };
1906
+ },
1907
+ getOutlineSnapshot() {
1908
+ return createDocumentOutlineSnapshot({
1909
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
1910
+ activeStory,
1911
+ selectionHead: state.selection.head,
1912
+ });
1913
+ },
1914
+ getTocSnapshot() {
1915
+ return createTocSnapshot(
1916
+ state.document,
1917
+ getCachedDocumentNavigationSnapshot(state, activeStory),
1918
+ );
1919
+ },
1920
+ getSections() {
1921
+ return createDocumentSectionSnapshots(
1922
+ state.document,
1923
+ getCachedDocumentNavigationSnapshot(state, activeStory),
1924
+ );
1925
+ },
1926
+ getSectionSnapshot(input) {
1927
+ const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
1928
+ const sections = createDocumentSectionSnapshots(state.document, navigation);
1929
+ if (input.headingId) {
1930
+ const heading = navigation.headings.find((entry) => entry.headingId === input.headingId);
1931
+ if (heading) {
1932
+ return sections.find((section) => section.sectionIndex === heading.sectionIndex) ?? null;
1933
+ }
1934
+ }
1935
+ return findDocumentSectionSnapshot(
1936
+ sections,
1937
+ input,
1938
+ createSectionLocations(state.document, sections),
1939
+ );
1940
+ },
1941
+ describeEventImpact(event) {
1942
+ return describeEventImpact(event);
1943
+ },
1944
+ getFieldSnapshot() {
1945
+ return buildFieldSnapshot(state.document);
1946
+ },
1947
+ updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult {
1948
+ const refreshed = refreshDocumentFields(
1949
+ state.document,
1950
+ state.selection.head,
1951
+ activeStory,
1952
+ options,
1953
+ );
1954
+ if (refreshed.changed) {
1955
+ this.dispatch({
1956
+ type: "document.replace",
1957
+ document: refreshed.document,
1958
+ mapping: createEmptyMapping(),
1959
+ protectionSelection: refreshed.protectionSelection,
1960
+ origin: createOrigin("api", clock()),
1961
+ });
1962
+ }
1963
+ const snapshot = buildFieldSnapshot(refreshed.document);
1964
+ return {
1965
+ totalCount: snapshot.totalCount,
1966
+ updatedCount: refreshed.updatedCount,
1967
+ preserveOnlyCount: snapshot.preserveOnlyCount,
1968
+ };
1969
+ },
1970
+ updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult {
1971
+ const refreshed = refreshDocumentTableOfContents(
1972
+ state.document,
1973
+ state.selection.head,
1974
+ activeStory,
1975
+ options,
1976
+ );
1977
+ if (refreshed.changed) {
1978
+ this.dispatch({
1979
+ type: "document.replace",
1980
+ document: refreshed.document,
1981
+ mapping: createEmptyMapping(),
1982
+ protectionSelection: refreshed.protectionSelection,
1983
+ origin: createOrigin("api", clock()),
1984
+ });
1985
+ }
1986
+ return refreshed.result;
1987
+ },
1988
+ getSessionState() {
1989
+ const compatibility = createDerivedCompatibility(state);
1990
+ return editorSessionStateFromPersistedSnapshot(
1991
+ {
1992
+ ...(createPersistedEditorSnapshot(state, {
1993
+ editorBuild,
1994
+ savedAt: clock(),
1995
+ compatibility,
1996
+ protectionSnapshot,
1997
+ }) as unknown as PersistedEditorSnapshot),
1998
+ workflowMetadata: deriveWorkflowMetadataSnapshot(),
1999
+ },
2000
+ );
2001
+ },
2002
+ getPersistedSnapshot() {
2003
+ return persistedSnapshotFromEditorSessionState(this.getSessionState(), {
2004
+ savedAt: clock(),
2005
+ });
2006
+ },
2007
+ getCompatibilityReport() {
2008
+ return toPublicCompatibilityReport(createDerivedCompatibility(state));
2009
+ },
2010
+ getWarnings() {
2011
+ return state.warnings.map((warning) => toPublicWarning(warning));
2012
+ },
2013
+ getSuggestionsSnapshot() {
2014
+ return getCachedSuggestionsSnapshot(state);
2015
+ },
2016
+ async exportDocx(exportOptions) {
2017
+ if (!options.exportDocx) {
2018
+ const error: InternalEditorError = {
2019
+ errorId: createEntityId("error", {}, clock()),
2020
+ code: "export_failed",
2021
+ isFatal: false,
2022
+ message: "DOCX export requires an injected exporter until the IO substrate lands.",
2023
+ source: "export",
2024
+ details: {
2025
+ requestedOptions: exportOptions ?? {},
2026
+ },
2027
+ };
2028
+ emitError(error);
2029
+ throw toStructuredRuntimeException(error);
2030
+ }
2031
+
2032
+ const result = await options.exportDocx(this.getSessionState(), exportOptions);
2033
+
2034
+ emit({
2035
+ type: "export_completed",
2036
+ documentId: state.documentId,
2037
+ result,
2038
+ });
2039
+
2040
+ return result;
2041
+ },
2042
+ setWorkflowOverlay(overlay) {
2043
+ workflowOverlay = structuredClone(overlay);
2044
+ cachedRenderSnapshot = refreshRenderSnapshot();
2045
+ const snapshot = deriveWorkflowScopeSnapshot()!;
2046
+ emit({
2047
+ type: "workflow_overlay_changed",
2048
+ documentId: state.documentId,
2049
+ snapshot,
2050
+ });
2051
+ if (workflowOverlay.activeWorkItemId !== undefined) {
2052
+ emit({
2053
+ type: "workflow_active_work_item_changed",
2054
+ documentId: state.documentId,
2055
+ activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
2056
+ });
2057
+ }
2058
+ for (const listener of listeners) {
2059
+ listener();
2060
+ }
2061
+ },
2062
+ clearWorkflowOverlay() {
2063
+ workflowOverlay = null;
2064
+ cachedRenderSnapshot = refreshRenderSnapshot();
2065
+ emit({
2066
+ type: "workflow_active_work_item_changed",
2067
+ documentId: state.documentId,
2068
+ activeWorkItemId: null,
2069
+ });
2070
+ emit({
2071
+ type: "workflow_overlay_changed",
2072
+ documentId: state.documentId,
2073
+ snapshot: {
2074
+ overlayPresent: false,
2075
+ activeWorkItemId: null,
2076
+ scopes: [],
2077
+ candidates: [],
2078
+ blockedReasons: [],
2079
+ },
2080
+ });
2081
+ for (const listener of listeners) {
2082
+ listener();
2083
+ }
2084
+ },
2085
+ getWorkflowScopeSnapshot() {
2086
+ return getCachedWorkflowScopeSnapshot();
2087
+ },
2088
+ getInteractionGuardSnapshot() {
2089
+ return getCachedInteractionGuardSnapshot();
2090
+ },
2091
+ getWorkflowMarkupSnapshot() {
2092
+ return getCachedWorkflowMarkupSnapshot();
2093
+ },
2094
+ setWorkflowMetadataDefinitions(definitions) {
2095
+ workflowMetadataDefinitions = structuredClone(definitions);
2096
+ const snapshot = deriveWorkflowMetadataSnapshot();
2097
+ emit({
2098
+ type: "workflow_metadata_changed",
2099
+ documentId: state.documentId,
2100
+ snapshot,
2101
+ });
2102
+ for (const listener of listeners) {
2103
+ listener();
2104
+ }
2105
+ },
2106
+ clearWorkflowMetadataDefinitions() {
2107
+ workflowMetadataDefinitions = [];
2108
+ const snapshot = deriveWorkflowMetadataSnapshot();
2109
+ emit({
2110
+ type: "workflow_metadata_changed",
2111
+ documentId: state.documentId,
2112
+ snapshot,
2113
+ });
2114
+ for (const listener of listeners) {
2115
+ listener();
2116
+ }
2117
+ },
2118
+ setWorkflowMetadataEntries(entries) {
2119
+ workflowMetadataEntries = structuredClone(entries);
2120
+ const snapshot = deriveWorkflowMetadataSnapshot();
2121
+ emit({
2122
+ type: "workflow_metadata_changed",
2123
+ documentId: state.documentId,
2124
+ snapshot,
2125
+ });
2126
+ for (const listener of listeners) {
2127
+ listener();
2128
+ }
2129
+ },
2130
+ clearWorkflowMetadataEntries() {
2131
+ workflowMetadataEntries = [];
2132
+ const snapshot = deriveWorkflowMetadataSnapshot();
2133
+ emit({
2134
+ type: "workflow_metadata_changed",
2135
+ documentId: state.documentId,
2136
+ snapshot,
2137
+ });
2138
+ for (const listener of listeners) {
2139
+ listener();
2140
+ }
2141
+ },
2142
+ getWorkflowMetadataSnapshot() {
2143
+ return deriveWorkflowMetadataSnapshot();
2144
+ },
2145
+ setHostAnnotationOverlay(overlay) {
2146
+ hostAnnotationOverlay = structuredClone(overlay);
2147
+ emit({
2148
+ type: "host_annotation_overlay_changed",
2149
+ documentId: state.documentId,
2150
+ snapshot: deriveHostAnnotationSnapshot(),
2151
+ });
2152
+ for (const listener of listeners) {
2153
+ listener();
2154
+ }
2155
+ },
2156
+ clearHostAnnotationOverlay() {
2157
+ hostAnnotationOverlay = null;
2158
+ emit({
2159
+ type: "host_annotation_overlay_changed",
2160
+ documentId: state.documentId,
2161
+ snapshot: deriveHostAnnotationSnapshot(),
2162
+ });
2163
+ for (const listener of listeners) {
2164
+ listener();
2165
+ }
2166
+ },
2167
+ getHostAnnotationSnapshot() {
2168
+ return deriveHostAnnotationSnapshot();
2169
+ },
2170
+ getWorkflowCandidateRanges(options) {
2171
+ return deriveWorkflowCandidateRangesFromMarkup(this.getWorkflowMarkupSnapshot(), options);
2172
+ },
2173
+ replaceWorkflowMarkupText(markupId, text) {
2174
+ const target = this
2175
+ .getWorkflowMarkupSnapshot()
2176
+ .items.find((item) => item.markupId === markupId);
2177
+ if (!target || target.anchor.kind === "detached") {
2178
+ return;
2179
+ }
2180
+ const targetStory = target.storyTarget ?? MAIN_STORY_TARGET;
2181
+ if (!storyTargetsEqual(activeStory, targetStory)) {
2182
+ if (targetStory.kind === "main") {
2183
+ this.closeStory();
2184
+ } else if (!this.openStory(targetStory)) {
2185
+ return;
2186
+ }
2187
+ }
2188
+ this.replaceText(text, target.anchor);
2189
+ },
2190
+ getDocumentTextStream() {
2191
+ return cachedRenderSnapshot.surface
2192
+ ? createDocumentTextStreamSnapshots({
2193
+ surface: cachedRenderSnapshot.surface,
2194
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
2195
+ })
2196
+ : [];
2197
+ },
2198
+ getStoryTextStream(target: EditorStoryTarget) {
2199
+ return this.getDocumentTextStream().find((stream) => storyTargetsEqual(stream.target, target)) ?? null;
2200
+ },
2201
+ getDocumentChunks() {
2202
+ return createDocumentChunks({
2203
+ document: state.document,
2204
+ renderSnapshot: cachedRenderSnapshot,
2205
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
2206
+ });
2207
+ },
2208
+ getWorkflowChunks() {
2209
+ return createWorkflowChunks({
2210
+ document: state.document,
2211
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
2212
+ workflowMarkup: getCachedWorkflowMarkupSnapshot(),
2213
+ });
2214
+ },
2215
+ getReviewWorkSnapshot() {
2216
+ return createReviewWorkSnapshot({
2217
+ comments: cachedRenderSnapshot.comments,
2218
+ trackedChanges: cachedRenderSnapshot.trackedChanges,
2219
+ workflowMarkup: getCachedWorkflowMarkupSnapshot(),
2220
+ document: state.document,
2221
+ navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
2222
+ });
2223
+ },
2224
+ getRuntimeContextAnalytics(query) {
2225
+ return getCachedRuntimeContextAnalytics(query);
2226
+ },
2227
+ };
2228
+
2229
+ function applyHistory(direction: "undo" | "redo"): void {
2230
+ const source = direction === "undo" ? history.past : history.future;
2231
+ const target = source.pop();
2232
+
2233
+ if (!target) {
2234
+ return;
2235
+ }
2236
+
2237
+ const counterpart = direction === "undo" ? history.future : history.past;
2238
+ counterpart.push(state);
2239
+
2240
+ const previous = state;
2241
+ // Undo/redo changes the document — must mint a new revisionToken so
2242
+ // autosave/export checkpoint dedup treats it as fresh content.
2243
+ state = finalizeState(target, true, clock(), previous.revision);
2244
+ storySelections.set(storyTargetKey(activeStory), state.selection);
2245
+ cachedRenderSnapshot = refreshRenderSnapshot();
2246
+ notify(previous, state, {
2247
+ nextState: state,
2248
+ mapping: { steps: [] },
2249
+ effects: {
2250
+ warningsAdded: [],
2251
+ warningsCleared: [],
2252
+ },
2253
+ historyBoundary: "skip",
2254
+ markDirty: true,
2255
+ });
2256
+ }
2257
+
2258
+ function commit(transaction: EditorTransaction): void {
2259
+ const previous = state;
2260
+
2261
+ if (transaction.historyBoundary === "push") {
2262
+ history.past.push(state);
2263
+ history.future = [];
2264
+ }
2265
+
2266
+ protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
2267
+ state = finalizeState(transaction.nextState, transaction.markDirty, clock());
2268
+ storySelections.set(storyTargetKey(activeStory), state.selection);
2269
+ cachedRenderSnapshot = refreshRenderSnapshot();
2270
+ notify(previous, state, transaction);
2271
+ }
2272
+
2273
+ function notify(
2274
+ previous: EditorState,
2275
+ next: EditorState,
2276
+ transaction: EditorTransaction,
2277
+ ): void {
2278
+ const emittedSuggestionIds = new Set<string>();
2279
+ if (previous.isDirty !== next.isDirty) {
2280
+ emit({
2281
+ type: "dirty_changed",
2282
+ documentId: next.documentId,
2283
+ isDirty: next.isDirty,
2284
+ });
2285
+ }
2286
+
2287
+ if (selectionChanged(previous.selection, next.selection)) {
2288
+ emit({
2289
+ type: "selection_changed",
2290
+ documentId: next.documentId,
2291
+ selection: toPublicSelectionSnapshot(next.selection, activeStory),
2292
+ });
2293
+ }
2294
+
2295
+ if (transaction.effects.commentAdded) {
2296
+ emit({
2297
+ type: "comment_added",
2298
+ documentId: next.documentId,
2299
+ commentId: transaction.effects.commentAdded.commentId,
2300
+ anchor: toPublicAnchorProjection(transaction.effects.commentAdded.anchor),
2301
+ });
2302
+ }
2303
+
2304
+ if (transaction.effects.commentResolved) {
2305
+ emit({
2306
+ type: "comment_resolved",
2307
+ documentId: next.documentId,
2308
+ commentId: transaction.effects.commentResolved.commentId,
2309
+ });
2310
+ }
2311
+
2312
+ if (transaction.effects.changeAccepted) {
2313
+ emit({
2314
+ type: "change_accepted",
2315
+ documentId: next.documentId,
2316
+ changeId: transaction.effects.changeAccepted.changeId,
2317
+ });
2318
+ const updatedSuggestion = findSuggestionByChangeId(
2319
+ getCachedSuggestionsSnapshot(next),
2320
+ transaction.effects.changeAccepted.changeId,
2321
+ );
2322
+ if (updatedSuggestion) {
2323
+ emit({
2324
+ type: "suggestion_updated",
2325
+ documentId: next.documentId,
2326
+ suggestionId: updatedSuggestion.suggestionId,
2327
+ changeIds: updatedSuggestion.changeIds,
2328
+ suggestionKind: updatedSuggestion.kind,
2329
+ status: updatedSuggestion.status,
2330
+ storyTarget: updatedSuggestion.storyTarget,
2331
+ anchor: updatedSuggestion.anchor,
2332
+ });
2333
+ }
2334
+ }
2335
+
2336
+ if (transaction.effects.changeRejected) {
2337
+ emit({
2338
+ type: "change_rejected",
2339
+ documentId: next.documentId,
2340
+ changeId: transaction.effects.changeRejected.changeId,
2341
+ });
2342
+ const updatedSuggestion = findSuggestionByChangeId(
2343
+ getCachedSuggestionsSnapshot(next),
2344
+ transaction.effects.changeRejected.changeId,
2345
+ );
2346
+ if (updatedSuggestion) {
2347
+ emit({
2348
+ type: "suggestion_updated",
2349
+ documentId: next.documentId,
2350
+ suggestionId: updatedSuggestion.suggestionId,
2351
+ changeIds: updatedSuggestion.changeIds,
2352
+ suggestionKind: updatedSuggestion.kind,
2353
+ status: updatedSuggestion.status,
2354
+ storyTarget: updatedSuggestion.storyTarget,
2355
+ anchor: updatedSuggestion.anchor,
2356
+ });
2357
+ }
2358
+ }
2359
+
2360
+ if (transaction.effects.revisionAuthored) {
2361
+ emit({
2362
+ type: "change_authored",
2363
+ documentId: next.documentId,
2364
+ changeId: transaction.effects.revisionAuthored.changeId,
2365
+ kind: transaction.effects.revisionAuthored.kind,
2366
+ });
2367
+ const authoredSuggestion = findSuggestionByChangeId(
2368
+ getCachedSuggestionsSnapshot(next),
2369
+ transaction.effects.revisionAuthored.changeId,
2370
+ );
2371
+ if (authoredSuggestion) {
2372
+ emittedSuggestionIds.add(authoredSuggestion.suggestionId);
2373
+ emit({
2374
+ type: "suggestion_authored",
2375
+ documentId: next.documentId,
2376
+ suggestionId: authoredSuggestion.suggestionId,
2377
+ changeIds: authoredSuggestion.changeIds,
2378
+ suggestionKind: authoredSuggestion.kind,
2379
+ storyTarget: authoredSuggestion.storyTarget,
2380
+ anchor: authoredSuggestion.anchor,
2381
+ isReplacement: authoredSuggestion.isReplacement,
2382
+ });
2383
+ }
2384
+ }
2385
+
2386
+ const addedRevisionIds = Object.keys(next.document.review.revisions).filter(
2387
+ (revisionId) => previous.document.review.revisions[revisionId] === undefined,
2388
+ );
2389
+ if (addedRevisionIds.length > 0) {
2390
+ const suggestionsSnapshot = getCachedSuggestionsSnapshot(next);
2391
+ for (const revisionId of addedRevisionIds) {
2392
+ const authoredSuggestion = findSuggestionByChangeId(suggestionsSnapshot, revisionId);
2393
+ if (!authoredSuggestion || emittedSuggestionIds.has(authoredSuggestion.suggestionId)) {
2394
+ continue;
2395
+ }
2396
+ emittedSuggestionIds.add(authoredSuggestion.suggestionId);
2397
+ emit({
2398
+ type: "suggestion_authored",
2399
+ documentId: next.documentId,
2400
+ suggestionId: authoredSuggestion.suggestionId,
2401
+ changeIds: authoredSuggestion.changeIds,
2402
+ suggestionKind: authoredSuggestion.kind,
2403
+ storyTarget: authoredSuggestion.storyTarget,
2404
+ anchor: authoredSuggestion.anchor,
2405
+ isReplacement: authoredSuggestion.isReplacement,
2406
+ });
2407
+ }
2408
+ }
2409
+
2410
+ if (transaction.effects.commandBlocked) {
2411
+ emit({
2412
+ type: "command_blocked",
2413
+ documentId: next.documentId,
2414
+ command: transaction.effects.commandBlocked.code,
2415
+ reasons: [{
2416
+ code: transaction.effects.commandBlocked.code as WorkflowBlockedCommandReason["code"],
2417
+ message: transaction.effects.commandBlocked.message,
2418
+ }],
2419
+ });
2420
+ }
2421
+
2422
+ for (const warning of transaction.effects.warningsAdded) {
2423
+ const publicWarning = toPublicWarning(warning);
2424
+ emit({
2425
+ type: "warning_added",
2426
+ documentId: next.documentId,
2427
+ warning: publicWarning,
2428
+ });
2429
+ options.onWarning?.(publicWarning);
2430
+ }
2431
+
2432
+ for (const cleared of transaction.effects.warningsCleared) {
2433
+ emit({
2434
+ type: "warning_cleared",
2435
+ documentId: next.documentId,
2436
+ warningId: cleared.warningId,
2437
+ code: cleared.code,
2438
+ });
2439
+ }
2440
+
2441
+ for (const listener of listeners) {
2442
+ listener();
2443
+ }
2444
+ }
2445
+
2446
+ function applyTextCommandInActiveStory(
2447
+ command: ActiveStoryTextCommand,
2448
+ options: {
2449
+ selection?: EditorState["selection"];
2450
+ blockedCommandName?: string;
2451
+ } = {},
2452
+ ): void {
2453
+ const selection = options.selection ?? state.selection;
2454
+ if (
2455
+ activeStory.kind !== "main" &&
2456
+ getEffectiveDocumentMode(selection) === "suggesting" &&
2457
+ command.type === "paragraph.split"
2458
+ ) {
2459
+ emit({
2460
+ type: "command_blocked",
2461
+ documentId: state.documentId,
2462
+ command: options.blockedCommandName ?? command.type,
2463
+ reasons: [{
2464
+ code: "suggesting_unsupported",
2465
+ message: `"${command.type}" is not supported in suggesting mode for this story.`,
2466
+ storyTarget: activeStory,
2467
+ }],
2468
+ });
2469
+ return;
2470
+ }
2471
+ const blockedReasons = evaluateWorkflowBlockedReasons(selection, command.type);
2472
+ if (blockedReasons.length > 0) {
2473
+ emit({
2474
+ type: "command_blocked",
2475
+ documentId: state.documentId,
2476
+ command: options.blockedCommandName ?? command.type,
2477
+ reasons: blockedReasons,
2478
+ });
2479
+ return;
2480
+ }
2481
+
2482
+ const timestamp = command.origin?.timestamp ?? clock();
2483
+ const context = {
2484
+ timestamp,
2485
+ documentMode: getEffectiveDocumentMode(selection),
2486
+ defaultAuthorId: defaultAuthorId ?? undefined,
2487
+ } as const;
2488
+ const baseState = selection === state.selection
2489
+ ? state
2490
+ : {
2491
+ ...state,
2492
+ selection,
2493
+ };
2494
+
2495
+ if (activeStory.kind === "main") {
2496
+ commit(executeEditorCommand(baseState, command, context));
2497
+ return;
2498
+ }
2499
+
2500
+ const localState = createEditorState({
2501
+ documentId: state.documentId,
2502
+ sessionId,
2503
+ sourceLabel: state.sourceLabel,
2504
+ readOnly: state.readOnly,
2505
+ canonicalDocument: {
2506
+ ...state.document,
2507
+ content: {
2508
+ type: "doc",
2509
+ children: [...getStoryBlocks(state.document, activeStory)],
2510
+ },
2511
+ review: createSecondaryStoryLocalReviewState(state.document.review, activeStory),
2512
+ },
2513
+ compatibility: state.compatibility,
2514
+ warnings: state.warnings,
2515
+ fatalError: state.fatalError,
2516
+ });
2517
+ localState.selection = selection;
2518
+ const localTransaction = executeEditorCommand(localState, command, context);
2519
+
2520
+ if (!localTransaction.markDirty) {
2521
+ notify(state, state, {
2522
+ nextState: state,
2523
+ mapping: createEmptyMapping(),
2524
+ effects: localTransaction.effects,
2525
+ historyBoundary: "skip",
2526
+ markDirty: false,
2527
+ });
2528
+ return;
2529
+ }
2530
+
2531
+ const nextDocument = replaceStoryBlocks(
2532
+ state.document,
2533
+ activeStory,
2534
+ localTransaction.nextState.document.content.children,
2535
+ );
2536
+ const nextDocumentWithReview = {
2537
+ ...nextDocument,
2538
+ review: mergeSecondaryStoryReviewState(
2539
+ state.document.review,
2540
+ localTransaction.nextState.document.review,
2541
+ localTransaction.effects,
2542
+ activeStory,
2543
+ ),
2544
+ };
2545
+ const fullTransaction = executeEditorCommand(
2546
+ baseState,
2547
+ {
2548
+ type: "document.replace",
2549
+ document: nextDocumentWithReview,
2550
+ selection: localTransaction.nextState.selection,
2551
+ mapping: createEmptyMapping(),
2552
+ protectionSelection: selection,
2553
+ origin: command.origin,
2554
+ },
2555
+ context,
2556
+ );
2557
+
2558
+ commit({
2559
+ ...fullTransaction,
2560
+ effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
2561
+ });
2562
+ }
2563
+
2564
+ function mergeTransactionEffects(
2565
+ base: EditorTransaction["effects"],
2566
+ local: EditorTransaction["effects"],
2567
+ ): EditorTransaction["effects"] {
2568
+ return {
2569
+ warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
2570
+ warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
2571
+ commentAdded: base.commentAdded ?? local.commentAdded,
2572
+ commentResolved: base.commentResolved ?? local.commentResolved,
2573
+ commentReopened: base.commentReopened ?? local.commentReopened,
2574
+ commentReplyAdded: base.commentReplyAdded ?? local.commentReplyAdded,
2575
+ commentBodyEdited: base.commentBodyEdited ?? local.commentBodyEdited,
2576
+ changeAccepted: base.changeAccepted ?? local.changeAccepted,
2577
+ changeRejected: base.changeRejected ?? local.changeRejected,
2578
+ revisionAuthored: base.revisionAuthored ?? local.revisionAuthored,
2579
+ commandBlocked: base.commandBlocked ?? local.commandBlocked,
2580
+ };
2581
+ }
2582
+
2583
+ function mergeSecondaryStoryReviewState(
2584
+ currentReview: EditorState["document"]["review"],
2585
+ localReview: EditorState["document"]["review"],
2586
+ effects: EditorTransaction["effects"],
2587
+ storyTarget: EditorStoryTarget,
2588
+ ): EditorState["document"]["review"] {
2589
+ const nextReview: EditorState["document"]["review"] = {
2590
+ comments: { ...currentReview.comments },
2591
+ revisions: { ...currentReview.revisions },
2592
+ };
2593
+
2594
+ const currentStoryRevisionIds = Object.values(currentReview.revisions)
2595
+ .filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
2596
+ .map((revision) => revision.changeId);
2597
+ for (const revisionId of currentStoryRevisionIds) {
2598
+ delete nextReview.revisions[revisionId];
2599
+ }
2600
+ for (const revision of Object.values(localReview.revisions)) {
2601
+ nextReview.revisions[revision.changeId] = {
2602
+ ...revision,
2603
+ metadata: {
2604
+ ...revision.metadata,
2605
+ storyTarget: createRevisionStoryTargetRecord(storyTarget),
2606
+ },
2607
+ };
2608
+ }
2609
+
2610
+ if (effects.commentAdded) {
2611
+ const commentId = effects.commentAdded.commentId;
2612
+ const comment = localReview.comments[commentId];
2613
+ if (comment) {
2614
+ nextReview.comments[commentId] = comment;
2615
+ }
2616
+ }
2617
+
2618
+ return nextReview;
2619
+ }
2620
+
2621
+ function emit(event: DocumentRuntimeEvent): void {
2622
+ emitInternal(event);
2623
+ if (shouldEmitContextAnalyticsChanged(event)) {
2624
+ emitContextAnalyticsChanged();
2625
+ }
2626
+ }
2627
+
2628
+ function emitInternal(event: DocumentRuntimeEvent): void {
2629
+ options.onEvent?.(event);
2630
+ for (const listener of eventListeners) {
2631
+ listener(event);
2632
+ }
2633
+ }
2634
+
2635
+ function shouldEmitContextAnalyticsChanged(event: DocumentRuntimeEvent): boolean {
2636
+ switch (event.type) {
2637
+ case "selection_changed":
2638
+ case "story_changed":
2639
+ case "workflow_overlay_changed":
2640
+ case "workflow_active_work_item_changed":
2641
+ case "change_authored":
2642
+ case "change_accepted":
2643
+ case "change_rejected":
2644
+ case "comment_added":
2645
+ case "comment_resolved":
2646
+ case "warning_added":
2647
+ case "warning_cleared":
2648
+ case "error":
2649
+ case "ready":
2650
+ case "command_blocked":
2651
+ case "suggestion_authored":
2652
+ case "suggestion_updated":
2653
+ return true;
2654
+ case "context_analytics_changed":
2655
+ case "dirty_changed":
2656
+ case "autosave_state":
2657
+ case "snapshot_saved":
2658
+ case "session_saved":
2659
+ case "export_completed":
2660
+ case "host_annotation_overlay_changed":
2661
+ return false;
2662
+ default:
2663
+ return false;
2664
+ }
2665
+ }
2666
+
2667
+ function emitContextAnalyticsChanged(): void {
2668
+ const trackedSnapshots = collectTrackedContextAnalyticsSnapshots();
2669
+ if (
2670
+ lastEmittedContextAnalyticsSnapshots !== undefined &&
2671
+ trackedContextAnalyticsSnapshotsEqual(lastEmittedContextAnalyticsSnapshots, trackedSnapshots)
2672
+ ) {
2673
+ return;
2674
+ }
2675
+ lastEmittedContextAnalyticsSnapshots = trackedSnapshots;
2676
+ emitInternal({
2677
+ type: "context_analytics_changed",
2678
+ documentId: state.documentId,
2679
+ snapshot:
2680
+ trackedSnapshots.get(
2681
+ getRuntimeContextAnalyticsQueryKey(
2682
+ resolveCurrentContextAnalyticsQuery({
2683
+ workflowScopeSnapshot: getCachedWorkflowScopeSnapshot(),
2684
+ interactionGuardSnapshot: getCachedInteractionGuardSnapshot(),
2685
+ }),
2686
+ ),
2687
+ ) ?? null,
2688
+ });
2689
+ }
2690
+
2691
+ function emitError(error: InternalEditorError): void {
2692
+ const nextState: EditorState = {
2693
+ ...state,
2694
+ phase: error.isFatal ? "error" : state.phase,
2695
+ fatalError: error.isFatal ? error : state.fatalError,
2696
+ };
2697
+ state = nextState;
2698
+ storySelections.set(storyTargetKey(activeStory), state.selection);
2699
+ cachedRenderSnapshot = refreshRenderSnapshot();
2700
+ const publicError = toPublicError(error);
2701
+ options.onError?.(publicError);
2702
+ emit({
2703
+ type: "error",
2704
+ documentId: state.documentId,
2705
+ error: publicError,
2706
+ });
2707
+ for (const listener of listeners) {
2708
+ listener();
2709
+ }
2710
+ }
2711
+
2712
+ function switchActiveStory(target: EditorStoryTarget): void {
2713
+ const previousStory = activeStory;
2714
+ const previousSelection = state.selection;
2715
+ storySelections.set(storyTargetKey(previousStory), previousSelection);
2716
+
2717
+ const restoredSelection =
2718
+ storySelections.get(storyTargetKey(target)) ?? createSelectionSnapshot(0, 0);
2719
+ activeStory = target;
2720
+ state = {
2721
+ ...state,
2722
+ selection: restoredSelection,
2723
+ };
2724
+ storySelections.set(storyTargetKey(target), restoredSelection);
2725
+ cachedRenderSnapshot = refreshRenderSnapshot();
2726
+
2727
+ if (selectionChanged(previousSelection, restoredSelection)) {
2728
+ emit({
2729
+ type: "selection_changed",
2730
+ documentId: state.documentId,
2731
+ selection: toPublicSelectionSnapshot(restoredSelection, activeStory),
2732
+ });
2733
+ }
2734
+
2735
+ emit({
2736
+ type: "story_changed",
2737
+ documentId: state.documentId,
2738
+ activeStory,
2739
+ });
2740
+ for (const listener of listeners) {
2741
+ listener();
2742
+ }
2743
+ }
2744
+ }
2745
+
2746
+ function createSessionId(documentId: string, timestamp: string): string {
2747
+ return `session-${documentId}-${timestamp.replace(/[^0-9]/gu, "")}`;
2748
+ }
2749
+
2750
+ function createOrigin(
2751
+ source: CommandOrigin["source"],
2752
+ timestamp: string,
2753
+ ): CommandOrigin {
2754
+ return {
2755
+ source,
2756
+ timestamp,
2757
+ };
2758
+ }
2759
+
2760
+ function createEntityId(
2761
+ prefix: string,
2762
+ existing: Record<string, unknown>,
2763
+ timestamp: string,
2764
+ ): string {
2765
+ let counter = Object.keys(existing).length;
2766
+ let nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
2767
+
2768
+ while (existing[nextId]) {
2769
+ counter += 1;
2770
+ nextId = `${prefix}-${timestamp.replace(/[^0-9]/gu, "")}-${counter}`;
2771
+ }
2772
+
2773
+ return nextId;
2774
+ }
2775
+
2776
+ function finalizeState(
2777
+ state: EditorState,
2778
+ markDirty: boolean,
2779
+ timestamp: string,
2780
+ baseRevision?: number,
2781
+ ): EditorState {
2782
+ // Only increment revision on actual document mutations (markDirty=true).
2783
+ // Selection-only changes must not churn the revisionToken, which would
2784
+ // cause autosave/checkpoint dedup to treat cursor movement as new content.
2785
+ const revision = markDirty ? (baseRevision ?? state.revision) + 1 : state.revision;
2786
+
2787
+ return {
2788
+ ...state,
2789
+ document: {
2790
+ ...state.document,
2791
+ updatedAt: markDirty ? timestamp : state.document.updatedAt,
2792
+ },
2793
+ selection: state.selection,
2794
+ revision,
2795
+ revisionToken: `${state.sessionId}:${revision}`,
2796
+ isDirty: state.isDirty || markDirty,
2797
+ };
2798
+ }
2799
+
2800
+ function toRuntimeError(error: unknown): InternalEditorError {
2801
+ if (typeof error === "object" && error && "message" in error) {
2802
+ if (
2803
+ "code" in error
2804
+ && "source" in error
2805
+ && "isFatal" in error
2806
+ && typeof (error as { code?: unknown }).code === "string"
2807
+ && typeof (error as { source?: unknown }).source === "string"
2808
+ && typeof (error as { isFatal?: unknown }).isFatal === "boolean"
2809
+ ) {
2810
+ return {
2811
+ errorId:
2812
+ typeof (error as { errorId?: unknown }).errorId === "string"
2813
+ ? String((error as { errorId?: unknown }).errorId)
2814
+ : createSessionId("runtime-error", new Date().toISOString()),
2815
+ code: (error as { code: InternalEditorError["code"] }).code,
2816
+ isFatal: (error as { isFatal: boolean }).isFatal,
2817
+ message: String((error as { message?: unknown }).message ?? "Runtime error"),
2818
+ source: (error as { source: InternalEditorError["source"] }).source,
2819
+ details:
2820
+ typeof (error as { details?: unknown }).details === "object"
2821
+ && (error as { details?: unknown }).details !== null
2822
+ ? ((error as { details?: Record<string, unknown> }).details)
2823
+ : undefined,
2824
+ };
2825
+ }
2826
+ return {
2827
+ errorId: createSessionId("runtime-error", new Date().toISOString()),
2828
+ code: "internal_invariant",
2829
+ isFatal: false,
2830
+ message: String((error as { message?: unknown }).message ?? "Runtime error"),
2831
+ source: "runtime",
2832
+ };
2833
+ }
2834
+
2835
+ return {
2836
+ errorId: createSessionId("runtime-error", new Date().toISOString()),
2837
+ code: "internal_invariant",
2838
+ isFatal: false,
2839
+ message: "Runtime error",
2840
+ source: "runtime",
2841
+ };
2842
+ }
2843
+
2844
+ function toStructuredRuntimeException<T extends InternalEditorError>(
2845
+ error: T,
2846
+ ): Error & T {
2847
+ return Object.assign(new Error(error.message), error);
2848
+ }
2849
+
2850
+ function toPublicDocumentStats(state: Pick<EditorState, "document">) {
2851
+ const stats = deriveDocumentStats(state);
2852
+ return {
2853
+ storyLength: stats.characterCount,
2854
+ commentCount: stats.commentCount,
2855
+ revisionCount: stats.revisionCount,
2856
+ opaqueFragmentCount: countOpaqueFragments(state.document.preservation.opaqueFragments),
2857
+ };
2858
+ }
2859
+
2860
+ function toPublicSelectionSnapshot(
2861
+ selection: EditorState["selection"],
2862
+ storyTarget?: EditorStoryTarget,
2863
+ ): SelectionSnapshot {
2864
+ return {
2865
+ anchor: selection.anchor,
2866
+ head: selection.head,
2867
+ isCollapsed: selection.isCollapsed,
2868
+ activeRange: toPublicAnchorProjection(selection.activeRange),
2869
+ ...(storyTarget && storyTarget.kind !== "main" ? { storyTarget } : {}),
2870
+ };
2871
+ }
2872
+
2873
+ function toPublicAnchorProjection(
2874
+ anchor: InternalEditorAnchorProjection,
2875
+ ): EditorAnchorProjection {
2876
+ switch (anchor.kind) {
2877
+ case "range":
2878
+ return {
2879
+ kind: "range",
2880
+ from: anchor.range.from,
2881
+ to: anchor.range.to,
2882
+ assoc: anchor.assoc,
2883
+ };
2884
+ case "node":
2885
+ return {
2886
+ kind: "node",
2887
+ at: anchor.at,
2888
+ assoc: anchor.assoc,
2889
+ };
2890
+ case "detached":
2891
+ return {
2892
+ kind: "detached",
2893
+ lastKnownRange: anchor.lastKnownRange,
2894
+ reason: anchor.reason,
2895
+ };
2896
+ }
2897
+ }
2898
+
2899
+ function toInternalAnchorProjection(
2900
+ anchor: EditorAnchorProjection,
2901
+ ): InternalEditorAnchorProjection {
2902
+ switch (anchor.kind) {
2903
+ case "range":
2904
+ return createRangeAnchor(anchor.from, anchor.to, anchor.assoc);
2905
+ case "node":
2906
+ return createNodeAnchor(anchor.at, anchor.assoc);
2907
+ case "detached":
2908
+ return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
2909
+ }
2910
+ }
2911
+
2912
+ function createSelectionFromPublicAnchor(
2913
+ anchor: EditorAnchorProjection,
2914
+ ): import("../core/state/editor-state.ts").SelectionSnapshot {
2915
+ switch (anchor.kind) {
2916
+ case "range":
2917
+ return createSelectionSnapshot(anchor.from, anchor.to);
2918
+ case "node":
2919
+ return createSelectionSnapshot(anchor.at, anchor.at);
2920
+ case "detached":
2921
+ return createSelectionSnapshot(
2922
+ anchor.lastKnownRange.from,
2923
+ anchor.lastKnownRange.to,
2924
+ );
2925
+ }
2926
+ }
2927
+
2928
+ function toPublicCompatibilityReport(
2929
+ report: InternalCompatibilityReport,
2930
+ ): CompatibilityReport {
2931
+ return {
2932
+ reportVersion: report.reportVersion,
2933
+ generatedAt: report.generatedAt,
2934
+ blockExport: report.blockExport,
2935
+ featureEntries: report.featureEntries.map((entry) =>
2936
+ toPublicCompatibilityFeatureEntry(entry),
2937
+ ),
2938
+ warnings: report.warnings.map((warning) => toPublicWarning(warning)),
2939
+ errors: report.errors.map((error) => toPublicError(error)),
2940
+ };
2941
+ }
2942
+
2943
+ function toPublicCompatibilityFeatureEntry(
2944
+ entry: InternalCompatibilityFeatureEntry,
2945
+ ) {
2946
+ return {
2947
+ ...entry,
2948
+ affectedAnchor: entry.affectedAnchor
2949
+ ? toPublicAnchorProjection(entry.affectedAnchor)
2950
+ : undefined,
2951
+ };
2952
+ }
2953
+
2954
+ function toPublicWarning(warning: InternalEditorWarning): EditorWarning {
2955
+ return {
2956
+ ...warning,
2957
+ affectedAnchor: warning.affectedAnchor
2958
+ ? toPublicAnchorProjection(warning.affectedAnchor)
2959
+ : undefined,
2960
+ };
2961
+ }
2962
+
2963
+ function toPublicError(error: InternalEditorError): EditorError {
2964
+ return { ...error };
2965
+ }
2966
+
2967
+ function countOpaqueFragments(opaqueFragments: Record<string, unknown>): number {
2968
+ return Object.keys(opaqueFragments).length;
2969
+ }
2970
+
2971
+ function createDerivedCompatibility(state: EditorState): InternalCompatibilityReport {
2972
+ const derived = buildCompatibilityReport({
2973
+ document: state.document,
2974
+ warnings: state.warnings,
2975
+ fatalError: state.fatalError,
2976
+ generatedAt: state.document.updatedAt,
2977
+ });
2978
+
2979
+ return mergeCompatibilityReports([state.compatibility as never, derived as never], {
2980
+ generatedAt: state.document.updatedAt,
2981
+ blockExport: state.compatibility.blockExport || derived.blockExport,
2982
+ }) as InternalCompatibilityReport;
2983
+ }
2984
+
2985
+ function toPublicCommentSidebarSnapshot(
2986
+ state: EditorState,
2987
+ ): CommentSidebarSnapshot {
2988
+ const projection = createCommentSidebarProjection(
2989
+ createCommentStoreFromRuntimeComments(state.document.review.comments),
2990
+ state.runtime.activeCommentId,
2991
+ );
2992
+
2993
+ return {
2994
+ activeCommentId: state.runtime.activeCommentId,
2995
+ openCommentIds: projection.openCommentIds,
2996
+ resolvedCommentIds: projection.resolvedCommentIds,
2997
+ detachedCommentIds: projection.detachedCommentIds,
2998
+ totalCount: projection.totalCount,
2999
+ threads: projection.threads.map((thread): CommentSidebarThreadSnapshot => {
3000
+ const sourceThread = state.document.review.comments[thread.commentId];
3001
+ const projectedEntries =
3002
+ sourceThread?.entries?.map((entry) => ({
3003
+ entryId: entry.entryId,
3004
+ authorId: entry.authorId,
3005
+ body: entry.body,
3006
+ createdAt: entry.createdAt,
3007
+ })) ??
3008
+ (sourceThread?.body
3009
+ ? [
3010
+ {
3011
+ entryId: `${thread.commentId}-entry-1`,
3012
+ authorId:
3013
+ sourceThread.authorId ?? sourceThread.createdBy ?? "unknown",
3014
+ body: sourceThread.body,
3015
+ createdAt: sourceThread.createdAt,
3016
+ },
3017
+ ]
3018
+ : []);
3019
+
3020
+ return {
3021
+ commentId: thread.commentId,
3022
+ status: thread.status,
3023
+ anchor: toPublicAnchorProjection(
3024
+ sourceThread?.anchor ??
3025
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
3026
+ ),
3027
+ excerpt: thread.excerpt,
3028
+ entryCount: thread.entryCount,
3029
+ createdAt: thread.createdAt,
3030
+ createdBy: thread.createdBy,
3031
+ warningCount: thread.warningCount,
3032
+ anchorLabel: thread.anchorLabel,
3033
+ detachedReason: sourceThread?.metadata?.detachedReason,
3034
+ actionabilityNote: sourceThread?.metadata?.actionabilityNote,
3035
+ isActive: thread.isActive,
3036
+ resolvedAt: thread.resolvedAt,
3037
+ resolvedBy: thread.resolvedBy,
3038
+ entries: projectedEntries,
3039
+ };
3040
+ }),
3041
+ };
3042
+ }
3043
+
3044
+ function toPublicTrackedChangesSnapshot(
3045
+ state: EditorState,
3046
+ ): TrackedChangesSnapshot {
3047
+ const projection = createRevisionSidebarProjection(
3048
+ createRevisionStoreFromDocument(state),
3049
+ );
3050
+ const storyPlainTextCache = new Map<string, string>();
3051
+
3052
+ return {
3053
+ pendingChangeIds: projection.activeRevisionIds,
3054
+ acceptedChangeIds: projection.acceptedRevisionIds,
3055
+ rejectedChangeIds: projection.rejectedRevisionIds,
3056
+ detachedChangeIds: projection.detachedRevisionIds,
3057
+ actionableChangeIds: projection.actionableRevisionIds,
3058
+ preserveOnlyChangeIds: projection.preserveOnlyRevisionIds,
3059
+ totalCount: projection.totalCount,
3060
+ revisions: projection.revisions.map((revision): TrackedChangeEntrySnapshot => {
3061
+ const sourceRevision = state.document.review.revisions[revision.revisionId];
3062
+ const storyTarget = getRevisionStoryTarget(sourceRevision);
3063
+ const preview = describeRevisionPreview(
3064
+ revision,
3065
+ sourceRevision?.anchor ??
3066
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
3067
+ getStoryPlainText(state.document, storyTarget, storyPlainTextCache),
3068
+ );
3069
+
3070
+ return {
3071
+ revisionId: revision.revisionId,
3072
+ kind: revision.kind,
3073
+ source: sourceRevision?.metadata?.source ?? "runtime",
3074
+ suggestionId: sourceRevision?.metadata?.suggestionId,
3075
+ semanticKind: sourceRevision?.metadata?.semanticKind,
3076
+ linkedRevisionIds: sourceRevision?.metadata?.linkedRevisionIds,
3077
+ predecessorSuggestionId: sourceRevision?.metadata?.predecessorSuggestionId,
3078
+ label: revision.label,
3079
+ status: revision.status,
3080
+ actionability: revision.actionability,
3081
+ storyTarget,
3082
+ anchor: toPublicAnchorProjection(
3083
+ sourceRevision?.anchor ??
3084
+ createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
3085
+ ),
3086
+ anchorLabel: revision.anchorLabel,
3087
+ createdAt: revision.createdAt,
3088
+ authorId: revision.authorId,
3089
+ warningCount: revision.warningCount,
3090
+ canAccept: revision.canAccept,
3091
+ canReject: revision.canReject,
3092
+ importedRevisionForm: sourceRevision?.metadata?.importedRevisionForm,
3093
+ preserveOnlyReason: revision.preserveOnlyReason,
3094
+ excerpt: preview.excerpt,
3095
+ detail: preview.detail,
3096
+ };
3097
+ }),
3098
+ };
3099
+ }
3100
+
3101
+ function createRevisionStoreFromDocument(
3102
+ state: Pick<EditorState, "document">,
3103
+ ): RevisionStore {
3104
+ return {
3105
+ version: "revision-store/1",
3106
+ revisions: Object.fromEntries(
3107
+ Object.values(state.document.review.revisions).map((revision) => [
3108
+ revision.changeId,
3109
+ {
3110
+ revisionId: revision.changeId,
3111
+ kind: revision.kind,
3112
+ anchor: revision.anchor,
3113
+ authorId: revision.authorId ?? "unknown",
3114
+ createdAt: revision.createdAt,
3115
+ status:
3116
+ revision.status === "open"
3117
+ ? "active"
3118
+ : revision.status,
3119
+ warningIds: [...(revision.warningIds ?? [])],
3120
+ metadata: {
3121
+ source: revision.metadata?.source ?? "runtime",
3122
+ storyTarget: revision.metadata?.storyTarget,
3123
+ preserveOnlyReason: revision.metadata?.preserveOnlyReason,
3124
+ suggestionId: revision.metadata?.suggestionId,
3125
+ semanticKind: revision.metadata?.semanticKind,
3126
+ linkedRevisionIds: revision.metadata?.linkedRevisionIds,
3127
+ predecessorSuggestionId: revision.metadata?.predecessorSuggestionId,
3128
+ importedRevisionForm: revision.metadata?.importedRevisionForm,
3129
+ originalRevisionType: revision.metadata?.originalRevisionType,
3130
+ ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
3131
+ },
3132
+ },
3133
+ ]),
3134
+ ),
3135
+ };
3136
+ }
3137
+
3138
+ function getRevisionStoryTarget(
3139
+ revision: EditorState["document"]["review"]["revisions"][string] | undefined,
3140
+ ): EditorStoryTarget {
3141
+ const storyTarget = revision?.metadata?.storyTarget;
3142
+ return storyTarget ? { ...storyTarget } : MAIN_STORY_TARGET;
3143
+ }
3144
+
3145
+ function findSuggestionByChangeId(
3146
+ snapshot: SuggestionsSnapshot,
3147
+ changeId: string,
3148
+ ) {
3149
+ return snapshot.suggestions.find((suggestion) => suggestion.changeIds.includes(changeId));
3150
+ }
3151
+
3152
+ function createSecondaryStoryLocalReviewState(
3153
+ review: EditorState["document"]["review"],
3154
+ storyTarget: EditorStoryTarget,
3155
+ ): EditorState["document"]["review"] {
3156
+ return {
3157
+ comments: {},
3158
+ revisions: Object.fromEntries(
3159
+ Object.values(review.revisions)
3160
+ .filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
3161
+ .map((revision) => [
3162
+ revision.changeId,
3163
+ {
3164
+ ...revision,
3165
+ metadata: {
3166
+ ...revision.metadata,
3167
+ storyTarget: createRevisionStoryTargetRecord(storyTarget),
3168
+ },
3169
+ },
3170
+ ]),
3171
+ ),
3172
+ };
3173
+ }
3174
+
3175
+ function getStoryPlainText(
3176
+ document: CanonicalDocumentEnvelope,
3177
+ storyTarget: EditorStoryTarget,
3178
+ cache: Map<string, string>,
3179
+ ): string {
3180
+ const key = storyTargetKey(storyTarget);
3181
+ const cached = cache.get(key);
3182
+ if (cached !== undefined) {
3183
+ return cached;
3184
+ }
3185
+ const plainText = createEditorSurfaceSnapshot(
3186
+ document,
3187
+ createSelectionSnapshot(0, 0),
3188
+ storyTarget,
3189
+ ).plainText;
3190
+ cache.set(key, plainText);
3191
+ return plainText;
3192
+ }
3193
+
3194
+ function createRevisionStoryTargetRecord(
3195
+ storyTarget: EditorStoryTarget,
3196
+ ): NonNullable<NonNullable<EditorState["document"]["review"]["revisions"][string]["metadata"]>["storyTarget"]> {
3197
+ return { ...storyTarget };
3198
+ }
3199
+
3200
+ function listBlockExportReasons(
3201
+ report: InternalCompatibilityReport,
3202
+ ): string[] {
3203
+ return [
3204
+ ...report.featureEntries
3205
+ .filter((entry) => entry.featureClass === "unsupported-fatal")
3206
+ .map((entry) => entry.message),
3207
+ ...report.errors
3208
+ .filter((error) => error.isFatal)
3209
+ .map((error) => error.message),
3210
+ ];
3211
+ }
3212
+
3213
+ function describeRevisionPreview(
3214
+ revision: ReturnType<typeof createRevisionSidebarProjection>["revisions"][number],
3215
+ anchor: InternalEditorAnchorProjection,
3216
+ plainText: string,
3217
+ ): { excerpt: string; detail: string } {
3218
+ const { from, to } = toAnchorBounds(anchor);
3219
+ const excerpt = summarizeRevisionExcerpt(plainText, from, to, revision.label);
3220
+
3221
+ if (revision.actionability === "preserve-only") {
3222
+ return {
3223
+ excerpt,
3224
+ detail:
3225
+ revision.preserveOnlyReason ??
3226
+ "Visible for review, but this change remains preserve-only in the current runtime.",
3227
+ };
3228
+ }
3229
+
3230
+ if (revision.status === "accepted") {
3231
+ return {
3232
+ excerpt,
3233
+ detail: "Accepted in the live review runtime and retained here for audit visibility.",
3234
+ };
3235
+ }
3236
+
3237
+ if (revision.status === "rejected") {
3238
+ return {
3239
+ excerpt,
3240
+ detail: "Rejected in the live review runtime and retained here for audit visibility.",
3241
+ };
3242
+ }
3243
+
3244
+ return {
3245
+ excerpt,
3246
+ detail:
3247
+ revision.kind === "deletion"
3248
+ ? "Deleted content stays reviewable here until it is accepted or rejected."
3249
+ : "Runtime-backed change. Review it here or reopen the anchor in the canvas.",
3250
+ };
3251
+ }
3252
+
3253
+ function toAnchorBounds(anchor: InternalEditorAnchorProjection): { from: number; to: number } {
3254
+ switch (anchor.kind) {
3255
+ case "range":
3256
+ return {
3257
+ from: Math.min(anchor.range.from, anchor.range.to),
3258
+ to: Math.max(anchor.range.from, anchor.range.to),
3259
+ };
3260
+ case "node":
3261
+ return {
3262
+ from: anchor.at,
3263
+ to: anchor.at + 1,
3264
+ };
3265
+ case "detached":
3266
+ return {
3267
+ from: Math.min(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
3268
+ to: Math.max(anchor.lastKnownRange.from, anchor.lastKnownRange.to),
3269
+ };
3270
+ }
3271
+ }
3272
+
3273
+ function summarizeRevisionExcerpt(
3274
+ plainText: string,
3275
+ from: number,
3276
+ to: number,
3277
+ fallback: string,
3278
+ ): string {
3279
+ const normalizedFrom = Math.max(0, Math.min(from, plainText.length));
3280
+ const normalizedTo = Math.max(normalizedFrom, Math.min(to, plainText.length));
3281
+ const collapsed = plainText
3282
+ .slice(normalizedFrom, normalizedTo)
3283
+ .replace(/\s+/g, " ")
3284
+ .trim();
3285
+
3286
+ if (!collapsed) {
3287
+ return fallback;
3288
+ }
3289
+
3290
+ return collapsed.length > 96 ? `${collapsed.slice(0, 93)}...` : collapsed;
3291
+ }
3292
+
3293
+ function isValidStoryTarget(
3294
+ state: EditorState,
3295
+ target: EditorStoryTarget,
3296
+ ): boolean {
3297
+ if (target.kind === "main") return true;
3298
+ const subParts = state.document.subParts;
3299
+ if (!subParts) return false;
3300
+
3301
+ switch (target.kind) {
3302
+ case "header":
3303
+ return Boolean(normalizeHeaderFooterTarget(state.document, target));
3304
+ case "footer":
3305
+ return Boolean(normalizeHeaderFooterTarget(state.document, target));
3306
+ case "footnote":
3307
+ return Boolean(subParts.footnoteCollection?.footnotes?.[target.noteId]);
3308
+ case "endnote":
3309
+ return Boolean(subParts.footnoteCollection?.endnotes?.[target.noteId]);
3310
+ }
3311
+ }
3312
+
3313
+ function derivePageLayoutSnapshot(
3314
+ state: EditorState,
3315
+ activeStory: EditorStoryTarget,
3316
+ storySelections?: ReadonlyMap<string, EditorState["selection"]>,
3317
+ ): PageLayoutSnapshot | null {
3318
+ const subParts = state.document.subParts;
3319
+ const sections = buildResolvedSections(state.document);
3320
+ if (!subParts && sections.length === 0) {
3321
+ return null;
3322
+ }
3323
+
3324
+ const activeSection = resolveActiveSection(
3325
+ state,
3326
+ activeStory,
3327
+ sections,
3328
+ storySelections,
3329
+ );
3330
+ return buildPageLayoutSnapshot(
3331
+ activeSection?.index ?? 0,
3332
+ activeSection?.properties ?? subParts?.finalSectionProperties,
3333
+ subParts,
3334
+ );
3335
+ }
3336
+
3337
+ function isRecord(value: unknown): value is Record<string, unknown> {
3338
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3339
+ }
3340
+
3341
+ /** Commands that are safe in viewing mode (no document mutation). */
3342
+ const NON_MUTATION_COMMANDS = new Set([
3343
+ "selection.set",
3344
+ "runtime.set-read-only",
3345
+ "runtime.focus",
3346
+ "warning.add",
3347
+ "warning.clear",
3348
+ "comment.open",
3349
+ ]);
3350
+
3351
+ /** Mutation commands that are not yet supported in suggesting mode. */
3352
+ const SUGGESTING_UNSUPPORTED_COMMANDS = new Set<string>([]);
3353
+
3354
+ const SUGGESTING_SECONDARY_STORY_UNSUPPORTED_COMMANDS = new Set<string>([
3355
+ "text.insert",
3356
+ "text.delete-backward",
3357
+ "text.delete-forward",
3358
+ "text.insert-tab",
3359
+ "text.insert-hard-break",
3360
+ ]);
3361
+
3362
+ function isMutationCommand(command: EditorCommand): boolean {
3363
+ return !NON_MUTATION_COMMANDS.has(command.type);
3364
+ }
3365
+
3366
+ // ── Field snapshot helpers ──────────────────────────────────────────────────
3367
+
3368
+ function buildFieldSnapshot(document: CanonicalDocumentEnvelope): FieldSnapshot {
3369
+ const entries: FieldEntrySnapshot[] = [];
3370
+ let index = 0;
3371
+ for (const block of document.content.children) {
3372
+ index = collectFieldsFromBlock(block, entries, index);
3373
+ }
3374
+ index = collectFieldsFromSubParts(document.subParts, entries, index);
3375
+ const supportedCount = entries.filter((e) => e.supported).length;
3376
+ return {
3377
+ totalCount: entries.length,
3378
+ supportedCount,
3379
+ preserveOnlyCount: entries.length - supportedCount,
3380
+ fields: entries,
3381
+ };
3382
+ }
3383
+
3384
+ function collectFieldsFromBlock(
3385
+ block: BlockNode,
3386
+ entries: FieldEntrySnapshot[],
3387
+ index: number,
3388
+ ): number {
3389
+ if (block.type === "paragraph") {
3390
+ for (const child of block.children) {
3391
+ index = collectFieldsFromInline(child, entries, index);
3392
+ }
3393
+ } else if (block.type === "table") {
3394
+ for (const row of block.rows) {
3395
+ for (const cell of row.cells) {
3396
+ for (const child of cell.children) {
3397
+ index = collectFieldsFromBlock(child, entries, index);
3398
+ }
3399
+ }
3400
+ }
3401
+ } else if (block.type === "sdt" || block.type === "custom_xml") {
3402
+ for (const child of block.children) {
3403
+ index = collectFieldsFromBlock(child, entries, index);
3404
+ }
3405
+ }
3406
+ return index;
3407
+ }
3408
+
3409
+ function collectFieldsFromInline(
3410
+ node: InlineNode,
3411
+ entries: FieldEntrySnapshot[],
3412
+ index: number,
3413
+ ): number {
3414
+ if (node.type === "field") {
3415
+ const fieldFamily = node.fieldFamily ?? "UNKNOWN";
3416
+ const supported = isSupportedFieldFamily(fieldFamily);
3417
+ const displayText = extractFieldDisplayText(node);
3418
+ entries.push({
3419
+ index,
3420
+ fieldFamily,
3421
+ supported,
3422
+ instruction: node.instruction,
3423
+ fieldTarget: node.fieldTarget,
3424
+ refreshStatus: node.refreshStatus ?? (supported ? "stale" : "preserve-only"),
3425
+ displayText,
3426
+ });
3427
+ index++;
3428
+ // Also walk children — fields can contain nested fields
3429
+ for (const child of node.children) {
3430
+ index = collectFieldsFromInline(child, entries, index);
3431
+ }
3432
+ } else if (node.type === "hyperlink") {
3433
+ for (const child of node.children) {
3434
+ index = collectFieldsFromInline(child, entries, index);
3435
+ }
3436
+ }
3437
+ return index;
3438
+ }
3439
+
3440
+ function extractFieldDisplayText(field: FieldNode): string {
3441
+ return flattenInlineDisplayText(field.children);
3442
+ }
3443
+
3444
+ function flattenInlineDisplayText(children: readonly InlineNode[]): string {
3445
+ return children
3446
+ .map((child) => {
3447
+ switch (child.type) {
3448
+ case "text":
3449
+ return child.text;
3450
+ case "tab":
3451
+ return "\t";
3452
+ case "hard_break":
3453
+ case "column_break":
3454
+ return "\n";
3455
+ case "hyperlink":
3456
+ case "field":
3457
+ return flattenInlineDisplayText(child.children);
3458
+ case "footnote_ref":
3459
+ return child.noteId;
3460
+ default:
3461
+ return "";
3462
+ }
3463
+ })
3464
+ .join("");
3465
+ }
3466
+
3467
+ function refreshDocumentFields(
3468
+ document: CanonicalDocumentEnvelope,
3469
+ selectionHead: number,
3470
+ activeStory: EditorStoryTarget,
3471
+ options?: UpdateFieldsOptions,
3472
+ ): {
3473
+ document: CanonicalDocumentEnvelope;
3474
+ updatedCount: number;
3475
+ changed: boolean;
3476
+ protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
3477
+ } {
3478
+ const supportedOnly = options?.supportedOnly ?? true;
3479
+ const bookmarkMap = buildBookmarkNameMap(document);
3480
+ const paragraphs = collectParagraphContexts(document.content.children);
3481
+ const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
3482
+ let updatedCount = 0;
3483
+ let changed = false;
3484
+ let changedFrom: number | undefined;
3485
+ let changedTo: number | undefined;
3486
+
3487
+ const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
3488
+ if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
3489
+ return field;
3490
+ }
3491
+ if (supportedOnly && field.fieldFamily === "TOC") {
3492
+ return field;
3493
+ }
3494
+ const display = resolveSupportedFieldDisplay(
3495
+ field,
3496
+ document,
3497
+ bookmarkMap,
3498
+ paragraphs,
3499
+ navigation,
3500
+ );
3501
+ if (!display) {
3502
+ return field;
3503
+ }
3504
+ updatedCount += 1;
3505
+ const nextField: FieldNode = {
3506
+ ...field,
3507
+ children: buildInlineNodesFromDisplayText(display.displayText),
3508
+ refreshStatus: display.refreshStatus,
3509
+ };
3510
+ if (
3511
+ nextField.refreshStatus !== field.refreshStatus ||
3512
+ flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)
3513
+ ) {
3514
+ changed = true;
3515
+ changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
3516
+ changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
3517
+ }
3518
+ return nextField;
3519
+ }).blocks;
3520
+ if (!changed) {
3521
+ return { document, updatedCount, changed: false };
3522
+ }
3523
+
3524
+ const nextDocument: CanonicalDocumentEnvelope = {
3525
+ ...document,
3526
+ content: {
3527
+ ...document.content,
3528
+ children: nextChildren,
3529
+ },
3530
+ };
3531
+ const nextRegistry = buildFieldRegistry({
3532
+ content: nextDocument.content,
3533
+ styles: nextDocument.styles,
3534
+ subParts: nextDocument.subParts,
3535
+ });
3536
+ nextDocument.fieldRegistry = nextRegistry;
3537
+ let protectionSelection:
3538
+ | import("../core/state/editor-state.ts").SelectionSnapshot
3539
+ | undefined;
3540
+ if (changedFrom !== undefined && changedTo !== undefined) {
3541
+ protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
3542
+ }
3543
+ return {
3544
+ document: nextDocument,
3545
+ updatedCount,
3546
+ changed: true,
3547
+ ...(protectionSelection ? { protectionSelection } : {}),
3548
+ };
3549
+ }
3550
+
3551
+ function refreshDocumentTableOfContents(
3552
+ document: CanonicalDocumentEnvelope,
3553
+ selectionHead: number,
3554
+ activeStory: EditorStoryTarget,
3555
+ options?: TocRefreshOptions,
3556
+ ): {
3557
+ document: CanonicalDocumentEnvelope;
3558
+ result: TocRefreshResult;
3559
+ changed: boolean;
3560
+ protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
3561
+ } {
3562
+ const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
3563
+ let changed = false;
3564
+ let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
3565
+ let changedFrom: number | undefined;
3566
+ let changedTo: number | undefined;
3567
+ const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
3568
+ if (field.fieldFamily !== "TOC") {
3569
+ return field;
3570
+ }
3571
+ const levelRange = options?.maxLevel
3572
+ ? { from: 1, to: options.maxLevel }
3573
+ : parseTocLevelRange(field.instruction);
3574
+ const entries = navigation.headings
3575
+ .filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
3576
+ .map((heading) => ({
3577
+ level: heading.level,
3578
+ text: heading.text,
3579
+ pageIndex: heading.pageIndex,
3580
+ }));
3581
+ if (resultEntries.length === 0) {
3582
+ resultEntries = entries;
3583
+ }
3584
+ const nextField: FieldNode = {
3585
+ ...field,
3586
+ children: buildTocInlineNodes(entries),
3587
+ refreshStatus: "current",
3588
+ };
3589
+ if (flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)) {
3590
+ changed = true;
3591
+ changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
3592
+ changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
3593
+ }
3594
+ return nextField;
3595
+ }).blocks;
3596
+ if (!changed) {
3597
+ return {
3598
+ document,
3599
+ result: { entryCount: resultEntries.length, entries: resultEntries },
3600
+ changed: false,
3601
+ };
3602
+ }
3603
+
3604
+ const nextDocument: CanonicalDocumentEnvelope = {
3605
+ ...document,
3606
+ content: {
3607
+ ...document.content,
3608
+ children: nextChildren,
3609
+ },
3610
+ };
3611
+ const nextRegistry = buildFieldRegistry({
3612
+ content: nextDocument.content,
3613
+ styles: nextDocument.styles,
3614
+ subParts: nextDocument.subParts,
3615
+ });
3616
+ nextDocument.fieldRegistry = nextRegistry.tocStructure
3617
+ ? {
3618
+ ...nextRegistry,
3619
+ tocStructure: {
3620
+ ...nextRegistry.tocStructure,
3621
+ status: "current",
3622
+ },
3623
+ }
3624
+ : nextRegistry;
3625
+ let protectionSelection:
3626
+ | import("../core/state/editor-state.ts").SelectionSnapshot
3627
+ | undefined;
3628
+ if (changedFrom !== undefined && changedTo !== undefined) {
3629
+ protectionSelection = createSelectionSnapshot(changedFrom, changedTo);
3630
+ }
3631
+
3632
+ return {
3633
+ document: nextDocument,
3634
+ result: { entryCount: resultEntries.length, entries: resultEntries },
3635
+ changed: true,
3636
+ ...(protectionSelection ? { protectionSelection } : {}),
3637
+ };
3638
+ }
3639
+
3640
+ function refreshBlocksWithCursor(
3641
+ blocks: readonly BlockNode[],
3642
+ visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
3643
+ cursor = 0,
3644
+ previousParagraph = false,
3645
+ ): {
3646
+ blocks: BlockNode[];
3647
+ cursor: number;
3648
+ previousParagraph: boolean;
3649
+ } {
3650
+ const nextBlocks = blocks.map((block) => {
3651
+ if (block.type === "paragraph") {
3652
+ const paragraphStart = previousParagraph ? cursor + 1 : cursor;
3653
+ const refreshedChildren = refreshInlineNodesWithCursor(
3654
+ block.children,
3655
+ visitField,
3656
+ paragraphStart,
3657
+ );
3658
+ cursor = paragraphStart + refreshedChildren.cursor;
3659
+ previousParagraph = true;
3660
+ return {
3661
+ ...block,
3662
+ children: refreshedChildren.nodes,
3663
+ };
3664
+ }
3665
+ if (block.type === "table") {
3666
+ cursor += 1;
3667
+ previousParagraph = false;
3668
+ return {
3669
+ ...block,
3670
+ rows: block.rows.map((row) => ({
3671
+ ...row,
3672
+ cells: row.cells.map((cell) => ({
3673
+ ...cell,
3674
+ children: (() => {
3675
+ const refreshed = refreshBlocksWithCursor(cell.children, visitField, cursor, false);
3676
+ cursor = refreshed.cursor;
3677
+ return refreshed.blocks;
3678
+ })(),
3679
+ })),
3680
+ })),
3681
+ };
3682
+ }
3683
+ if (block.type === "sdt" || block.type === "custom_xml") {
3684
+ const refreshed = refreshBlocksWithCursor(
3685
+ block.children,
3686
+ visitField,
3687
+ cursor,
3688
+ previousParagraph,
3689
+ );
3690
+ cursor = refreshed.cursor;
3691
+ previousParagraph = refreshed.previousParagraph;
3692
+ return {
3693
+ ...block,
3694
+ children: refreshed.blocks,
3695
+ };
3696
+ }
3697
+ cursor += 1;
3698
+ previousParagraph = false;
3699
+ return block;
3700
+ });
3701
+ return { blocks: nextBlocks, cursor, previousParagraph };
3702
+ }
3703
+
3704
+ function refreshInlineNodesWithCursor(
3705
+ nodes: readonly InlineNode[],
3706
+ visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
3707
+ cursor = 0,
3708
+ ): {
3709
+ nodes: InlineNode[];
3710
+ cursor: number;
3711
+ } {
3712
+ const nextNodes = nodes.map((node) => {
3713
+ if (node.type === "field") {
3714
+ const fieldStart = cursor;
3715
+ const refreshedChildren = refreshInlineNodesWithCursor(node.children, visitField, cursor);
3716
+ const fieldLength = measureInlineNodes(node.children);
3717
+ cursor = fieldStart + fieldLength;
3718
+ return visitField({
3719
+ ...node,
3720
+ children: refreshedChildren.nodes,
3721
+ }, {
3722
+ from: fieldStart,
3723
+ to: fieldStart + fieldLength,
3724
+ });
3725
+ }
3726
+ if (node.type === "hyperlink") {
3727
+ cursor += measureInlineNodes(node.children);
3728
+ return {
3729
+ ...node,
3730
+ // Hyperlinks only contain text-like children in the canonical model.
3731
+ children: [...node.children],
3732
+ };
3733
+ }
3734
+ cursor += measureInlineNode(node);
3735
+ return node;
3736
+ });
3737
+ return { nodes: nextNodes, cursor };
3738
+ }
3739
+
3740
+ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
3741
+ if (text.length === 0) {
3742
+ return [];
3743
+ }
3744
+ const children: InlineNode[] = [];
3745
+ let buffer = "";
3746
+ const flushBuffer = () => {
3747
+ if (buffer.length > 0) {
3748
+ children.push({ type: "text", text: buffer });
3749
+ buffer = "";
3750
+ }
3751
+ };
3752
+ for (const character of text) {
3753
+ if (character === "\t") {
3754
+ flushBuffer();
3755
+ children.push({ type: "tab" });
3756
+ continue;
3757
+ }
3758
+ if (character === "\n") {
3759
+ flushBuffer();
3760
+ children.push({ type: "hard_break" });
3761
+ continue;
3762
+ }
3763
+ buffer += character;
3764
+ }
3765
+ flushBuffer();
3766
+ return children;
3767
+ }
3768
+
3769
+ function buildTocInlineNodes(
3770
+ entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
3771
+ ): InlineNode[] {
3772
+ const children: InlineNode[] = [];
3773
+ entries.forEach((entry, index) => {
3774
+ children.push({ type: "text", text: entry.text });
3775
+ children.push({ type: "tab" });
3776
+ children.push({ type: "text", text: String(entry.pageIndex + 1) });
3777
+ if (index < entries.length - 1) {
3778
+ children.push({ type: "hard_break" });
3779
+ }
3780
+ });
3781
+ return children;
3782
+ }
3783
+
3784
+ function collectFieldsFromSubParts(
3785
+ subParts: SubPartsCatalog | undefined,
3786
+ entries: FieldEntrySnapshot[],
3787
+ index: number,
3788
+ ): number {
3789
+ if (!subParts) {
3790
+ return index;
3791
+ }
3792
+ let nextIndex = index;
3793
+ for (const header of subParts.headers ?? []) {
3794
+ for (const block of header.blocks) {
3795
+ nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
3796
+ }
3797
+ }
3798
+ for (const footer of subParts.footers ?? []) {
3799
+ for (const block of footer.blocks) {
3800
+ nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
3801
+ }
3802
+ }
3803
+ if (subParts.footnoteCollection) {
3804
+ for (const note of Object.values(subParts.footnoteCollection.footnotes)) {
3805
+ for (const block of note.blocks) {
3806
+ nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
3807
+ }
3808
+ }
3809
+ for (const note of Object.values(subParts.footnoteCollection.endnotes)) {
3810
+ for (const block of note.blocks) {
3811
+ nextIndex = collectFieldsFromBlock(block, entries, nextIndex);
3812
+ }
3813
+ }
3814
+ }
3815
+ return nextIndex;
3816
+ }
3817
+
3818
+ function resolveSupportedFieldDisplay(
3819
+ field: FieldNode,
3820
+ document: CanonicalDocumentEnvelope,
3821
+ bookmarkMap: Map<string, { bookmarkId: string; paragraphIndex: number }>,
3822
+ paragraphs: readonly ParagraphContext[],
3823
+ navigation: DocumentNavigationSnapshot,
3824
+ ): { displayText: string; refreshStatus: FieldRefreshStatus } | undefined {
3825
+ if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
3826
+ return undefined;
3827
+ }
3828
+ if (!field.fieldTarget) {
3829
+ return field.fieldFamily === "TOC"
3830
+ ? undefined
3831
+ : { displayText: "", refreshStatus: "unresolvable" };
3832
+ }
3833
+ if (field.fieldFamily === "REF") {
3834
+ const result = resolveRefFieldText(document, bookmarkMap, field.fieldTarget);
3835
+ return result
3836
+ ? { displayText: result.text, refreshStatus: result.refreshStatus }
3837
+ : { displayText: "", refreshStatus: "unresolvable" };
3838
+ }
3839
+ const bookmark = bookmarkMap.get(field.fieldTarget);
3840
+ if (!bookmark) {
3841
+ return { displayText: "", refreshStatus: "unresolvable" };
3842
+ }
3843
+ if (field.fieldFamily === "PAGEREF") {
3844
+ const paragraph = paragraphs[bookmark.paragraphIndex];
3845
+ if (!paragraph) {
3846
+ return { displayText: "", refreshStatus: "unresolvable" };
3847
+ }
3848
+ const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
3849
+ return { displayText: String(pageIndex + 1), refreshStatus: "current" };
3850
+ }
3851
+ if (field.fieldFamily === "NOTEREF") {
3852
+ const paragraph = paragraphs[bookmark.paragraphIndex]?.paragraph;
3853
+ if (!paragraph) {
3854
+ return { displayText: "", refreshStatus: "unresolvable" };
3855
+ }
3856
+ const noteText = resolveNoteReferenceText(paragraph, bookmark.bookmarkId);
3857
+ return noteText
3858
+ ? { displayText: noteText, refreshStatus: "current" }
3859
+ : { displayText: "", refreshStatus: "unresolvable" };
3860
+ }
3861
+ return undefined;
3862
+ }
3863
+
3864
+ interface ParagraphContext {
3865
+ paragraph: ParagraphNode;
3866
+ startOffset: number;
3867
+ }
3868
+
3869
+ function collectParagraphContexts(blocks: readonly BlockNode[]): ParagraphContext[] {
3870
+ const paragraphs: ParagraphContext[] = [];
3871
+ collectParagraphContextsFromBlocks(blocks, paragraphs, 0, false);
3872
+ return paragraphs;
3873
+ }
3874
+
3875
+ function collectParagraphContextsFromBlocks(
3876
+ blocks: readonly BlockNode[],
3877
+ paragraphs: ParagraphContext[],
3878
+ cursor: number,
3879
+ previousParagraph: boolean,
3880
+ ): { cursor: number; previousParagraph: boolean } {
3881
+ let nextCursor = cursor;
3882
+ let nextPreviousParagraph = previousParagraph;
3883
+ for (const block of blocks) {
3884
+ if (block.type === "paragraph") {
3885
+ if (nextPreviousParagraph) {
3886
+ nextCursor += 1;
3887
+ }
3888
+ paragraphs.push({ paragraph: block, startOffset: nextCursor });
3889
+ nextCursor += measureInlineNodes(block.children);
3890
+ nextPreviousParagraph = true;
3891
+ continue;
3892
+ }
3893
+ if (block.type === "table") {
3894
+ nextCursor += 1;
3895
+ nextPreviousParagraph = false;
3896
+ for (const row of block.rows) {
3897
+ for (const cell of row.cells) {
3898
+ const result = collectParagraphContextsFromBlocks(
3899
+ cell.children,
3900
+ paragraphs,
3901
+ nextCursor,
3902
+ false,
3903
+ );
3904
+ nextCursor = result.cursor;
3905
+ }
3906
+ }
3907
+ continue;
3908
+ }
3909
+ if (block.type === "sdt" || block.type === "custom_xml") {
3910
+ const result = collectParagraphContextsFromBlocks(
3911
+ block.children,
3912
+ paragraphs,
3913
+ nextCursor,
3914
+ nextPreviousParagraph,
3915
+ );
3916
+ nextCursor = result.cursor;
3917
+ nextPreviousParagraph = result.previousParagraph;
3918
+ continue;
3919
+ }
3920
+ nextCursor += 1;
3921
+ nextPreviousParagraph = false;
3922
+ }
3923
+ return { cursor: nextCursor, previousParagraph: nextPreviousParagraph };
3924
+ }
3925
+
3926
+ function measureInlineNodes(nodes: readonly InlineNode[]): number {
3927
+ return nodes.reduce((size, node) => size + measureInlineNode(node), 0);
3928
+ }
3929
+
3930
+ function measureInlineNode(node: InlineNode): number {
3931
+ switch (node.type) {
3932
+ case "text":
3933
+ return node.text.length;
3934
+ case "tab":
3935
+ case "hard_break":
3936
+ case "column_break":
3937
+ case "footnote_ref":
3938
+ case "image":
3939
+ case "opaque_inline":
3940
+ case "bookmark_start":
3941
+ case "bookmark_end":
3942
+ return 1;
3943
+ case "hyperlink":
3944
+ case "field":
3945
+ return measureInlineNodes(node.children);
3946
+ default:
3947
+ return 1;
3948
+ }
3949
+ }
3950
+
3951
+ function resolveNoteReferenceText(paragraph: ParagraphNode, bookmarkId: string): string | undefined {
3952
+ let inside = false;
3953
+ let sawBoundary = false;
3954
+ for (const child of paragraph.children) {
3955
+ if (child.type === "bookmark_start" && child.bookmarkId === bookmarkId) {
3956
+ inside = true;
3957
+ sawBoundary = true;
3958
+ continue;
3959
+ }
3960
+ if (child.type === "bookmark_end" && child.bookmarkId === bookmarkId) {
3961
+ break;
3962
+ }
3963
+ if (!inside) {
3964
+ continue;
3965
+ }
3966
+ if (child.type === "footnote_ref") {
3967
+ return child.noteId;
3968
+ }
3969
+ }
3970
+ return sawBoundary ? undefined : undefined;
3971
+ }
3972
+
3973
+ function getCommandSelection(
3974
+ command: EditorCommand,
3975
+ fallbackSelection: import("../core/state/editor-state.ts").SelectionSnapshot,
3976
+ ): import("../core/state/editor-state.ts").SelectionSnapshot {
3977
+ if ("protectionSelection" in command && command.protectionSelection) {
3978
+ return command.protectionSelection;
3979
+ }
3980
+ if ("selection" in command && command.selection) {
3981
+ return command.selection;
3982
+ }
3983
+ return fallbackSelection;
3984
+ }
3985
+
3986
+ function isBlockedByProtection(
3987
+ protection: ProtectionSnapshot,
3988
+ selection: import("../core/state/editor-state.ts").SelectionSnapshot,
3989
+ ): boolean {
3990
+ const enforcedRanges = protection.ranges.filter(
3991
+ (range): range is typeof range & { start: number; end: number } =>
3992
+ range.enforced && typeof range.start === "number" && typeof range.end === "number",
3993
+ );
3994
+ if (enforcedRanges.length === 0) {
3995
+ return false;
3996
+ }
3997
+ const from = Math.min(selection.anchor, selection.head);
3998
+ const to = Math.max(selection.anchor, selection.head);
3999
+ return !enforcedRanges.some((range) =>
4000
+ from >= range.start && to <= range.end,
4001
+ );
4002
+ }
4003
+
4004
+ function remapProtectionSnapshot(
4005
+ protection: ProtectionSnapshot,
4006
+ mapping: import("../core/selection/mapping.ts").TransactionMapping,
4007
+ ): ProtectionSnapshot {
4008
+ if (mapping.steps.length === 0 || protection.ranges.length === 0) {
4009
+ return protection;
4010
+ }
4011
+ let changed = false;
4012
+ const nextRanges = protection.ranges.map((range) => {
4013
+ if (
4014
+ !range.enforced ||
4015
+ typeof range.start !== "number" ||
4016
+ typeof range.end !== "number"
4017
+ ) {
4018
+ return range;
4019
+ }
4020
+ const mapped = mapRange(
4021
+ { from: range.start, to: range.end },
4022
+ { start: -1, end: 1 },
4023
+ mapping,
4024
+ );
4025
+ if (mapped.kind === "detached") {
4026
+ changed = true;
4027
+ return {
4028
+ ...range,
4029
+ start: undefined,
4030
+ end: undefined,
4031
+ enforced: false,
4032
+ enforcementReason:
4033
+ "preserve-only: permission range could not be remapped after runtime edits",
4034
+ };
4035
+ }
4036
+ if (mapped.range.from !== range.start || mapped.range.to !== range.end) {
4037
+ changed = true;
4038
+ return {
4039
+ ...range,
4040
+ start: mapped.range.from,
4041
+ end: mapped.range.to,
4042
+ };
4043
+ }
4044
+ return range;
4045
+ });
4046
+ if (!changed) {
4047
+ return protection;
4048
+ }
4049
+ return {
4050
+ ...protection,
4051
+ ranges: nextRanges,
4052
+ enforcedRangeCount: nextRanges.filter((range) => range.enforced).length,
4053
+ preservedRangeCount: nextRanges.filter((range) => !range.enforced).length,
4054
+ };
4055
+ }