@beyondwork/docx-react-component 1.0.66 → 1.0.69

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 (384) hide show
  1. package/README.md +75 -931
  2. package/package.json +26 -27
  3. package/src/api/anchor-conversion.ts +43 -0
  4. package/src/api/editor-state-types.ts +2 -1
  5. package/src/api/public-types.ts +504 -101
  6. package/src/api/session-state.ts +4 -0
  7. package/src/api/v3/README.md +91 -0
  8. package/src/api/v3/_create.ts +146 -0
  9. package/src/api/v3/_layer-metadata.ts +362 -0
  10. package/src/api/v3/_mocks.ts +84 -0
  11. package/src/api/v3/_runtime-handle.ts +162 -0
  12. package/src/api/v3/_ux-response.ts +73 -0
  13. package/src/api/v3/ai/_metadata-audit.ts +225 -0
  14. package/src/api/v3/ai/attach.ts +235 -0
  15. package/src/api/v3/ai/bundle.ts +132 -0
  16. package/src/api/v3/ai/explain.ts +144 -0
  17. package/src/api/v3/ai/export.ts +54 -0
  18. package/src/api/v3/ai/inspect.ts +118 -0
  19. package/src/api/v3/ai/policy.ts +77 -0
  20. package/src/api/v3/ai/replacement.ts +341 -0
  21. package/src/api/v3/ai/resolve.ts +133 -0
  22. package/src/api/v3/index.ts +79 -0
  23. package/src/api/v3/runtime/chart.ts +310 -0
  24. package/src/api/v3/runtime/clipboard.ts +81 -0
  25. package/src/api/v3/runtime/collab.ts +331 -0
  26. package/src/api/v3/runtime/content.ts +236 -0
  27. package/src/api/v3/runtime/document.ts +282 -0
  28. package/src/api/v3/runtime/formatting.ts +186 -0
  29. package/src/api/v3/runtime/geometry.ts +349 -0
  30. package/src/api/v3/runtime/layout.ts +108 -0
  31. package/src/api/v3/runtime/review.ts +129 -0
  32. package/src/api/v3/runtime/search.ts +74 -0
  33. package/src/api/v3/runtime/table.ts +63 -0
  34. package/src/api/v3/runtime/workflow.ts +434 -0
  35. package/src/api/v3/ui/_context.ts +86 -0
  36. package/src/api/v3/ui/_create.ts +65 -0
  37. package/src/api/v3/ui/_types.ts +520 -0
  38. package/src/api/v3/ui/chrome-composition.ts +342 -0
  39. package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
  40. package/src/api/v3/ui/chrome.ts +476 -0
  41. package/src/api/v3/ui/debug.ts +124 -0
  42. package/src/api/v3/ui/index.ts +64 -0
  43. package/src/api/v3/ui/overlays-visibility.ts +170 -0
  44. package/src/api/v3/ui/overlays.ts +427 -0
  45. package/src/api/v3/ui/scope.ts +71 -0
  46. package/src/api/v3/ui/session.ts +100 -0
  47. package/src/api/v3/ui/surface.ts +170 -0
  48. package/src/api/v3/ui/viewport.ts +303 -0
  49. package/src/core/commands/index.ts +28 -6
  50. package/src/core/commands/list-commands.ts +3 -2
  51. package/src/core/commands/section-layout-commands.ts +9 -8
  52. package/src/core/schema/text-schema.ts +16 -0
  53. package/src/core/selection/mapping.ts +33 -72
  54. package/src/core/state/editor-state.ts +96 -189
  55. package/src/index.ts +23 -4
  56. package/src/io/chart-preview-resolver.ts +1 -1
  57. package/src/io/docx-session.ts +36 -4795
  58. package/src/io/export/build-app-properties-xml.ts +1 -1
  59. package/src/io/export/serialize-comments.ts +1 -1
  60. package/src/io/export/serialize-headers-footers.ts +6 -1
  61. package/src/io/export/serialize-main-document.ts +45 -0
  62. package/src/io/export/serialize-run-formatting.ts +17 -2
  63. package/src/io/export/twip.ts +1 -1
  64. package/src/io/normalize/normalize-text.ts +27 -20
  65. package/src/io/ooxml/chart/parse-series.ts +1 -1
  66. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  67. package/src/io/ooxml/chart/types.ts +1 -1
  68. package/src/io/ooxml/classify-embedding.ts +83 -33
  69. package/src/io/ooxml/parse-fill.ts +1 -1
  70. package/src/io/ooxml/parse-main-document.ts +71 -1
  71. package/src/io/ooxml/parse-object.ts +14 -10
  72. package/src/io/ooxml/parse-run-formatting.ts +47 -1
  73. package/src/io/ooxml/property-grab-bag.ts +2 -2
  74. package/src/io/ooxml/units.ts +11 -0
  75. package/src/io/ooxml/workflow-payload.ts +282 -7
  76. package/src/model/anchor.ts +85 -0
  77. package/src/model/canonical-document.ts +351 -15
  78. package/src/model/chart-types.ts +1 -1
  79. package/src/model/layout/index.ts +83 -0
  80. package/src/model/layout/page-graph-types.ts +181 -0
  81. package/src/model/layout/page-layout-snapshot.ts +105 -0
  82. package/src/model/layout/resolved-layout-types.ts +47 -0
  83. package/src/model/layout/runtime-page-graph-types.ts +102 -0
  84. package/src/model/paragraph-scope-ids.ts +72 -0
  85. package/src/model/review/comment-types.ts +112 -0
  86. package/src/model/review/index.ts +2 -0
  87. package/src/model/review/revision-types.ts +215 -0
  88. package/src/model/snapshot.ts +32 -0
  89. package/src/review/store/comment-store.ts +21 -47
  90. package/src/review/store/revision-types.ts +40 -198
  91. package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
  92. package/src/runtime/collab/runtime-collab-sync.ts +13 -3
  93. package/src/runtime/collab-session.ts +1 -1
  94. package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
  95. package/src/runtime/debug/event-ring-buffer.ts +64 -0
  96. package/src/runtime/debug/probability-sampler.ts +18 -0
  97. package/src/runtime/debug/runtime-debug-facet.ts +67 -0
  98. package/src/runtime/debug/stage-tokens.ts +31 -0
  99. package/src/runtime/debug/telemetry-bus.ts +271 -0
  100. package/src/runtime/debug/types.ts +275 -0
  101. package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
  102. package/src/runtime/document-layout.ts +8 -6
  103. package/src/runtime/document-runtime.ts +843 -1141
  104. package/src/runtime/document-search.ts +1 -1
  105. package/src/runtime/edit-ops/index.ts +1 -1
  106. package/src/runtime/external-send-runtime.ts +1 -1
  107. package/src/runtime/formatting/document-lookup.ts +235 -0
  108. package/src/runtime/formatting/field/registry.ts +41 -0
  109. package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
  110. package/src/runtime/formatting/font-resolution.ts +83 -0
  111. package/src/runtime/formatting/formatting-context.ts +903 -0
  112. package/src/runtime/formatting/formatting-types.ts +157 -0
  113. package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
  114. package/src/runtime/formatting/index.ts +125 -0
  115. package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
  116. package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
  117. package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
  118. package/src/runtime/formatting/projector.ts +75 -0
  119. package/src/runtime/formatting/resolve-effective.ts +407 -0
  120. package/src/runtime/formatting/revision-display.ts +105 -0
  121. package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
  122. package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
  123. package/src/runtime/formatting/telemetry-bridge.ts +106 -0
  124. package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
  125. package/src/runtime/geometry/caret-geometry.ts +164 -0
  126. package/src/runtime/geometry/geometry-facet.ts +364 -0
  127. package/src/runtime/geometry/geometry-types.ts +256 -0
  128. package/src/runtime/geometry/hit-test.ts +125 -0
  129. package/src/runtime/geometry/index.ts +71 -0
  130. package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
  131. package/src/runtime/geometry/invalidation.ts +35 -0
  132. package/src/runtime/geometry/object-handles.ts +77 -0
  133. package/src/runtime/geometry/overlay-rects.ts +85 -0
  134. package/src/runtime/geometry/project-anchors.ts +100 -0
  135. package/src/runtime/geometry/project-fragments.ts +216 -0
  136. package/src/runtime/geometry/projector.ts +129 -0
  137. package/src/runtime/geometry/replacement-envelope.ts +130 -0
  138. package/src/runtime/geometry/viewport.ts +218 -0
  139. package/src/runtime/layout/compat-input-ledger.ts +211 -0
  140. package/src/runtime/layout/index.ts +6 -1
  141. package/src/runtime/layout/inert-layout-facet.ts +12 -7
  142. package/src/runtime/layout/layout-engine-instance.ts +189 -11
  143. package/src/runtime/layout/layout-engine-version.ts +450 -1
  144. package/src/runtime/layout/layout-facet-types.ts +60 -0
  145. package/src/runtime/layout/layout-measurement-provider.ts +13 -0
  146. package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
  147. package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
  148. package/src/runtime/layout/page-graph.ts +62 -209
  149. package/src/runtime/layout/page-story-resolver.ts +7 -12
  150. package/src/runtime/layout/paginated-layout-engine.ts +186 -11
  151. package/src/runtime/layout/project-block-fragments.ts +11 -0
  152. package/src/runtime/layout/projector.ts +90 -0
  153. package/src/runtime/layout/public-facet.ts +187 -442
  154. package/src/runtime/layout/resolved-formatting-state.ts +158 -26
  155. package/src/runtime/layout/table-render-plan.ts +1 -1
  156. package/src/runtime/prerender/cache-envelope.ts +6 -1
  157. package/src/runtime/prerender/prerender-document.ts +18 -23
  158. package/src/runtime/render/decoration-resolver.ts +1 -1
  159. package/src/runtime/render/render-frame-types.ts +20 -0
  160. package/src/runtime/render/render-kernel.ts +94 -25
  161. package/src/runtime/scopes/_formatting-seam.ts +262 -0
  162. package/src/runtime/scopes/_scope-dependencies.ts +49 -0
  163. package/src/runtime/scopes/action-validation.ts +356 -0
  164. package/src/runtime/scopes/attach-explanation.ts +102 -0
  165. package/src/runtime/scopes/audit-bundle.ts +71 -0
  166. package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
  167. package/src/runtime/scopes/compile-scope.ts +262 -0
  168. package/src/runtime/scopes/compiler-service.ts +431 -0
  169. package/src/runtime/scopes/create-issue.ts +107 -0
  170. package/src/runtime/scopes/enumerate-scopes.ts +543 -0
  171. package/src/runtime/scopes/evidence.ts +233 -0
  172. package/src/runtime/scopes/index.ts +150 -0
  173. package/src/runtime/scopes/position-map.ts +214 -0
  174. package/src/runtime/scopes/preservation-boundary.ts +91 -0
  175. package/src/runtime/scopes/projector.ts +49 -0
  176. package/src/runtime/scopes/replaceability.ts +87 -0
  177. package/src/runtime/scopes/replacement/apply.ts +228 -0
  178. package/src/runtime/scopes/replacement/compile.ts +59 -0
  179. package/src/runtime/scopes/replacement/propose.ts +42 -0
  180. package/src/runtime/scopes/resolve-reference.ts +347 -0
  181. package/src/runtime/scopes/review-bundle.ts +141 -0
  182. package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
  183. package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
  184. package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
  185. package/src/runtime/scopes/scope-kinds/field.ts +65 -0
  186. package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
  187. package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
  188. package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
  189. package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
  190. package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
  191. package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
  192. package/src/runtime/scopes/scope-kinds/table.ts +55 -0
  193. package/src/runtime/scopes/scope-range.ts +208 -0
  194. package/src/runtime/scopes/semantic-scope-types.ts +454 -0
  195. package/src/runtime/scopes/workflow-overlap.ts +92 -0
  196. package/src/runtime/selection/index.ts +1 -1
  197. package/src/runtime/structure-ops/fragment-insert.ts +1 -1
  198. package/src/runtime/structure-ops/index.ts +1 -1
  199. package/src/runtime/surface-projection.ts +232 -262
  200. package/src/runtime/units.ts +4 -2
  201. package/src/runtime/workflow/coordinator.ts +1348 -0
  202. package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
  203. package/src/runtime/workflow/index.ts +25 -0
  204. package/src/runtime/workflow/markup-mode-policy.ts +98 -0
  205. package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
  206. package/src/runtime/workflow/metadata-persistence.ts +306 -0
  207. package/src/runtime/workflow/metadata-writer.ts +123 -0
  208. package/src/runtime/workflow/overlay-store.ts +690 -0
  209. package/src/runtime/workflow/projector.ts +127 -0
  210. package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
  211. package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
  212. package/src/runtime/workflow/rail/types.ts +198 -0
  213. package/src/runtime/workflow/scope-rail-composer.ts +39 -0
  214. package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
  215. package/src/runtime/workflow/scope-writer.ts +188 -0
  216. package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
  217. package/src/runtime/workflow/visibility-policy.ts +129 -0
  218. package/src/session/_sync-legacy.ts +66 -0
  219. package/src/session/export/embedded-reconstitute.ts +104 -0
  220. package/src/session/export/export-diagnostics.ts +85 -0
  221. package/src/session/export/export-validation.ts +110 -0
  222. package/src/session/export/index.ts +34 -0
  223. package/src/session/export/preservation-reattach.ts +30 -0
  224. package/src/session/export/serialize-dispatch.ts +165 -0
  225. package/src/session/export/stateful-export-pipeline.ts +432 -0
  226. package/src/session/export/stateful-export.ts +684 -0
  227. package/src/session/import/canonical-assembly.ts +227 -0
  228. package/src/session/import/diagnostics-session.ts +54 -0
  229. package/src/session/import/embedded-discovery.ts +225 -0
  230. package/src/session/import/embedded-offload.ts +337 -0
  231. package/src/session/import/import-diagnostics.ts +69 -0
  232. package/src/session/import/loader-types.ts +313 -0
  233. package/src/session/import/loader.ts +1834 -0
  234. package/src/session/import/normalize.ts +195 -0
  235. package/src/session/import/package-parts.ts +217 -0
  236. package/src/session/import/package-read.ts +195 -0
  237. package/src/session/import/parse-orchestration.ts +105 -0
  238. package/src/session/import/part-constants.ts +70 -0
  239. package/src/session/import/part-discovery.ts +94 -0
  240. package/src/session/import/preservation-index.ts +46 -0
  241. package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
  242. package/src/session/import/review-import.ts +508 -0
  243. package/src/session/import/styles-consolidation.ts +281 -0
  244. package/src/session/import/workflow-scope-import.ts +256 -0
  245. package/src/session/index.ts +37 -0
  246. package/src/session/session-state.ts +69 -0
  247. package/src/session/session.ts +532 -0
  248. package/src/session/shared/protection.ts +228 -0
  249. package/src/session/shared/session-utils.ts +82 -0
  250. package/src/session/types.ts +499 -0
  251. package/src/shell/chart-snapshots.ts +96 -0
  252. package/src/shell/media-previews.ts +85 -0
  253. package/src/shell/overlay-anchor-bridge.ts +53 -0
  254. package/src/shell/paste-adapter.ts +23 -0
  255. package/src/shell/ref-commands.ts +1697 -0
  256. package/src/shell/ref-utilities.ts +48 -0
  257. package/src/shell/search.ts +51 -0
  258. package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
  259. package/src/shell/ui-subscriber-channels.ts +81 -0
  260. package/src/shell/use-collab-sync.ts +116 -0
  261. package/src/ui/WordReviewEditor.tsx +496 -2051
  262. package/src/ui/editor-shell-view.tsx +30 -1
  263. package/src/ui/editor-surface-controller.tsx +49 -1
  264. package/src/ui/headless/revision-decoration-model.ts +83 -0
  265. package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
  266. package/src/ui/headless/scoped-chrome-policy.ts +2 -2
  267. package/src/ui/headless/selection-tool-context.ts +1 -1
  268. package/src/ui/headless/selection-tool-resolver.ts +1 -1
  269. package/src/ui/runtime-shortcut-dispatch.ts +46 -1
  270. package/src/ui/ui-controller-factory.ts +221 -0
  271. package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
  272. package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
  273. package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
  274. package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
  275. package/src/ui-tailwind/chart/render/area.tsx +3 -3
  276. package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
  277. package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
  278. package/src/ui-tailwind/chart/render/combo.tsx +2 -2
  279. package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
  280. package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
  281. package/src/ui-tailwind/chart/render/line.tsx +3 -3
  282. package/src/ui-tailwind/chart/render/pie.tsx +6 -6
  283. package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
  284. package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
  285. package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
  286. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
  287. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
  288. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
  289. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
  290. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
  291. package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
  292. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
  293. package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
  294. package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
  295. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
  296. package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
  297. package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
  298. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
  299. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
  300. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
  301. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
  302. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
  303. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
  304. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
  305. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
  306. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
  307. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
  308. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
  309. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
  310. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
  311. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
  312. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
  313. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
  314. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
  315. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
  316. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
  317. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
  318. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
  319. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
  320. package/src/ui-tailwind/debug/README.md +57 -0
  321. package/src/ui-tailwind/debug/index.ts +3 -0
  322. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
  323. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
  324. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
  325. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
  326. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
  327. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
  328. package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
  329. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
  330. package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
  331. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
  332. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
  333. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
  334. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
  335. package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
  336. package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
  337. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
  338. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
  339. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
  340. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
  341. package/src/ui-tailwind/index.ts +0 -5
  342. package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
  343. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
  344. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
  345. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
  346. package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
  347. package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
  348. package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
  349. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
  350. package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
  351. package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
  352. package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
  353. package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
  354. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
  355. package/src/ui-tailwind/review-workspace/types.ts +408 -0
  356. package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
  357. package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
  358. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
  359. package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
  360. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
  361. package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
  362. package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
  363. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
  364. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
  365. package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
  366. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
  367. package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
  368. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
  369. package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
  370. package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
  371. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
  372. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
  373. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  374. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
  375. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
  376. package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
  377. package/src/ui-tailwind/ui-api-context.tsx +43 -0
  378. package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
  379. package/src/validation/compatibility-engine.ts +6 -6
  380. package/src/runtime/styles-cascade.ts +0 -33
  381. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
  382. /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
  383. /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
  384. /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
@@ -0,0 +1,903 @@
1
+ /**
2
+ * Layer 03 — production formatting orchestrator (`FormattingContext`).
3
+ *
4
+ * The authoritative entry point for the hot path. One context is built per
5
+ * canonical-document projection pass (`surface-projection`, layout resolve
6
+ * pass, etc.); it internally owns the per-document resolvers that the
7
+ * layer previously forced every consumer to construct on their own:
8
+ *
9
+ * - `ThemeColorResolver` (theme-color.ts)
10
+ * - `NumberingPrefixResolver` (numbering/prefix.ts, stateful counters)
11
+ * - the `StylesCatalog` for cascade walks
12
+ * - the `FontTable` for ECMA-376 §17.3.2.26 font precedence
13
+ * - the `FieldResolver` (when a page-graph is threaded in)
14
+ *
15
+ * Every method below is a production boundary call. The earlier
16
+ * `resolveEffectiveFormatting(doc, nodeRef)` API is a thin façade built on
17
+ * top of this context so tests + external callers exercise the same code
18
+ * path as the hot production path.
19
+ *
20
+ * Architecture contract F1: every consumer that needs effective formatting
21
+ * flows through this context (directly, or via
22
+ * `resolveEffectiveFormatting`). Scattered direct calls to sub-resolvers
23
+ * from non-Layer-03 code are rejected by
24
+ * `scripts/ci-check-formatting-layer-purity.mjs` (guards under
25
+ * `src/runtime/formatting/`) and by
26
+ * `scripts/ci-check-formatting-production-boundary.mjs` (guards the
27
+ * production consumers that migrated).
28
+ *
29
+ * Per F2, the context holds NO document state — it only caches the
30
+ * resolvers built from the input document. Caller is responsible for
31
+ * discarding the context when the document changes.
32
+ */
33
+
34
+ import type {
35
+ CanonicalDocument,
36
+ CanonicalFontTable,
37
+ CanonicalParagraphFormatting,
38
+ CanonicalRunFormatting,
39
+ CanonicalTheme,
40
+ FieldRegistryEntry,
41
+ HyperlinkNode,
42
+ InlineNode,
43
+ ParagraphNode,
44
+ RevisionRecord,
45
+ TableNode,
46
+ TableStyleConditionalRegion,
47
+ } from "../../model/canonical-document.ts";
48
+ import type { FieldPageGraph, FieldResolver, ResolvedField } from "./field/resolver.ts";
49
+ import { createFieldResolver } from "./field/resolver.ts";
50
+ import { ThemeColorResolver, concretizeThemeColors } from "./theme-color.ts";
51
+ import type { NumberingPrefixResolver, NumberingPrefixResult } from "./numbering/prefix.ts";
52
+ import { createNumberingPrefixResolver } from "./numbering/prefix.ts";
53
+ import {
54
+ resolveEffectiveParagraphFormatting,
55
+ resolveEffectiveRunFormatting,
56
+ resolveNumberingMarkerRunFormatting,
57
+ resolveTableCellTextFormatting,
58
+ type RunResolveInput,
59
+ } from "./style-cascade.ts";
60
+ import { resolveParagraphStyleChain } from "./paragraph-style-resolver.ts";
61
+ import { resolveTableStyleResolution, type ResolvedTableStyleResolution } from "./table-style-resolver.ts";
62
+ import { resolveHyperlinkRunFormatting } from "./hyperlink-color.ts";
63
+ import { resolveRunFontFamily } from "./font-resolution.ts";
64
+ import { applyRevisionDisplay } from "./revision-display.ts";
65
+ import type {
66
+ EffectiveFieldDisplay,
67
+ EffectiveNumbering,
68
+ EffectiveParagraphFormatting,
69
+ EffectiveRunFormatting,
70
+ } from "./formatting-types.ts";
71
+
72
+ /**
73
+ * Direct-text-mark direct formatting. Mirrors (and replaces) the
74
+ * `buildDirectRunFormattingFromMarks` helper that previously lived
75
+ * inside `surface-projection.ts`. The production hot path passes
76
+ * projected marks + markAttrs in; `resolveEffectiveFormatting(doc,
77
+ * nodeRef)` callers use `resolveDirectRunFormattingAtSegment` on a
78
+ * canonical `TextNode` instead.
79
+ */
80
+ /**
81
+ * Projected run marks — a string-literal union a size wider than the
82
+ * canonical direct-formatting projection. The extra marks (`superscript`,
83
+ * `subscript`, `emboss`, `imprint`, `shadow`) are preserved through the
84
+ * surface snapshot but have no direct-formatting projection into
85
+ * `CanonicalRunFormatting`; they flow through the surface marks array
86
+ * untouched. The context ignores them.
87
+ */
88
+ export type ProjectedSurfaceMark =
89
+ | "bold"
90
+ | "italic"
91
+ | "underline"
92
+ | "strikethrough"
93
+ | "doubleStrikethrough"
94
+ | "superscript"
95
+ | "subscript"
96
+ | "vanish"
97
+ | "emboss"
98
+ | "imprint"
99
+ | "shadow"
100
+ | "smallCaps"
101
+ | "allCaps";
102
+
103
+ export interface ProjectedRunMarks {
104
+ readonly marks?: ReadonlyArray<ProjectedSurfaceMark>;
105
+ readonly markAttrs?: {
106
+ readonly backgroundColor?: string;
107
+ readonly charSpacing?: number;
108
+ readonly kerning?: number;
109
+ readonly textFill?: string;
110
+ readonly fontFamily?: string;
111
+ readonly fontSize?: number;
112
+ readonly textColor?: string;
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Numbering resolution result shape returned to the production consumer.
118
+ * Kept structurally compatible with the legacy `NumberingPrefixResult`
119
+ * from `./numbering/prefix.ts` so surface-projection can keep its
120
+ * downstream adapters unchanged.
121
+ */
122
+ export type NumberingResolution = NumberingPrefixResult;
123
+
124
+ /**
125
+ * Cascade-tier provenance for a run resolution. Research doc
126
+ * `ResolvedProperty { value, source, sourceId }` maps onto this shape:
127
+ * each populated field points at the tier that won, plus (for style
128
+ * tiers) the styleId whose rPr carried the winning value.
129
+ *
130
+ * `source: "direct"` ≥ `source: "characterStyle"` ≥
131
+ * `source: "style"` ≥ `source: "docDefaults"` — highest priority wins.
132
+ */
133
+ export interface RunResolvedProperty<TValue = unknown> {
134
+ readonly value: TValue;
135
+ readonly source: "direct" | "characterStyle" | "style" | "docDefaults";
136
+ /** Winning styleId for `style` / `characterStyle` sources. */
137
+ readonly sourceId?: string;
138
+ }
139
+
140
+ export interface RunProvenance {
141
+ readonly run: EffectiveRunFormatting;
142
+ readonly properties: { readonly [K in keyof CanonicalRunFormatting]?: RunResolvedProperty };
143
+ }
144
+
145
+ export interface FormattingContextOptions {
146
+ readonly fontTable?: CanonicalFontTable;
147
+ /** Active markup mode. When set, per-run resolution attaches
148
+ * `revisionDisplay` flags via `applyRevisionDisplay`. */
149
+ readonly revisionMarkupMode?: "clean" | "simple" | "all";
150
+ /**
151
+ * Live markup-mode callback. When set, it is consulted on every
152
+ * per-run resolve — composed L06 `workflow.getMarkupModePolicy()`
153
+ * (class-A document default) with L10 `ui.viewport.getEffectiveMarkupMode()`
154
+ * (class-C user preference) drive the active mode without the
155
+ * context being rebuilt. Precedence: callback result (if not
156
+ * `undefined`) wins over the static `revisionMarkupMode` field.
157
+ *
158
+ * The layer itself stays F6-neutral (applies posture, does not decide
159
+ * the mode — see `03-formatting-semantics.md` §F6). When the callback
160
+ * returns `undefined`, no posture is applied on that pass (same as
161
+ * the static field being unset).
162
+ */
163
+ readonly getEffectiveMarkupMode?: () =>
164
+ | "clean"
165
+ | "simple"
166
+ | "all"
167
+ | undefined;
168
+ /** Author-id → color palette for revision-display posture. */
169
+ readonly authorColorPalette?: ReadonlyMap<string, string>;
170
+ /**
171
+ * Page-graph reference for field resolution. When absent, the field
172
+ * resolver is not built and field dispatches return
173
+ * `refreshStatus: "unresolved"` with the registry's cached displayText.
174
+ */
175
+ readonly pageGraph?: FieldPageGraph;
176
+ /** Active page index for PAGE field resolution. Ignored when
177
+ * `pageGraph` is absent. */
178
+ readonly activePageIndex?: number;
179
+ /**
180
+ * Optional pre-built bookmark map keyed on bookmark name. The field
181
+ * resolver needs it for REF / PAGEREF. The context accepts it so
182
+ * consumers that already compute bookmarks don't duplicate the work.
183
+ */
184
+ readonly bookmarkMap?: Map<string, { bookmarkId: string; paragraphIndex: number }>;
185
+ /** Per-paragraph cursor offsets used by REF/PAGEREF relative-position
186
+ * resolution. */
187
+ readonly paragraphOffsets?: readonly number[];
188
+ }
189
+
190
+ export interface FormattingContext {
191
+ readonly doc: CanonicalDocument;
192
+ readonly theme: ThemeColorResolver | undefined;
193
+
194
+ /** Build (or reuse) the stateful numbering prefix resolver. */
195
+ readonly numbering: NumberingPrefixResolver;
196
+
197
+ /** Lazy field resolver; undefined until a page graph is threaded via opts. */
198
+ readonly field: FieldResolver | undefined;
199
+
200
+ /**
201
+ * Return the first open revision whose anchor range covers the
202
+ * half-open segment `[from, to)`, or `undefined` when no revision is
203
+ * attached. Pre-computed once per document from
204
+ * `doc.review.revisions`; consumers call this per segment to obtain
205
+ * the attached revision that `resolveRunFromMarks({ revision })` will
206
+ * use for F6 posture application.
207
+ *
208
+ * Accepted / rejected / detached revisions are ignored — F6's markup
209
+ * modes only change how *open* revisions render.
210
+ */
211
+ findRevisionAtRange(from: number, to: number): RevisionRecord | undefined;
212
+
213
+ /**
214
+ * Full paragraph cascade: docDefaults → style chain → direct. Returns
215
+ * the flat canonical shape for downstream consumers that want to read
216
+ * individual fields directly.
217
+ */
218
+ resolveParagraphCascade(para: ParagraphNode): CanonicalParagraphFormatting;
219
+
220
+ /**
221
+ * Per-paragraph "effective numbering" — inherits from
222
+ * `paragraph.numbering` or (when missing) walks the paragraph-style
223
+ * chain for a style-linked numbering reference. Mirrors the
224
+ * `resolveEffectiveParagraphNumbering` logic that previously lived in
225
+ * `surface-projection.ts` and moves it into L03 ownership.
226
+ */
227
+ resolveEffectiveParagraphNumbering(para: ParagraphNode): ParagraphNode["numbering"] | undefined;
228
+
229
+ /**
230
+ * Resolve the per-paragraph numbering prefix + geometry (marker,
231
+ * indent, tab) for rendering. Returns `null` when the paragraph has
232
+ * no numbering. Advances the stateful counter when `advance = true`
233
+ * (the default).
234
+ *
235
+ * For culled paragraphs on the viewport edge, callers pass
236
+ * `advance: true, emitGeometry: false` so the counter stays in sync
237
+ * without paying the geometry-projection cost.
238
+ */
239
+ resolveParagraphNumbering(
240
+ para: ParagraphNode,
241
+ options?: { advance?: boolean; emitGeometry?: boolean },
242
+ ): NumberingResolution | null;
243
+
244
+ /**
245
+ * Per-paragraph numbering-marker rPr cascade (ECMA-376 §17.9).
246
+ * Mirrors `resolveNumberingMarkerRunFormatting` with the layer's
247
+ * owned style catalog.
248
+ */
249
+ resolveNumberingMarkerRunFormatting(
250
+ paragraphStyleId: string | undefined,
251
+ levelRunProperties: CanonicalRunFormatting | undefined,
252
+ ): CanonicalRunFormatting;
253
+
254
+ /**
255
+ * Production hot-path run cascade. Accepts the surface-projected marks
256
+ * + markAttrs representation and does:
257
+ * 1. `buildDirectRunFormattingFromMarks` equivalent → direct rPr.
258
+ * 2. docDefaults → paragraph-style chain → character-style chain →
259
+ * direct cascade.
260
+ * 3. Hyperlink-color cascade when the run is inside a `HyperlinkNode`.
261
+ * 4. Theme-color concretization via the layer-owned `ThemeColorResolver`.
262
+ * 5. Revision-display posture application when opts configured.
263
+ *
264
+ * The return value is `EffectiveRunFormatting` — the same type the
265
+ * entry-point `resolveEffectiveFormatting` produces; consumers should
266
+ * treat them as interchangeable. When revision posture is attached,
267
+ * `run.revisionDisplay` is populated.
268
+ */
269
+ resolveRunFromMarks(input: {
270
+ paragraphStyleId: string | undefined;
271
+ marks: ProjectedRunMarks | undefined;
272
+ inHyperlink: boolean;
273
+ characterStyleId?: string | undefined;
274
+ revision?: RevisionRecord;
275
+ }): EffectiveRunFormatting;
276
+
277
+ /**
278
+ * Pure canonical-inline-oriented run cascade. Used by the
279
+ * `resolveEffectiveFormatting(doc, nodeRef)` façade when callers pass
280
+ * a `TextNode` with canonical `marks` instead of the projected form.
281
+ */
282
+ resolveRunFromCanonicalInline(input: {
283
+ paragraphStyleId: string | undefined;
284
+ characterStyleId: string | undefined;
285
+ direct: CanonicalRunFormatting | undefined;
286
+ inHyperlink: boolean;
287
+ revision?: RevisionRecord;
288
+ }): EffectiveRunFormatting;
289
+
290
+ /**
291
+ * Table-style resolution; delegates to the pure resolver and exposes
292
+ * the same shape. Kept on the context so production consumers
293
+ * (surface-projection, layout public-facet) import from one place.
294
+ */
295
+ resolveTable(table: TableNode): ResolvedTableStyleResolution;
296
+
297
+ /**
298
+ * Cell-text cascade floor (docDefaults → table-style chain →
299
+ * optional conditional-region layering). Paragraph + run direct
300
+ * formatting from the cell's content layers on top via the normal
301
+ * paragraph/run cascades.
302
+ *
303
+ * When `activeRegions` is supplied (from `getActiveCellRegions()`
304
+ * via `resolveTable(table).rows[i].cells[j].activeConditionalRegions`)
305
+ * the cascade layers each region's `tblStylePr/pPr` + `rPr` after the
306
+ * basedOn-chain walk — fixes the L03 pre-A1 gap where banded tables
307
+ * and header rows received no region-level paragraph/run properties.
308
+ * Borders / shading / margins were already region-aware via
309
+ * `mergeStyleFormattingCell`; this brings text formatting to parity.
310
+ */
311
+ resolveTableCellFloor(
312
+ tableStyleId: string | undefined,
313
+ activeRegions?: readonly TableStyleConditionalRegion[],
314
+ ): { paragraph: CanonicalParagraphFormatting; run: CanonicalRunFormatting };
315
+
316
+ /** Resolve a registered field entry. Returns `undefined` when the
317
+ * context has no page-graph (so the resolver was never built). */
318
+ resolveField(entry: FieldRegistryEntry): ResolvedField | undefined;
319
+
320
+ /** Resolve the effective font family for a run following
321
+ * ECMA-376 §17.3.2.26 precedence + theme-minor fallback. */
322
+ resolveFontFamily(input: RunResolveInput, themeMinorFont?: string): string | undefined;
323
+
324
+ /** Build an `EffectiveParagraphFormatting` wrapper (cascade +
325
+ * numbering + paragraphMarkRun) for the entry-point façade. */
326
+ resolveParagraph(para: ParagraphNode): EffectiveParagraphFormatting;
327
+
328
+ /**
329
+ * Layer-03 §C (provenance). Resolve the run cascade AND return a
330
+ * per-field source map — for each populated field, name the cascade
331
+ * tier that won the resolution: `"docDefaults"`, `"style"` (with the
332
+ * winning styleId), `"characterStyle"` (ditto), or `"direct"`.
333
+ *
334
+ * This powers agent explanations ("this bold came from the Heading 1
335
+ * style, not from direct formatting"). It is NOT called on the hot
336
+ * path — production consumers use `resolveRunFromCanonicalInline`
337
+ * which pays zero provenance cost.
338
+ *
339
+ * Research doc `ResolvedProperty { value, source, sourceId }` maps
340
+ * directly onto the return shape here.
341
+ */
342
+ resolveRunWithProvenance(input: {
343
+ paragraphStyleId: string | undefined;
344
+ characterStyleId: string | undefined;
345
+ direct: CanonicalRunFormatting | undefined;
346
+ }): RunProvenance;
347
+
348
+ /** Resolve an `EffectiveFieldDisplay` shape for an inline field. */
349
+ resolveFieldDisplay(
350
+ entry: FieldRegistryEntry | undefined,
351
+ cachedDisplayText: string | undefined,
352
+ ): EffectiveFieldDisplay;
353
+ }
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Implementation
357
+ // ---------------------------------------------------------------------------
358
+
359
+ class FormattingContextImpl implements FormattingContext {
360
+ readonly doc: CanonicalDocument;
361
+ readonly theme: ThemeColorResolver | undefined;
362
+ readonly numbering: NumberingPrefixResolver;
363
+ readonly field: FieldResolver | undefined;
364
+ private readonly opts: FormattingContextOptions;
365
+ // F6 revision-range index. Built once per context lifetime (O(R)),
366
+ // consulted per segment (O(R) linear in the common case — the document
367
+ // has tens of open revisions, not thousands, so a sorted walk is fine).
368
+ // `null` when the caller did not configure revision markup, so the
369
+ // segment walk pays zero cost.
370
+ private readonly revisionIndex:
371
+ | ReadonlyArray<{ from: number; to: number; revision: RevisionRecord }>
372
+ | null;
373
+ // Paragraph-cascade memoization (D). Keyed by the weak `para` reference
374
+ // so cache frees automatically when the paragraph is GC'd. Hot-path
375
+ // surface-projection calls `resolveParagraphCascade` on the same
376
+ // paragraph via `resolveParagraph` + numbering resolve + marker-rPr;
377
+ // each of those re-walked the style catalog before.
378
+ private readonly paragraphCascadeCache: WeakMap<
379
+ ParagraphNode,
380
+ CanonicalParagraphFormatting
381
+ > = new WeakMap();
382
+
383
+ constructor(doc: CanonicalDocument, opts: FormattingContextOptions) {
384
+ this.doc = doc;
385
+ this.opts = opts;
386
+ const canonicalTheme: CanonicalTheme | undefined = doc.subParts?.canonicalTheme;
387
+ this.theme = canonicalTheme ? new ThemeColorResolver(canonicalTheme) : undefined;
388
+ this.numbering = createNumberingPrefixResolver(doc.numbering);
389
+ this.field =
390
+ opts.pageGraph !== undefined
391
+ ? createFieldResolver({
392
+ pageGraph: opts.pageGraph,
393
+ activePageIndex: opts.activePageIndex ?? 0,
394
+ bookmarkMap: opts.bookmarkMap ?? new Map(),
395
+ paragraphOffsets: opts.paragraphOffsets ?? [],
396
+ styles: doc.styles,
397
+ contentRoot: doc.content,
398
+ })
399
+ : undefined;
400
+ this.revisionIndex =
401
+ opts.revisionMarkupMode !== undefined ||
402
+ opts.getEffectiveMarkupMode !== undefined
403
+ ? buildRevisionRangeIndex(doc)
404
+ : null;
405
+ }
406
+
407
+ findRevisionAtRange(from: number, to: number): RevisionRecord | undefined {
408
+ const index = this.revisionIndex;
409
+ if (!index) return undefined;
410
+ for (const entry of index) {
411
+ if (entry.from > to) break;
412
+ if (entry.from <= from && entry.to >= to) return entry.revision;
413
+ if (entry.from <= from && entry.to > from) return entry.revision;
414
+ }
415
+ return undefined;
416
+ }
417
+
418
+ resolveParagraphCascade(para: ParagraphNode): CanonicalParagraphFormatting {
419
+ const cached = this.paragraphCascadeCache.get(para);
420
+ if (cached !== undefined) return cached;
421
+ const resolved = resolveEffectiveParagraphFormatting(
422
+ {
423
+ styleId: para.styleId,
424
+ direct: extractDirectParagraphFormatting(para),
425
+ },
426
+ this.doc.styles,
427
+ );
428
+ this.paragraphCascadeCache.set(para, resolved);
429
+ return resolved;
430
+ }
431
+
432
+ resolveEffectiveParagraphNumbering(
433
+ para: ParagraphNode,
434
+ ): ParagraphNode["numbering"] | undefined {
435
+ if (para.numbering) return para.numbering;
436
+ if (!para.styleId) return undefined;
437
+ const paragraphStyles = this.doc.styles?.paragraphs ?? {};
438
+ const styleChain = resolveParagraphStyleChain(para.styleId, this.doc.styles);
439
+ let styleNumbering: { numberingInstanceId: string; level?: number } | undefined;
440
+ for (const styleId of styleChain) {
441
+ const style = paragraphStyles[styleId];
442
+ if (style?.numbering) {
443
+ styleNumbering = style.numbering;
444
+ break;
445
+ }
446
+ }
447
+ if (!styleNumbering) return undefined;
448
+ if (styleNumbering.level !== undefined) {
449
+ return {
450
+ numberingInstanceId: styleNumbering.numberingInstanceId,
451
+ level: styleNumbering.level,
452
+ };
453
+ }
454
+ const resolvedLevel = resolveStyleLinkedNumberingLevel(
455
+ this.doc,
456
+ styleNumbering.numberingInstanceId,
457
+ styleChain,
458
+ );
459
+ return resolvedLevel !== undefined
460
+ ? {
461
+ numberingInstanceId: styleNumbering.numberingInstanceId,
462
+ level: resolvedLevel,
463
+ }
464
+ : undefined;
465
+ }
466
+
467
+ resolveParagraphNumbering(
468
+ para: ParagraphNode,
469
+ options: { advance?: boolean; emitGeometry?: boolean } = {},
470
+ ): NumberingResolution | null {
471
+ const advance = options.advance !== false;
472
+ const emitGeometry = options.emitGeometry !== false;
473
+ const effectiveNumbering = this.resolveEffectiveParagraphNumbering(para);
474
+ if (!effectiveNumbering) return null;
475
+ if (!emitGeometry) {
476
+ // Advance the counter but do not project geometry.
477
+ if (advance) this.numbering.resolve(effectiveNumbering);
478
+ return null;
479
+ }
480
+ if (!advance) {
481
+ // The underlying resolver is stateful and does not expose a
482
+ // "peek" primitive. When the caller needs the read without
483
+ // mutation, they must fork the context; for now, treat as a
484
+ // contract violation in a dev assertion.
485
+ // eslint-disable-next-line no-console -- dev-time only
486
+ console.warn("[formatting-context] resolveParagraphNumbering({advance:false}) is not supported; counter always advances");
487
+ }
488
+ return this.numbering.resolveDetailed(effectiveNumbering, para);
489
+ }
490
+
491
+ resolveNumberingMarkerRunFormatting(
492
+ paragraphStyleId: string | undefined,
493
+ levelRunProperties: CanonicalRunFormatting | undefined,
494
+ ): CanonicalRunFormatting {
495
+ return resolveNumberingMarkerRunFormatting(
496
+ { paragraphStyleId, levelRunProperties },
497
+ this.doc.styles,
498
+ );
499
+ }
500
+
501
+ resolveRunFromMarks(input: {
502
+ paragraphStyleId: string | undefined;
503
+ marks: ProjectedRunMarks | undefined;
504
+ inHyperlink: boolean;
505
+ characterStyleId?: string | undefined;
506
+ revision?: RevisionRecord;
507
+ }): EffectiveRunFormatting {
508
+ const direct = buildDirectRunFormattingFromProjected(input.marks);
509
+ return this.resolveRunCore({
510
+ paragraphStyleId: input.paragraphStyleId,
511
+ characterStyleId: input.characterStyleId,
512
+ direct,
513
+ inHyperlink: input.inHyperlink,
514
+ revision: input.revision,
515
+ });
516
+ }
517
+
518
+ resolveRunFromCanonicalInline(input: {
519
+ paragraphStyleId: string | undefined;
520
+ characterStyleId: string | undefined;
521
+ direct: CanonicalRunFormatting | undefined;
522
+ inHyperlink: boolean;
523
+ revision?: RevisionRecord;
524
+ }): EffectiveRunFormatting {
525
+ return this.resolveRunCore(input);
526
+ }
527
+
528
+ private resolveRunCore(input: {
529
+ paragraphStyleId: string | undefined;
530
+ characterStyleId: string | undefined;
531
+ direct: CanonicalRunFormatting | undefined;
532
+ inHyperlink: boolean;
533
+ revision?: RevisionRecord;
534
+ }): EffectiveRunFormatting {
535
+ const resolveInput: RunResolveInput = {
536
+ paragraphStyleId: input.paragraphStyleId,
537
+ characterStyleId: input.characterStyleId,
538
+ direct: input.direct,
539
+ };
540
+ const cascade = input.inHyperlink
541
+ ? resolveHyperlinkRunFormatting(resolveInput, this.doc.styles, this.theme)
542
+ : this.theme
543
+ ? concretizeThemeColors(
544
+ resolveEffectiveRunFormatting(resolveInput, this.doc.styles),
545
+ this.theme,
546
+ )
547
+ : resolveEffectiveRunFormatting(resolveInput, this.doc.styles);
548
+ const activeMarkupMode = this.resolveActiveMarkupMode();
549
+ if (activeMarkupMode !== undefined && input.revision !== undefined) {
550
+ const authorColor =
551
+ input.revision.authorId && this.opts.authorColorPalette
552
+ ? this.opts.authorColorPalette.get(input.revision.authorId)
553
+ : undefined;
554
+ const flags = applyRevisionDisplay(
555
+ cascade,
556
+ input.revision,
557
+ activeMarkupMode,
558
+ authorColor,
559
+ );
560
+ return { ...cascade, revisionDisplay: flags };
561
+ }
562
+ return cascade;
563
+ }
564
+
565
+ // Resolve the markup mode for the current per-run call. Callback
566
+ // wins when set; a callback that returns `undefined` means "no
567
+ // posture this pass" and does not fall back to the static field —
568
+ // that's how the composition site disables posture at runtime
569
+ // without rebuilding the context.
570
+ private resolveActiveMarkupMode():
571
+ | "clean"
572
+ | "simple"
573
+ | "all"
574
+ | undefined {
575
+ if (this.opts.getEffectiveMarkupMode !== undefined) {
576
+ return this.opts.getEffectiveMarkupMode();
577
+ }
578
+ return this.opts.revisionMarkupMode;
579
+ }
580
+
581
+ resolveTable(table: TableNode): ResolvedTableStyleResolution {
582
+ return resolveTableStyleResolution(table, this.doc.styles.tables);
583
+ }
584
+
585
+ resolveTableCellFloor(
586
+ tableStyleId: string | undefined,
587
+ activeRegions?: readonly TableStyleConditionalRegion[],
588
+ ): { paragraph: CanonicalParagraphFormatting; run: CanonicalRunFormatting } {
589
+ return resolveTableCellTextFormatting(
590
+ tableStyleId,
591
+ this.doc.styles,
592
+ activeRegions,
593
+ );
594
+ }
595
+
596
+ resolveField(entry: FieldRegistryEntry): ResolvedField | undefined {
597
+ if (!this.field) return undefined;
598
+ return this.field.resolve(entry);
599
+ }
600
+
601
+ resolveFontFamily(
602
+ input: RunResolveInput,
603
+ themeMinorFont?: string,
604
+ ): string | undefined {
605
+ return resolveRunFontFamily(
606
+ input,
607
+ this.doc.styles,
608
+ themeMinorFont,
609
+ this.opts.fontTable,
610
+ this.doc.subParts?.canonicalTheme?.fontScheme,
611
+ );
612
+ }
613
+
614
+ resolveParagraph(para: ParagraphNode): EffectiveParagraphFormatting {
615
+ const cascade = this.resolveParagraphCascade(para);
616
+ const numbering = buildEffectiveNumbering(
617
+ this.resolveParagraphNumbering(para, { advance: true }),
618
+ );
619
+ const paragraphMarkRun = cascade.paragraphMarkRunProperties && this.theme
620
+ ? (concretizeThemeColors(cascade.paragraphMarkRunProperties, this.theme) as EffectiveRunFormatting)
621
+ : (cascade.paragraphMarkRunProperties as EffectiveRunFormatting | undefined);
622
+ return {
623
+ ...cascade,
624
+ ...(numbering ? { numbering } : {}),
625
+ ...(paragraphMarkRun ? { paragraphMarkRun } : {}),
626
+ };
627
+ }
628
+
629
+ resolveRunWithProvenance(input: {
630
+ paragraphStyleId: string | undefined;
631
+ characterStyleId: string | undefined;
632
+ direct: CanonicalRunFormatting | undefined;
633
+ }): RunProvenance {
634
+ // Walk each tier in priority-ascending order, recording which tier
635
+ // last set each field. Highest-priority writer wins — same order
636
+ // as `resolveEffectiveRunFormatting` but with provenance tracked.
637
+ const properties: { [K in keyof CanonicalRunFormatting]?: RunResolvedProperty } = {};
638
+ const applyTier = (
639
+ tierRecord: CanonicalRunFormatting | undefined,
640
+ source: RunResolvedProperty["source"],
641
+ sourceId: string | undefined,
642
+ ): void => {
643
+ if (!tierRecord) return;
644
+ for (const key of Object.keys(tierRecord) as Array<keyof CanonicalRunFormatting>) {
645
+ const value = tierRecord[key];
646
+ if (value === undefined) continue;
647
+ properties[key] = {
648
+ value: value as unknown,
649
+ source,
650
+ ...(sourceId !== undefined ? { sourceId } : {}),
651
+ } as RunResolvedProperty;
652
+ }
653
+ };
654
+ const styles = this.doc.styles;
655
+ applyTier(styles.docDefaults?.run, "docDefaults", undefined);
656
+ if (input.paragraphStyleId) {
657
+ const chain = resolveParagraphStyleChain(input.paragraphStyleId, styles);
658
+ // Walk root-to-leaf so the most specific style wins.
659
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
660
+ const styleId = chain[i]!;
661
+ applyTier(styles.paragraphs[styleId]?.runProperties, "style", styleId);
662
+ }
663
+ }
664
+ if (input.characterStyleId) {
665
+ // Character chain — lazy import to avoid cyclic dep.
666
+ const charStyles = styles.characters;
667
+ const visited = new Set<string>();
668
+ const chain: string[] = [];
669
+ let cursor: string | undefined = input.characterStyleId;
670
+ while (cursor && !visited.has(cursor)) {
671
+ visited.add(cursor);
672
+ if (charStyles[cursor]) chain.push(cursor);
673
+ cursor = charStyles[cursor]?.basedOn;
674
+ }
675
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
676
+ const styleId = chain[i]!;
677
+ applyTier(charStyles[styleId]?.runProperties, "characterStyle", styleId);
678
+ }
679
+ }
680
+ applyTier(input.direct, "direct", undefined);
681
+ // Reconstruct the EffectiveRunFormatting from the winning values.
682
+ const run: EffectiveRunFormatting = {} as EffectiveRunFormatting;
683
+ for (const [key, prop] of Object.entries(properties) as Array<
684
+ [keyof CanonicalRunFormatting, RunResolvedProperty | undefined]
685
+ >) {
686
+ if (prop !== undefined) {
687
+ (run as Record<string, unknown>)[key] = prop.value;
688
+ }
689
+ }
690
+ // Theme concretization runs downstream of cascade — keep provenance
691
+ // stable ("style" or "direct" owns `colorTheme`; theme concretize
692
+ // populates `colorHex` which is still tied to the `colorTheme` tier).
693
+ if (this.theme) {
694
+ const concrete = concretizeThemeColors(run, this.theme);
695
+ if (concrete.colorHex && concrete.colorHex !== run.colorHex) {
696
+ (run as Record<string, unknown>).colorHex = concrete.colorHex;
697
+ properties.colorHex = {
698
+ value: concrete.colorHex,
699
+ source: properties.colorThemeSlot?.source ?? "style",
700
+ ...(properties.colorThemeSlot?.sourceId !== undefined
701
+ ? { sourceId: properties.colorThemeSlot.sourceId }
702
+ : {}),
703
+ };
704
+ }
705
+ }
706
+ return { run, properties };
707
+ }
708
+
709
+ resolveFieldDisplay(
710
+ entry: FieldRegistryEntry | undefined,
711
+ cachedDisplayText: string | undefined,
712
+ ): EffectiveFieldDisplay {
713
+ if (!entry) {
714
+ return cachedDisplayText
715
+ ? { refreshStatus: "unresolved", displayText: cachedDisplayText }
716
+ : { refreshStatus: "unresolved" };
717
+ }
718
+ const resolved = this.resolveField(entry);
719
+ if (!resolved) {
720
+ return {
721
+ refreshStatus: "unresolved",
722
+ ...(entry.displayText ? { displayText: entry.displayText } : {}),
723
+ };
724
+ }
725
+ const status: EffectiveFieldDisplay["refreshStatus"] =
726
+ resolved.refreshStatus === "current" || resolved.refreshStatus === "stale"
727
+ ? "resolved"
728
+ : resolved.refreshStatus === "unresolvable"
729
+ ? "unresolvable"
730
+ : "unresolved";
731
+ return {
732
+ refreshStatus: status,
733
+ ...(resolved.displayText !== undefined
734
+ ? { displayText: resolved.displayText }
735
+ : entry.displayText
736
+ ? { displayText: entry.displayText }
737
+ : {}),
738
+ };
739
+ }
740
+ }
741
+
742
+ export function createFormattingContext(
743
+ doc: CanonicalDocument,
744
+ opts: FormattingContextOptions = {},
745
+ ): FormattingContext {
746
+ return new FormattingContextImpl(doc, opts);
747
+ }
748
+
749
+ // ---------------------------------------------------------------------------
750
+ // Private helpers
751
+ // ---------------------------------------------------------------------------
752
+
753
+ function buildDirectRunFormattingFromProjected(
754
+ projected: ProjectedRunMarks | undefined,
755
+ ): CanonicalRunFormatting | undefined {
756
+ if (!projected) return undefined;
757
+ const direct: CanonicalRunFormatting = {};
758
+ const marks = projected.marks;
759
+ if (marks) {
760
+ if (marks.includes("bold")) direct.bold = true;
761
+ if (marks.includes("italic")) direct.italic = true;
762
+ if (marks.includes("underline")) direct.underline = "single";
763
+ if (marks.includes("strikethrough")) direct.strikethrough = true;
764
+ if (marks.includes("doubleStrikethrough")) direct.doubleStrikethrough = true;
765
+ if (marks.includes("vanish")) direct.vanish = true;
766
+ if (marks.includes("allCaps")) direct.allCaps = true;
767
+ if (marks.includes("smallCaps")) direct.smallCaps = true;
768
+ }
769
+ const markAttrs = projected.markAttrs;
770
+ if (markAttrs) {
771
+ if (markAttrs.fontFamily) {
772
+ direct.fontFamily = markAttrs.fontFamily;
773
+ direct.fontFamilyAscii = markAttrs.fontFamily;
774
+ }
775
+ if (typeof markAttrs.fontSize === "number") {
776
+ direct.fontSizeHalfPoints = markAttrs.fontSize;
777
+ }
778
+ if (markAttrs.textColor) {
779
+ direct.colorHex = markAttrs.textColor.replace(/^#/, "");
780
+ }
781
+ if (markAttrs.backgroundColor) {
782
+ direct.highlight = markAttrs.backgroundColor;
783
+ }
784
+ }
785
+ return Object.keys(direct).length > 0 ? direct : undefined;
786
+ }
787
+
788
+ function extractDirectParagraphFormatting(
789
+ para: ParagraphNode,
790
+ ): CanonicalParagraphFormatting {
791
+ const direct: CanonicalParagraphFormatting = {};
792
+ if (para.alignment !== undefined) direct.alignment = para.alignment;
793
+ if (para.spacing !== undefined) direct.spacing = para.spacing;
794
+ if (para.contextualSpacing !== undefined) direct.contextualSpacing = para.contextualSpacing;
795
+ if (para.indentation !== undefined) direct.indentation = para.indentation;
796
+ if (para.tabStops !== undefined) direct.tabStops = para.tabStops;
797
+ if (para.keepNext !== undefined) direct.keepNext = para.keepNext;
798
+ if (para.keepLines !== undefined) direct.keepLines = para.keepLines;
799
+ if (para.outlineLevel !== undefined) direct.outlineLevel = para.outlineLevel;
800
+ if (para.pageBreakBefore !== undefined) direct.pageBreakBefore = para.pageBreakBefore;
801
+ if (para.widowControl !== undefined) direct.widowControl = para.widowControl;
802
+ if (para.borders !== undefined) direct.borders = para.borders;
803
+ if (para.shading !== undefined) direct.shading = para.shading;
804
+ if (para.bidi !== undefined) direct.bidi = para.bidi;
805
+ if (para.suppressLineNumbers !== undefined) direct.suppressLineNumbers = para.suppressLineNumbers;
806
+ return direct;
807
+ }
808
+
809
+ function resolveStyleLinkedNumberingLevel(
810
+ doc: CanonicalDocument,
811
+ numberingInstanceId: string,
812
+ styleChain: readonly string[],
813
+ ): number | undefined {
814
+ const instance = doc.numbering.instances[numberingInstanceId];
815
+ if (!instance) return undefined;
816
+
817
+ for (const styleId of styleChain) {
818
+ const overrideMatch = instance.overrides.find(
819
+ (override) => override.levelDefinition?.paragraphStyleId === styleId,
820
+ );
821
+ if (overrideMatch) return overrideMatch.level;
822
+ }
823
+
824
+ const abstractDefinition = doc.numbering.abstractDefinitions[instance.abstractNumberingId];
825
+ if (!abstractDefinition) return undefined;
826
+
827
+ for (const styleId of styleChain) {
828
+ const levelMatch = abstractDefinition.levels.find(
829
+ (levelDefinition) => levelDefinition.paragraphStyleId === styleId,
830
+ );
831
+ if (levelMatch) return levelMatch.level;
832
+ }
833
+ return undefined;
834
+ }
835
+
836
+ function buildEffectiveNumbering(
837
+ detail: NumberingResolution | null,
838
+ ): EffectiveNumbering | undefined {
839
+ if (!detail) return undefined;
840
+ const result: EffectiveNumbering = {
841
+ level: detail.level,
842
+ ...(detail.text !== null ? { marker: detail.text } : {}),
843
+ ...(detail.markerRunProperties
844
+ ? { markerRunFormatting: detail.markerRunProperties }
845
+ : {}),
846
+ ...(detail.geometry
847
+ ? {
848
+ indentation: {
849
+ ...(detail.geometry.indentation?.left !== undefined
850
+ ? { left: detail.geometry.indentation.left }
851
+ : {}),
852
+ ...(detail.geometry.indentation?.hanging !== undefined
853
+ ? { hanging: detail.geometry.indentation.hanging }
854
+ : {}),
855
+ ...(detail.geometry.indentation?.firstLine !== undefined
856
+ ? { firstLine: detail.geometry.indentation.firstLine }
857
+ : {}),
858
+ ...(detail.geometry.tabStops && detail.geometry.tabStops.length > 0
859
+ ? { tab: detail.geometry.tabStops[0]!.position }
860
+ : {}),
861
+ },
862
+ }
863
+ : {}),
864
+ ...(detail.picBulletMediaId
865
+ ? { pictureBulletRef: detail.picBulletMediaId }
866
+ : {}),
867
+ };
868
+ return result;
869
+ }
870
+
871
+ // Type-only re-export of HyperlinkNode so callers can type their walk.
872
+ export type { HyperlinkNode, InlineNode };
873
+
874
+ /**
875
+ * Build a sorted-by-`from` array of the document's OPEN revisions +
876
+ * their anchor ranges. Accepted / rejected / detached revisions are
877
+ * excluded because F6 markup modes ("clean" | "simple" | "all") only
878
+ * change how open revisions render. The array is immutable after
879
+ * construction; `FormattingContext.findRevisionAtRange` walks it per
880
+ * segment.
881
+ */
882
+ function buildRevisionRangeIndex(
883
+ doc: CanonicalDocument,
884
+ ): ReadonlyArray<{ from: number; to: number; revision: RevisionRecord }> {
885
+ const records: { from: number; to: number; revision: RevisionRecord }[] = [];
886
+ const revisions = doc.review?.revisions;
887
+ if (!revisions) return records;
888
+ for (const revision of Object.values(revisions)) {
889
+ if (revision.status !== "open") continue;
890
+ const anchor = revision.anchor;
891
+ let range: { from: number; to: number } | undefined;
892
+ if (anchor.kind === "range") {
893
+ range = { from: anchor.range.from, to: anchor.range.to };
894
+ } else if (anchor.kind === "node") {
895
+ range = { from: anchor.at, to: anchor.at };
896
+ } else {
897
+ continue;
898
+ }
899
+ records.push({ from: range.from, to: range.to, revision });
900
+ }
901
+ records.sort((a, b) => a.from - b.from);
902
+ return records;
903
+ }