@stll/folio-core 0.54.2 → 0.56.0

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 (267) hide show
  1. package/dist/ai-edits/apply.js +185 -33
  2. package/dist/ai-edits/headless.js +3 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/content-types.d.ts +3 -15
  6. package/dist/compare/reproducible-package.d.ts +0 -17
  7. package/dist/compare/reproducible-package.js +12 -18
  8. package/dist/controller/canonicalClipboard.d.ts +23 -0
  9. package/dist/controller/canonicalClipboard.js +476 -0
  10. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  11. package/dist/controller/canonicalClipboardResources.js +382 -0
  12. package/dist/controller/canonicalComposition.d.ts +53 -0
  13. package/dist/controller/canonicalComposition.js +296 -0
  14. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  15. package/dist/controller/canonicalInlineProjection.js +393 -0
  16. package/dist/controller/canonicalInput.d.ts +54 -0
  17. package/dist/controller/canonicalInput.js +377 -0
  18. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  19. package/dist/controller/canonicalInputTimer.js +11 -0
  20. package/dist/controller/canonicalOperations.d.ts +22 -0
  21. package/dist/controller/canonicalOperations.js +85 -0
  22. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  23. package/dist/controller/canonicalPublicOperations.js +556 -0
  24. package/dist/controller/canonicalReview.d.ts +12 -0
  25. package/dist/controller/canonicalReview.js +19 -0
  26. package/dist/controller/canonicalSession.d.ts +167 -0
  27. package/dist/controller/canonicalSession.js +1186 -0
  28. package/dist/controller/canonicalStoryEditor.d.ts +36 -0
  29. package/dist/controller/canonicalStoryEditor.js +87 -0
  30. package/dist/controller/canonicalStructure.d.ts +16 -0
  31. package/dist/controller/canonicalStructure.js +322 -0
  32. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  33. package/dist/controller/canonicalTocSelection.js +43 -0
  34. package/dist/controller/folioEditor.d.ts +3 -10
  35. package/dist/controller/folioEditor.js +14 -4
  36. package/dist/controller/headerFooterEditorManager.d.ts +5 -0
  37. package/dist/controller/headerFooterEditorManager.js +72 -3
  38. package/dist/controller/hiddenEditorApi.d.ts +45 -0
  39. package/dist/controller/hiddenEditorApi.js +29 -8
  40. package/dist/controller/hiddenEditorManager.d.ts +13 -2
  41. package/dist/controller/hiddenEditorManager.js +639 -31
  42. package/dist/controller/layoutPipeline.d.ts +1 -1
  43. package/dist/controller/layoutPipeline.js +2 -0
  44. package/dist/controller/noteEditorManager.d.ts +5 -0
  45. package/dist/controller/noteEditorManager.js +114 -15
  46. package/dist/document-operations.d.ts +8 -1
  47. package/dist/document-operations.js +6 -4
  48. package/dist/docx/attributeRemainder.js +1 -0
  49. package/dist/docx/blockPlainText.d.ts +1 -1
  50. package/dist/docx/blockPlainText.js +11 -3
  51. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  52. package/dist/docx/canonicalResourceSave.js +27 -0
  53. package/dist/docx/canonicalSave.d.ts +29 -0
  54. package/dist/docx/canonicalSave.js +101 -0
  55. package/dist/docx/canonicalSessionInput.d.ts +11 -0
  56. package/dist/docx/canonicalSessionInput.js +27 -0
  57. package/dist/docx/canonicalStoryRepack.d.ts +10 -0
  58. package/dist/docx/canonicalStoryRepack.js +74 -0
  59. package/dist/docx/contentTypeRegistry.d.ts +8 -0
  60. package/dist/docx/contentTypeRegistry.js +64 -0
  61. package/dist/docx/documentParser.d.ts +1 -13
  62. package/dist/docx/documentParser.js +1 -47
  63. package/dist/docx/ensureParaIds.d.ts +7 -1
  64. package/dist/docx/ensureParaIds.js +173 -215
  65. package/dist/docx/foldedListNumberFields.d.ts +85 -0
  66. package/dist/docx/foldedListNumberFields.js +267 -0
  67. package/dist/docx/footnoteParser.d.ts +1 -11
  68. package/dist/docx/footnoteParser.js +1 -16
  69. package/dist/docx/headerFooterParser.d.ts +1 -9
  70. package/dist/docx/headerFooterParser.js +1 -12
  71. package/dist/docx/headerFooterVerbatim.d.ts +39 -1
  72. package/dist/docx/headerFooterVerbatim.js +173 -2
  73. package/dist/docx/imageParser.d.ts +13 -1
  74. package/dist/docx/imageParser.js +20 -11
  75. package/dist/docx/metadataPrivacy.js +6 -1
  76. package/dist/docx/noteIds.d.ts +5 -0
  77. package/dist/docx/noteIds.js +6 -2
  78. package/dist/docx/noteReferenceMark.d.ts +3 -1
  79. package/dist/docx/noteReferenceMark.js +15 -14
  80. package/dist/docx/numberingParser.d.ts +3 -1
  81. package/dist/docx/numberingParser.js +17 -3
  82. package/dist/docx/packageParts.d.ts +3 -1
  83. package/dist/docx/packageParts.js +74 -19
  84. package/dist/docx/paragraphMarkPropertyPresence.d.ts +9 -0
  85. package/dist/docx/paragraphMarkPropertyPresence.js +21 -0
  86. package/dist/docx/paragraphParser.js +101 -53
  87. package/dist/docx/paragraphPropertySource.d.ts +15 -1
  88. package/dist/docx/paragraphPropertySource.js +77 -22
  89. package/dist/docx/parser.js +3 -1
  90. package/dist/docx/removeHeaderFooterParts.js +17 -6
  91. package/dist/docx/reviewHistoryNamespace.d.ts +4 -0
  92. package/dist/docx/reviewHistoryNamespace.js +4 -0
  93. package/dist/docx/reviewResolutionProvenance.d.ts +25 -0
  94. package/dist/docx/reviewResolutionProvenance.js +153 -0
  95. package/dist/docx/rezip.d.ts +54 -6
  96. package/dist/docx/rezip.js +835 -257
  97. package/dist/docx/runConsolidator.d.ts +2 -58
  98. package/dist/docx/runConsolidator.js +3 -175
  99. package/dist/docx/runParser.d.ts +2 -1
  100. package/dist/docx/runParser.js +156 -116
  101. package/dist/docx/saveDiagnostics.d.ts +22 -0
  102. package/dist/docx/saveDiagnostics.js +0 -0
  103. package/dist/docx/selectiveSave.d.ts +3 -1
  104. package/dist/docx/selectiveSave.js +36 -4
  105. package/dist/docx/selectiveXmlPatch.d.ts +16 -8
  106. package/dist/docx/selectiveXmlPatch.js +81 -7
  107. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  108. package/dist/docx/serializer/documentSerializer.js +29 -10
  109. package/dist/docx/serializer/headerFooterSerializer.d.ts +5 -1
  110. package/dist/docx/serializer/headerFooterSerializer.js +19 -11
  111. package/dist/docx/serializer/noteSerializer.d.ts +2 -2
  112. package/dist/docx/serializer/noteSerializer.js +24 -8
  113. package/dist/docx/serializer/paragraphSerializer.js +41 -4
  114. package/dist/docx/serializer/partNamespaces.js +2 -1
  115. package/dist/docx/serializer/runSerializer.js +3 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  117. package/dist/docx/settingsHeaderFooterUpdate.d.ts +5 -0
  118. package/dist/docx/settingsHeaderFooterUpdate.js +90 -0
  119. package/dist/docx/settingsParser.d.ts +1 -2
  120. package/dist/docx/settingsParser.js +5 -6
  121. package/dist/docx/storyBlockReplay.d.ts +31 -0
  122. package/dist/docx/storyBlockReplay.js +471 -0
  123. package/dist/docx/storyPlainText.d.ts +20 -0
  124. package/dist/docx/storyPlainText.js +23 -0
  125. package/dist/docx/streamingXmlParser.d.ts +22 -1
  126. package/dist/docx/streamingXmlParser.js +49 -3
  127. package/dist/docx/structuralXmlPatch.js +18 -23
  128. package/dist/docx/styleParser.d.ts +5 -1
  129. package/dist/docx/styleParser.js +1 -1
  130. package/dist/docx/tableParser.d.ts +1 -8
  131. package/dist/docx/tableParser.js +1 -28
  132. package/dist/docx/textBoxParser.d.ts +1 -5
  133. package/dist/docx/textBoxParser.js +1 -20
  134. package/dist/docx/unzip.js +6 -1
  135. package/dist/docx/verbatimCapture.d.ts +3 -1
  136. package/dist/docx/verbatimCapture.js +49 -6
  137. package/dist/i18n/messages/ar.gen.d.ts +5 -0
  138. package/dist/i18n/messages/ar.gen.js +656 -0
  139. package/dist/i18n/messages/cs.gen.d.ts +5 -0
  140. package/dist/i18n/messages/cs.gen.js +656 -0
  141. package/dist/i18n/messages/de.gen.d.ts +5 -0
  142. package/dist/i18n/messages/de.gen.js +656 -0
  143. package/dist/i18n/messages/en.gen.d.ts +5 -0
  144. package/dist/i18n/messages/en.gen.js +656 -0
  145. package/dist/i18n/messages/es.gen.d.ts +5 -0
  146. package/dist/i18n/messages/es.gen.js +656 -0
  147. package/dist/i18n/messages/et.gen.d.ts +5 -0
  148. package/dist/i18n/messages/et.gen.js +656 -0
  149. package/dist/i18n/messages/fr.gen.d.ts +5 -0
  150. package/dist/i18n/messages/fr.gen.js +656 -0
  151. package/dist/i18n/messages/he.gen.d.ts +5 -0
  152. package/dist/i18n/messages/he.gen.js +656 -0
  153. package/dist/i18n/messages/hi.gen.d.ts +5 -0
  154. package/dist/i18n/messages/hi.gen.js +656 -0
  155. package/dist/i18n/messages/hu.gen.d.ts +5 -0
  156. package/dist/i18n/messages/hu.gen.js +656 -0
  157. package/dist/i18n/messages/locales.d.ts +5 -0
  158. package/dist/i18n/messages/locales.js +22 -0
  159. package/dist/i18n/messages/lt.gen.d.ts +5 -0
  160. package/dist/i18n/messages/lt.gen.js +656 -0
  161. package/dist/i18n/messages/lv.gen.d.ts +5 -0
  162. package/dist/i18n/messages/lv.gen.js +656 -0
  163. package/dist/i18n/messages/pl.gen.d.ts +5 -0
  164. package/dist/i18n/messages/pl.gen.js +656 -0
  165. package/dist/i18n/messages/pt-BR.gen.d.ts +5 -0
  166. package/dist/i18n/messages/pt-BR.gen.js +656 -0
  167. package/dist/i18n/messages/sk.gen.d.ts +5 -0
  168. package/dist/i18n/messages/sk.gen.js +656 -0
  169. package/dist/i18n/messages/tr.gen.d.ts +5 -0
  170. package/dist/i18n/messages/tr.gen.js +656 -0
  171. package/dist/i18n/messages/zh-CN.gen.d.ts +5 -0
  172. package/dist/i18n/messages/zh-CN.gen.js +656 -0
  173. package/dist/i18n/messages.d.ts +2 -3
  174. package/dist/i18n/messages.js +1 -19
  175. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  176. package/dist/internal/acceptedBlockProjection.js +45 -0
  177. package/dist/internal/noteMarkerAttrs.d.ts +8 -0
  178. package/dist/internal/noteMarkerAttrs.js +16 -0
  179. package/dist/internal/pageBreakRunSourceDescendantIndex.js +1 -0
  180. package/dist/internal/prosemirrorAttrBoundary.d.ts +21 -0
  181. package/dist/internal/prosemirrorAttrBoundary.js +50 -0
  182. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  183. package/dist/layout-bridge/convert/footnoteLayout.js +3 -3
  184. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  185. package/dist/layout-bridge/convert/headerFooterLayout.js +1 -0
  186. package/dist/layout-bridge/convert/noteReferences.d.ts +1 -3
  187. package/dist/layout-bridge/convert/noteReferences.js +1 -6
  188. package/dist/layout-bridge/convert/paragraphRuns.js +2 -4
  189. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -2
  190. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -2
  191. package/dist/managers/DocumentLoaderManager.d.ts +1 -0
  192. package/dist/managers/DocumentLoaderManager.js +9 -1
  193. package/dist/markdown/escape.d.ts +3 -1
  194. package/dist/markdown/escape.js +9 -2
  195. package/dist/markdown/renderBlock.js +20 -121
  196. package/dist/markdown/renderParagraph.js +13 -15
  197. package/dist/markdown/renderRuns.d.ts +7 -1
  198. package/dist/markdown/renderRuns.js +35 -15
  199. package/dist/markdown/renderTable.js +2 -0
  200. package/dist/markdown/types.d.ts +3 -9
  201. package/dist/prosemirror/attrs/index.d.ts +2 -12
  202. package/dist/prosemirror/attrs/index.js +22 -48
  203. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +2 -1
  204. package/dist/prosemirror/canonicalCommands.d.ts +121 -0
  205. package/dist/prosemirror/canonicalCommands.js +35 -0
  206. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  207. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  208. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  209. package/dist/prosemirror/clipboardIntent.js +9 -0
  210. package/dist/prosemirror/commands/pageBreak.js +15 -1
  211. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  212. package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
  213. package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
  214. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  215. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  216. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
  217. package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
  218. package/dist/prosemirror/conversion/toProseDoc.d.ts +11 -4
  219. package/dist/prosemirror/conversion/toProseDoc.js +55 -14
  220. package/dist/prosemirror/executeEditorCommand.d.ts +11 -0
  221. package/dist/prosemirror/executeEditorCommand.js +26 -0
  222. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  223. package/dist/prosemirror/extensions/core/ParagraphExtension.js +88 -48
  224. package/dist/prosemirror/extensions/features/ListExtension.js +18 -5
  225. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +5 -2
  226. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +13 -1
  227. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  228. package/dist/prosemirror/extensions/marks/TextColorExtension.js +3 -2
  229. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  230. package/dist/prosemirror/extensions/marks/markUtils.js +72 -8
  231. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.d.ts +6 -0
  232. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.js +36 -0
  233. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +4 -2
  234. package/dist/prosemirror/insertOperations.js +2 -1
  235. package/dist/prosemirror/listMarker.d.ts +1 -0
  236. package/dist/prosemirror/listMarker.js +16 -7
  237. package/dist/prosemirror/listNumbering.js +26 -5
  238. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  239. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  240. package/dist/prosemirror/markupViewNotes.js +3 -2
  241. package/dist/prosemirror/moveRangeBoundaryAttrs.d.ts +2 -1
  242. package/dist/prosemirror/noteReferenceReview.js +5 -3
  243. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  244. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  245. package/dist/prosemirror/plugins/suggestionMode.js +57 -7
  246. package/dist/prosemirror/rangeAnchorAttrs.d.ts +2 -1
  247. package/dist/prosemirror/runFormattingFromMarks.d.ts +4 -1
  248. package/dist/prosemirror/runFormattingFromMarks.js +15 -1
  249. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +2 -1
  250. package/dist/prosemirror/runFormattingInlineCarriers.js +4 -1
  251. package/dist/prosemirror/schema/marks.d.ts +2 -0
  252. package/dist/prosemirror/schema/nodes.d.ts +10 -2
  253. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +2 -1
  254. package/dist/prosemirror/textInput.js +5 -1
  255. package/dist/prosemirror/trackedRevisionPath.js +2 -1
  256. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -0
  257. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -0
  258. package/dist/prosemirror/validation.js +4 -0
  259. package/dist/types/canonicalCapabilities.d.ts +215 -0
  260. package/dist/types/canonicalCapabilities.js +212 -0
  261. package/dist/types/canonicalSave.d.ts +11 -0
  262. package/dist/types/canonicalSave.js +0 -0
  263. package/dist/types/content.d.ts +2 -2
  264. package/dist/types/docxSerialization.d.ts +14 -0
  265. package/dist/types/docxSerialization.js +7 -0
  266. package/dist/utils/createDocument.js +2 -2
  267. package/package.json +16 -4
@@ -0,0 +1,1186 @@
1
+ import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
+ import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
3
+ import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
4
+ import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
5
+ import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
6
+ import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
7
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
8
+ import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
9
+ import { mapTocSelection } from "./canonicalTocSelection.js";
10
+ import { Result, TaggedError, panic } from "better-result";
11
+ import { hasIllegalXmlCharacters } from "@stll/docx-core";
12
+ import { Fragment } from "prosemirror-model";
13
+ import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
14
+ import { AllSelection, TextSelection } from "prosemirror-state";
15
+ //#region src/controller/canonicalSession.ts
16
+ var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
17
+ const refuse = (message) => Result.err(new CanonicalSessionError({
18
+ gap: CANONICAL_GAP.dispatch,
19
+ message,
20
+ reason: "refused"
21
+ }));
22
+ const noChange = (message) => Result.err(new CanonicalSessionError({
23
+ gap: CANONICAL_GAP.dispatch,
24
+ message,
25
+ reason: "noChange"
26
+ }));
27
+ const operationChangesPackage = (op) => {
28
+ switch (op.type) {
29
+ case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
30
+ case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
31
+ case DOCUMENT_OP_TYPES.ADD_NOTE:
32
+ case DOCUMENT_OP_TYPES.REMOVE_NOTE:
33
+ case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
34
+ case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
35
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
36
+ case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
37
+ case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
38
+ case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
39
+ default:
40
+ if ("story" in op) return !sameStory(op.story, OP_STORIES.MAIN);
41
+ if ("at" in op) return !sameStory(op.at.story, OP_STORIES.MAIN);
42
+ if ("from" in op) return !sameStory(op.from.story, OP_STORIES.MAIN);
43
+ return op;
44
+ }
45
+ };
46
+ const operationChangesStructure = (op) => {
47
+ switch (op.type) {
48
+ case DOCUMENT_OP_TYPES.DELETE_BLOCKS:
49
+ case DOCUMENT_OP_TYPES.INSERT_BLOCKS:
50
+ case DOCUMENT_OP_TYPES.SPLIT_BLOCK:
51
+ case DOCUMENT_OP_TYPES.JOIN_BLOCKS:
52
+ case DOCUMENT_OP_TYPES.REPLACE_BLOCKS:
53
+ case DOCUMENT_OP_TYPES.INSERT_TABLE:
54
+ case DOCUMENT_OP_TYPES.DELETE_TABLE:
55
+ case DOCUMENT_OP_TYPES.SET_CONTAINER_BLOCKS:
56
+ case DOCUMENT_OP_TYPES.INSERT_ROW:
57
+ case DOCUMENT_OP_TYPES.DELETE_ROW:
58
+ case DOCUMENT_OP_TYPES.SET_TABLE_ROWS:
59
+ case DOCUMENT_OP_TYPES.INSERT_COLUMN:
60
+ case DOCUMENT_OP_TYPES.DELETE_COLUMN:
61
+ case DOCUMENT_OP_TYPES.MERGE_CELLS:
62
+ case DOCUMENT_OP_TYPES.SPLIT_CELL:
63
+ case DOCUMENT_OP_TYPES.SET_TABLE:
64
+ case DOCUMENT_OP_TYPES.RESOLVE_REVISION: return true;
65
+ case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
66
+ case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
67
+ case DOCUMENT_OP_TYPES.ADD_NOTE:
68
+ case DOCUMENT_OP_TYPES.REMOVE_NOTE:
69
+ case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
70
+ case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
71
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
72
+ case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
73
+ case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
74
+ case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT:
75
+ case DOCUMENT_OP_TYPES.INSERT_TEXT:
76
+ case DOCUMENT_OP_TYPES.INSERT_CONTENT:
77
+ case DOCUMENT_OP_TYPES.DELETE_RANGE:
78
+ case DOCUMENT_OP_TYPES.SPLIT_INLINE:
79
+ case DOCUMENT_OP_TYPES.JOIN_INLINE:
80
+ case DOCUMENT_OP_TYPES.SET_RUN_PROPS:
81
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS:
82
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_REVIEW:
83
+ case DOCUMENT_OP_TYPES.REPLACE_INLINE:
84
+ case DOCUMENT_OP_TYPES.SET_TABLE_GRID:
85
+ case DOCUMENT_OP_TYPES.SET_CELL_PROPS:
86
+ case DOCUMENT_OP_TYPES.SET_ROW_PROPS:
87
+ case DOCUMENT_OP_TYPES.SET_TABLE_PROPS: return false;
88
+ default: return op;
89
+ }
90
+ };
91
+ const operationChangesBodyProjection = (op) => {
92
+ switch (op.type) {
93
+ case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
94
+ case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
95
+ case DOCUMENT_OP_TYPES.ADD_NOTE:
96
+ case DOCUMENT_OP_TYPES.REMOVE_NOTE:
97
+ case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
98
+ case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
99
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
100
+ case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
101
+ case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
102
+ case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
103
+ default:
104
+ if ("story" in op) return sameStory(op.story, OP_STORIES.MAIN);
105
+ if ("at" in op) return sameStory(op.at.story, OP_STORIES.MAIN);
106
+ if ("from" in op) return sameStory(op.from.story, OP_STORIES.MAIN);
107
+ return op;
108
+ }
109
+ };
110
+ const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
111
+ /** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
112
+ var CanonicalProjection = class {
113
+ doc;
114
+ story;
115
+ paragraphs;
116
+ paragraphsById;
117
+ constructor({ doc, paragraphs, story }) {
118
+ this.story = story;
119
+ this.doc = doc;
120
+ this.paragraphs = paragraphs;
121
+ this.paragraphsById = new Map(paragraphs.map((paragraph) => [paragraph.blockId, paragraph]));
122
+ }
123
+ addressAt(position) {
124
+ if (!Number.isInteger(position)) return refuse("The input position is not an integer.");
125
+ const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
126
+ if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
127
+ const relative = position - paragraph.start;
128
+ const offset = paragraph.boundaries.findIndex((gaps) => gaps.some((gap) => gap.position === relative));
129
+ if (offset < 0) return refuse("The input splits an indivisible inline element.");
130
+ if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
131
+ const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
132
+ const gap = gaps.find((entry) => entry.position === relative) ?? panic("Missing canonical native gap");
133
+ return Result.ok({
134
+ story: this.story,
135
+ blockId: paragraph.blockId,
136
+ offset,
137
+ ...gaps.length > 1 || gap.zeroWidthBefore > 0 ? { zeroWidthBefore: gap.zeroWidthBefore } : {}
138
+ });
139
+ }
140
+ inputAddressAt(position) {
141
+ const address = this.addressAt(position);
142
+ if (address.isErr()) return address;
143
+ const paragraph = this.paragraph(address.value.blockId);
144
+ if (paragraph === void 0) panic("Input lost its paragraph.");
145
+ let destination = position;
146
+ paragraph.node.forEach((node, start) => {
147
+ if (node.marks.some((mark) => mark.type.name === "deletion") && position > paragraph.start + start && position < paragraph.start + start + node.nodeSize) destination = paragraph.start + start + node.nodeSize;
148
+ });
149
+ return destination === position ? address : this.addressAt(destination);
150
+ }
151
+ positionAt(address) {
152
+ const paragraph = this.paragraph(address.blockId);
153
+ if (!sameStory(address.story, this.story) || paragraph === void 0 || !Number.isInteger(address.offset) || address.offset < 0 || address.offset > paragraph.text.length || splitsSurrogatePair(paragraph.text, address.offset)) return refuse("The canonical selection is outside a plain paragraph.");
154
+ const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
155
+ const ordinal = address.zeroWidthBefore ?? (gaps.at(-1) ?? panic("Missing canonical gap")).zeroWidthBefore;
156
+ const gap = gaps.find(({ zeroWidthBefore }) => zeroWidthBefore === ordinal);
157
+ if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal > (gaps.at(-1)?.zeroWidthBefore ?? 0)) return refuse("The canonical selection is outside a paragraph gap.");
158
+ if (gap === void 0) return refuse("This source gap is inside a collapsed or erased inline boundary and has no editor position.");
159
+ return Result.ok(paragraph.start + gap.position);
160
+ }
161
+ selectionAt(state) {
162
+ if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
163
+ const range = canonicalSelectionRange(state);
164
+ const type = state.selection instanceof AllSelection ? "all" : "text";
165
+ const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
166
+ if (anchor.isErr()) return anchor;
167
+ const head = this.addressAt(type === "all" ? range.to : state.selection.head);
168
+ if (head.isErr()) return head;
169
+ return Result.ok({
170
+ type,
171
+ anchor: anchor.value,
172
+ head: head.value
173
+ });
174
+ }
175
+ paragraph(blockId) {
176
+ return this.paragraphsById.get(blockId);
177
+ }
178
+ };
179
+ /** Immutable operations retain content identities for every untouched story. */
180
+ const changedStories = ({ document, previous }) => {
181
+ const stories = [];
182
+ if (document.package.document.content !== previous.package.document.content) stories.push(OP_STORIES.MAIN);
183
+ for (const kind of ["header", "footer"]) {
184
+ const parts = kind === "header" ? document.package.headers : document.package.footers;
185
+ const prior = kind === "header" ? previous.package.headers : previous.package.footers;
186
+ if (parts === prior) continue;
187
+ for (const [rId, body] of parts ?? []) if (body.content !== prior?.get(rId)?.content) stories.push({
188
+ kind,
189
+ rId
190
+ });
191
+ }
192
+ for (const kind of ["footnote", "endnote"]) {
193
+ const notes = kind === "footnote" ? document.package.footnotes : document.package.endnotes;
194
+ const prior = kind === "footnote" ? previous.package.footnotes : previous.package.endnotes;
195
+ if (notes === prior) continue;
196
+ const priorById = new Map(prior?.map((note) => [note.id, note]));
197
+ for (const note of notes ?? []) {
198
+ const original = priorById.get(note.id);
199
+ if (note.content !== original?.content || note.noteType !== original?.noteType) stories.push({
200
+ kind,
201
+ id: note.id
202
+ });
203
+ }
204
+ }
205
+ return stories;
206
+ };
207
+ const preservePropertySources = ({ target, source, stories = documentStories(source) }) => {
208
+ for (const story of stories) {
209
+ const originals = findStoryBody(source, story)?.content ?? [];
210
+ const derived = findStoryBody(target, story)?.content ?? [];
211
+ if (originals === derived) continue;
212
+ const paragraphs = new Map(derived.flatMap((block) => block.type === "paragraph" ? [[block.paraId, block]] : []));
213
+ for (const [index, original] of originals.entries()) {
214
+ if (original.type !== "paragraph") continue;
215
+ const next = original.paraId === void 0 ? derived.at(index) : paragraphs.get(original.paraId);
216
+ if (next?.type === "paragraph" && next !== original) copyParagraphPropertySource(next, original);
217
+ }
218
+ }
219
+ copyDocumentParagraphPropertySourceContract(target, source);
220
+ };
221
+ const unsupportedSeedReason = (document, stories = documentStories(document)) => {
222
+ for (const story of stories) {
223
+ const body = findStoryBody(document, story);
224
+ if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
225
+ const note = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id);
226
+ if (note?.noteType !== void 0 && note.noteType !== "normal") continue;
227
+ }
228
+ const content = body?.content;
229
+ if (!content || content.length === 0) return "Canonical sessions require a nonempty paragraph story.";
230
+ for (const paragraph of content) {
231
+ if (paragraph.type !== "paragraph" || paragraph.reviewCarrier !== void 0) return "Canonical sessions currently require plain paragraphs and supported inline atoms.";
232
+ const issue = canonicalInlineSourceIssue(paragraph.content);
233
+ if (issue !== null) return issue;
234
+ }
235
+ }
236
+ return null;
237
+ };
238
+ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
239
+ const body = findStoryBody(document, story);
240
+ if (!body) return refuse("The canonical story no longer exists.");
241
+ const currentStyles = Object.hasOwn(document.package, "styles") ? document.package.styles : styles;
242
+ const options = {
243
+ ...currentStyles == null ? {} : { styles: currentStyles },
244
+ ...document.package.theme === void 0 ? {} : { theme: document.package.theme }
245
+ };
246
+ const converted = Result.try({
247
+ try: () => {
248
+ if (story === OP_STORIES.MAIN) return toProseDoc(document, options);
249
+ if (story.kind === "header" || story.kind === "footer") return headerFooterToProseDoc(body.content, options);
250
+ return footnoteToProseDoc(body.content, options);
251
+ },
252
+ catch: (cause) => new CanonicalSessionError({
253
+ gap: CANONICAL_GAP.dispatch,
254
+ reason: "refused",
255
+ message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
256
+ })
257
+ });
258
+ if (converted.isErr()) return converted;
259
+ const pairedBookmarkIds = collectPairedBookmarkIds(body.content);
260
+ const paragraphs = [];
261
+ let failure;
262
+ converted.value.forEach((node, offset, index) => {
263
+ const source = body.content.at(index);
264
+ if (source?.type !== "paragraph" || source.paraId === void 0) {
265
+ failure = new CanonicalSessionError({
266
+ gap: CANONICAL_GAP.dispatch,
267
+ reason: "refused",
268
+ message: "The projection changed paragraph structure."
269
+ });
270
+ return;
271
+ }
272
+ if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
273
+ failure = new CanonicalSessionError({
274
+ gap: CANONICAL_GAP.dispatch,
275
+ reason: "refused",
276
+ message: "The projection changed paragraph structure."
277
+ });
278
+ return;
279
+ }
280
+ const inline = projectCanonicalInline({
281
+ source,
282
+ paragraph: node,
283
+ pairedBookmarkIds
284
+ });
285
+ if (inline.isErr()) {
286
+ failure = new CanonicalSessionError({
287
+ gap: CANONICAL_GAP.dispatch,
288
+ reason: "refused",
289
+ message: inline.error.message
290
+ });
291
+ return;
292
+ }
293
+ paragraphs.push({
294
+ blockId: source.paraId,
295
+ start: offset + 1,
296
+ ...inline.value,
297
+ node,
298
+ source
299
+ });
300
+ });
301
+ if (failure !== void 0) return Result.err(failure);
302
+ if (paragraphs.length !== body.content.length) return refuse("The projection changed paragraph structure.");
303
+ return Result.ok(new CanonicalProjection({
304
+ doc: converted.value,
305
+ paragraphs,
306
+ story
307
+ }));
308
+ };
309
+ const intentStory = (intent) => {
310
+ switch (intent.type) {
311
+ case "setHyperlink":
312
+ case "removeHyperlink":
313
+ case "insertHyperlink":
314
+ case "replaceFragment":
315
+ case "moveFragment":
316
+ case "replaceText":
317
+ case "formatRun":
318
+ case "insertAtom": return intent.from.story;
319
+ case "generateTOC":
320
+ case "splitParagraph":
321
+ case "formatParagraph": return intent.at.story;
322
+ case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
323
+ case "joinParagraphs": return intent.story;
324
+ case "table": return intent.operation.story;
325
+ default: return intent;
326
+ }
327
+ };
328
+ const UNDO_GROUP_WINDOW_MS = 500;
329
+ const samePosition = (left, right) => sameStory(left.story, right.story) && left.blockId === right.blockId && left.offset === right.offset && (left.zeroWidthBefore ?? 0) === (right.zeroWidthBefore ?? 0);
330
+ const sameSelection = (left, right) => left.type === right.type && samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
331
+ const continuesGroup = (previous, next) => {
332
+ switch (next.semantic) {
333
+ case "typing":
334
+ case "deleteBackward":
335
+ case "deleteForward": return previous.grouping === "run" && next.grouping === "run" && previous.semantic === next.semantic && previous.boundary === next.boundary && next.time >= previous.time && next.time - previous.time <= UNDO_GROUP_WINDOW_MS && sameSelection(previous.postSelection, next.preSelection) && samePosition(next.preSelection.anchor, next.preSelection.head);
336
+ case "composition": return next.grouping === "compositionCorrection" && previous.semantic === "composition" && previous.boundary === next.boundary && sameSelection(previous.postSelection, next.preSelection);
337
+ case "paste":
338
+ case "replacement":
339
+ case "structure": return false;
340
+ default: {
341
+ const unreachable = next.semantic;
342
+ return panic(`Unknown canonical input semantic ${unreachable}`);
343
+ }
344
+ }
345
+ };
346
+ const authorizedProjections = /* @__PURE__ */ new WeakMap();
347
+ /** Metadata is descriptive; transaction identity and session version authorize mutations. */
348
+ const isCanonicalProjectionTransaction = (transaction, session) => {
349
+ const authorization = authorizedProjections.get(transaction);
350
+ return authorization?.session === session && authorization.version === session.version && transaction.doc.eq(authorization.doc);
351
+ };
352
+ /** The model publishes only after PM and every plugin accept its exact projection. */
353
+ const publishCanonicalProjection = ({ state, commit, session }) => {
354
+ if (!isCanonicalProjectionTransaction(commit.transaction, session)) return refuse("The staged canonical projection is unauthorized.");
355
+ const applied = Result.try({
356
+ try: () => state.applyTransaction(commit.transaction),
357
+ catch: (cause) => new CanonicalSessionError({
358
+ gap: CANONICAL_GAP.dispatch,
359
+ reason: "refused",
360
+ message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
361
+ })
362
+ });
363
+ if (applied.isErr()) return applied;
364
+ const staged = applied.value;
365
+ if (!staged.transactions.includes(commit.transaction) || !staged.state.doc.eq(commit.projection.doc) || staged.transactions.some((transaction) => transaction.docChanged && !isCanonicalProjectionTransaction(transaction, session))) return refuse("A plugin refused or changed the canonical projection.");
366
+ const published = commit.publish();
367
+ if (published.isErr()) return published;
368
+ return Result.ok(staged);
369
+ };
370
+ const storyProjectionKey = (story) => {
371
+ switch (story.kind) {
372
+ case "header":
373
+ case "footer": return `${story.kind}:${story.rId}`;
374
+ case "footnote":
375
+ case "endnote": return `${story.kind}:${story.id}`;
376
+ default: return panic(`Unknown canonical story ${story}`);
377
+ }
378
+ };
379
+ /** Immutable model authority with a journal staged independently of the PM view. */
380
+ var CanonicalSession = class {
381
+ currentDocument;
382
+ currentProjection;
383
+ sourceBlockIds;
384
+ currentVersion = 0;
385
+ saveTouched = /* @__PURE__ */ new Set();
386
+ saveStructure = "stable";
387
+ allocateIntentIds = createEditorIntentIdAllocator();
388
+ mode = { type: "editing" };
389
+ allocatedBlockIds = /* @__PURE__ */ new Set();
390
+ nextBlockId = 1;
391
+ sourceOwners = /* @__PURE__ */ new Map();
392
+ storyProjections = /* @__PURE__ */ new Map();
393
+ currentSelection = null;
394
+ applied = [];
395
+ groupingBoundary = 0;
396
+ lifecycle = { type: "committed" };
397
+ undone = [];
398
+ styles;
399
+ constructor({ document, projection, styles }) {
400
+ this.currentDocument = document;
401
+ this.currentProjection = projection;
402
+ this.sourceBlockIds = new Set(packageParagraphIds(document.package));
403
+ for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
404
+ this.advanceBlockId();
405
+ this.styles = styles == null ? styles : structuredClone(styles);
406
+ this.rememberSources(document);
407
+ }
408
+ setMode(mode) {
409
+ if (this.mode.type !== mode.type || this.mode.type === "suggesting" && mode.type === "suggesting" && this.mode.author !== mode.author) this.breakUndoGroup();
410
+ this.mode = mode.type === "editing" ? { type: "editing" } : {
411
+ type: "suggesting",
412
+ author: mode.author
413
+ };
414
+ }
415
+ hasStyle(styleId) {
416
+ return (this.styles ?? this.currentDocument.package.styles)?.styles.some((style) => style.styleId === styleId) ?? false;
417
+ }
418
+ advanceBlockId() {
419
+ while (this.allocatedBlockIds.has(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase())) this.nextBlockId += 1;
420
+ }
421
+ freshBlockId() {
422
+ if (this.nextBlockId > 2147483647) return refuse("The paragraph identity space is exhausted.");
423
+ return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
424
+ }
425
+ intentMode(document, intent) {
426
+ if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
427
+ type: "editing",
428
+ reservedBlockIds: this.allocatedBlockIds
429
+ };
430
+ const ids = this.allocateIntentIds(document, intent);
431
+ return this.mode.type === "editing" ? {
432
+ type: "editing",
433
+ newIds: ids.newIds,
434
+ reservedBlockIds: this.allocatedBlockIds
435
+ } : {
436
+ type: "suggesting",
437
+ reservedBlockIds: this.allocatedBlockIds,
438
+ revision: {
439
+ id: ids.revisionId,
440
+ author: this.mode.author,
441
+ date: (/* @__PURE__ */ new Date()).toISOString()
442
+ },
443
+ newIds: ids.newIds
444
+ };
445
+ }
446
+ intentNeedsIdentityIds(document, intent) {
447
+ let blockIds;
448
+ switch (intent.type) {
449
+ case "table": return true;
450
+ case "generateTOC":
451
+ case "setList":
452
+ case "formatParagraph": return false;
453
+ case "setHyperlink":
454
+ case "removeHyperlink":
455
+ case "insertHyperlink":
456
+ case "replaceFragment":
457
+ case "moveFragment": return true;
458
+ case "replaceText":
459
+ case "insertAtom":
460
+ case "formatRun":
461
+ if (intent.from.blockId !== intent.to.blockId) return true;
462
+ blockIds = [intent.from.blockId];
463
+ break;
464
+ case "splitParagraph":
465
+ if (intent.to !== void 0 && intent.at.blockId !== intent.to.blockId) return true;
466
+ blockIds = [intent.at.blockId];
467
+ break;
468
+ case "joinParagraphs":
469
+ blockIds = [intent.blockId, intent.nextBlockId];
470
+ break;
471
+ default: return intent;
472
+ }
473
+ const hasIdentity = (value) => {
474
+ if (typeof value !== "object" || value === null) return false;
475
+ if (Array.isArray(value)) return value.some(hasIdentity);
476
+ if ("id" in value && typeof value.id === "number" && ("author" in value || "sdtType" in value)) return true;
477
+ return Object.values(value).some(hasIdentity);
478
+ };
479
+ const story = intentStory(intent);
480
+ return blockIds.some((blockId) => {
481
+ const paragraph = document === this.currentDocument && story === OP_STORIES.MAIN ? this.currentProjection.paragraph(blockId)?.source : findStoryBody(document, story)?.content.find((block) => block.type === "paragraph" && block.paraId === blockId);
482
+ return paragraph !== void 0 && hasIdentity(paragraph);
483
+ });
484
+ }
485
+ rememberSources(document) {
486
+ for (const block of document.package.document.content) if (block.type === "paragraph" && block.paraId !== void 0) this.sourceOwners.set(block.paraId, block);
487
+ }
488
+ get document() {
489
+ if (this.isComposing) throw new CanonicalSessionError({
490
+ gap: CANONICAL_GAP.dispatch,
491
+ reason: "refused",
492
+ message: "Composition must finish before taking a snapshot."
493
+ });
494
+ return this.currentDocument;
495
+ }
496
+ captureSaveSnapshot() {
497
+ if (this.isComposing) throw new CanonicalSessionError({
498
+ gap: CANONICAL_GAP.save,
499
+ reason: "refused",
500
+ message: "Composition must finish before saving the canonical document."
501
+ });
502
+ const retainedBlockIdKeys = new Set([...this.sourceBlockIds, ...packageParagraphIds(this.currentDocument.package)].map((blockId) => blockId.toUpperCase()));
503
+ return {
504
+ document: cloneDocumentWithParagraphPropertySources(this.currentDocument),
505
+ version: this.currentVersion,
506
+ changedBlockIds: [...this.saveTouched].filter((blockId) => retainedBlockIdKeys.has(blockId.toUpperCase())),
507
+ structure: this.saveStructure
508
+ };
509
+ }
510
+ get projection() {
511
+ return this.currentProjection;
512
+ }
513
+ get selection() {
514
+ return this.currentSelection;
515
+ }
516
+ get version() {
517
+ return this.currentVersion;
518
+ }
519
+ get canUndo() {
520
+ return this.applied.length > 0;
521
+ }
522
+ get canRedo() {
523
+ return this.undone.length > 0;
524
+ }
525
+ get isComposing() {
526
+ return this.lifecycle.type === "composing";
527
+ }
528
+ breakUndoGroup() {
529
+ this.groupingBoundary += 1;
530
+ }
531
+ beginComposition() {
532
+ if (this.isComposing) return refuse("A canonical composition is already pending.");
533
+ this.lifecycle = { type: "composing" };
534
+ this.breakUndoGroup();
535
+ return Result.ok(void 0);
536
+ }
537
+ endComposition() {
538
+ this.lifecycle = { type: "committed" };
539
+ this.breakUndoGroup();
540
+ }
541
+ checkState(state, projection = this.projection) {
542
+ if (this.isComposing) return refuse("Composition must finish before committing another edit.");
543
+ if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
544
+ return Result.ok(void 0);
545
+ }
546
+ prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", compositionPhase, time = Date.now() }) {
547
+ const projected = this.projectStory(story);
548
+ if (projected.isErr()) return projected;
549
+ const projection = projected.value;
550
+ const checked = this.checkState(state, projection);
551
+ if (checked.isErr()) return checked;
552
+ if (!Number.isFinite(time)) return refuse("The input time is invalid.");
553
+ if (from > to || hasIllegalXmlCharacters(text) || /[\t\r\n]/u.test(text)) return refuse("Canonical input accepts a well-formed text replacement.");
554
+ if (from === to && text.length === 0) return noChange("The input makes no text change.");
555
+ const start = projection.inputAddressAt(from);
556
+ if (start.isErr()) return start;
557
+ const end = projection.inputAddressAt(to);
558
+ if (end.isErr()) return end;
559
+ const preSelection = projection.selectionAt(state);
560
+ if (preSelection.isErr()) return preSelection;
561
+ const intent = {
562
+ type: "replaceText",
563
+ from: start.value,
564
+ to: end.value,
565
+ text,
566
+ ...state.storedMarks === null ? {} : { runPropsPatch: runFormattingPatchFromMarks(state.selection.$from.marks(), state.storedMarks) }
567
+ };
568
+ const compiled = compileEditorIntent(this.currentDocument, {
569
+ intent,
570
+ mode: this.intentMode(this.currentDocument, intent)
571
+ });
572
+ if (compiled.isErr()) return refuse(compiled.error.message);
573
+ const { ops, selection: caret } = compiled.value;
574
+ const postSelection = {
575
+ type: "text",
576
+ anchor: caret,
577
+ head: caret
578
+ };
579
+ const preDocument = this.currentDocument;
580
+ const isCaret = state.selection.empty;
581
+ const deletion = isCaret && text.length === 0 && (semantic === "deleteBackward" || semantic === "deleteForward") ? deletionRange(state, semantic === "deleteBackward" ? "backward" : "forward") : null;
582
+ const grouping = isCaret && (semantic === "typing" && from === to && from === state.selection.head || deletion?.isOk() && deletion.value.from === from && deletion.value.to === to) ? "run" : "isolated";
583
+ return this.stage({
584
+ state,
585
+ story,
586
+ ops,
587
+ selection: postSelection,
588
+ origin: "input",
589
+ onPublish: (inverse, version) => {
590
+ const entry = {
591
+ type: "applied",
592
+ ops,
593
+ inverse,
594
+ preSelection: preSelection.value,
595
+ postSelection,
596
+ preDocument,
597
+ postDocument: this.currentDocument,
598
+ version,
599
+ origin: "input",
600
+ semantic,
601
+ grouping: compositionPhase === "correction" ? "compositionCorrection" : grouping,
602
+ time,
603
+ boundary: this.groupingBoundary
604
+ };
605
+ const group = this.applied.at(-1);
606
+ const previous = group?.entries.at(-1);
607
+ if (group !== void 0 && previous !== void 0 && continuesGroup(previous, entry)) {
608
+ group.entries.push(entry);
609
+ group.postSelection = postSelection;
610
+ } else this.applied.push({
611
+ entries: [entry],
612
+ undo: { type: "entries" },
613
+ preSelection: preSelection.value,
614
+ postSelection
615
+ });
616
+ this.undone.length = 0;
617
+ }
618
+ });
619
+ }
620
+ prepareIntent(state, intent) {
621
+ return this.prepareIntents(state, { intents: [intent] });
622
+ }
623
+ prepareIntents(state, { intents, resourceOps = [], semantic = "structure" }) {
624
+ const first = intents.at(0);
625
+ if (first === void 0) return noChange("The intent batch is empty.");
626
+ const story = intentStory(first);
627
+ if (intents.some((intent) => !sameStory(story, intentStory(intent)))) return refuse("A compound input must address one story.");
628
+ const projected = this.projectStory(story);
629
+ if (projected.isErr()) return projected;
630
+ const checked = this.checkState(state, projected.value);
631
+ if (checked.isErr()) return checked;
632
+ const preSelection = projected.value.selectionAt(state);
633
+ if (preSelection.isErr()) return preSelection;
634
+ let document = this.currentDocument;
635
+ let postSelection = preSelection.value;
636
+ const ops = [...resourceOps];
637
+ const edits = [];
638
+ if (resourceOps.length > 0) {
639
+ const imported = applyDocumentOps(document, resourceOps);
640
+ if (imported.isErr()) return refuse(imported.error.message);
641
+ document = imported.value.document;
642
+ edits.push(imported.value);
643
+ }
644
+ for (const intent of intents) {
645
+ if (this.mode.type === "suggesting" && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink" || intent.type === "generateTOC")) return Result.err(new CanonicalSessionError({
646
+ gap: CANONICAL_GAP.trackedHyperlinkResolution,
647
+ reason: "refused",
648
+ message: "Hyperlink and TOC suggestions require serializable wrapper review provenance."
649
+ }));
650
+ const compiled = compileEditorIntent(document, {
651
+ intent,
652
+ mode: this.intentMode(document, intent),
653
+ firstBlockId: this.nextBlockId
654
+ });
655
+ if (compiled.isErr()) return refuse(compiled.error.message);
656
+ const applied = applyDocumentOps(document, compiled.value.ops);
657
+ if (applied.isErr()) return refuse(applied.error.message);
658
+ document = applied.value.document;
659
+ edits.push(applied.value);
660
+ ops.push(...compiled.value.ops);
661
+ if (intent.type === "generateTOC") {
662
+ const mapping = {
663
+ at: intent.at,
664
+ after: compiled.value.selection,
665
+ ops: compiled.value.ops
666
+ };
667
+ postSelection = Object.assign({}, postSelection, {
668
+ anchor: mapTocSelection(postSelection.anchor, mapping),
669
+ head: mapTocSelection(postSelection.head, mapping)
670
+ });
671
+ } else if (intent.type !== "setHyperlink" && intent.type !== "removeHyperlink" && intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
672
+ type: "text",
673
+ anchor: compiled.value.selection,
674
+ head: compiled.value.selection
675
+ };
676
+ }
677
+ const stagedApplied = {
678
+ ...combineEdits(this.currentDocument, edits),
679
+ revisions: edits.flatMap(({ revisions }) => revisions)
680
+ };
681
+ return this.prepareJournalledOps({
682
+ state,
683
+ ops,
684
+ postSelection,
685
+ stagedApplied,
686
+ semantic
687
+ });
688
+ }
689
+ prepareOps(state, ops, postSelection) {
690
+ return this.prepareJournalledOps({
691
+ state,
692
+ ops,
693
+ postSelection
694
+ });
695
+ }
696
+ prepareJournalledOps({ state, ops, postSelection, stagedApplied, semantic = "structure" }) {
697
+ const story = postSelection.anchor.story;
698
+ const projected = this.projectStory(story);
699
+ if (projected.isErr()) return projected;
700
+ const preSelection = projected.value.selectionAt(state);
701
+ if (preSelection.isErr()) return preSelection;
702
+ if (ops.length === 0 || stagedApplied?.inverse.length === 0) return noChange("The intent makes no document change.");
703
+ const preDocument = this.currentDocument;
704
+ return this.stage({
705
+ state,
706
+ story,
707
+ ops,
708
+ selection: postSelection,
709
+ origin: "input",
710
+ ...stagedApplied === void 0 ? {} : { stagedApplied },
711
+ onPublish: (inverse, version) => {
712
+ const entry = {
713
+ type: "applied",
714
+ ops,
715
+ inverse,
716
+ preSelection: preSelection.value,
717
+ postSelection,
718
+ preDocument,
719
+ postDocument: this.currentDocument,
720
+ version,
721
+ origin: "input",
722
+ semantic,
723
+ grouping: "isolated",
724
+ time: Date.now(),
725
+ boundary: this.groupingBoundary
726
+ };
727
+ this.applied.push({
728
+ entries: [entry],
729
+ undo: { type: "entries" },
730
+ preSelection: preSelection.value,
731
+ postSelection
732
+ });
733
+ this.undone.length = 0;
734
+ }
735
+ });
736
+ }
737
+ prepareSplit(state) {
738
+ const from = this.projection.inputAddressAt(state.selection.from);
739
+ if (from.isErr()) return from;
740
+ const to = this.projection.inputAddressAt(state.selection.to);
741
+ if (to.isErr()) return to;
742
+ const source = this.projection.paragraph(from.value.blockId);
743
+ const nextStyle = (this.styles ?? this.currentDocument.package.styles)?.styles.find(({ styleId }) => styleId === source?.source.formatting?.styleId)?.next;
744
+ const fresh = this.freshBlockId();
745
+ if (fresh.isErr()) return fresh;
746
+ const intents = [{
747
+ type: "splitParagraph",
748
+ at: from.value,
749
+ to: to.value,
750
+ newBlockId: fresh.value
751
+ }];
752
+ if (state.selection.empty && source !== void 0 && from.value.offset === source.text.length && nextStyle !== void 0 && nextStyle !== source.source.formatting?.styleId) intents.push({
753
+ type: "formatParagraph",
754
+ at: {
755
+ ...from.value,
756
+ offset: 0
757
+ },
758
+ patch: { styleId: nextStyle }
759
+ });
760
+ return this.prepareIntents(state, { intents });
761
+ }
762
+ prepareJoin(state, direction) {
763
+ const checked = this.checkState(state);
764
+ if (checked.isErr()) return checked;
765
+ if (!state.selection.empty) return refuse("Join requires a collapsed text selection.");
766
+ const at = this.projection.inputAddressAt(state.selection.from);
767
+ if (at.isErr()) return at;
768
+ const groups = editorParagraphGroups(this.currentDocument, at.value.story);
769
+ const index = groups.findIndex(({ paragraphs }) => paragraphs.some((paragraph) => paragraph.paraId === at.value.blockId));
770
+ if (index < 0 || direction === "backward" && index === 0) return noChange("There is no adjacent paragraph to join.");
771
+ const firstGroup = groups.at(direction === "backward" ? index - 1 : index);
772
+ const secondGroup = groups.at(direction === "backward" ? index : index + 1);
773
+ const first = firstGroup?.paragraphs.at(-1);
774
+ const second = secondGroup?.paragraphs.at(0);
775
+ if (!first?.paraId || !second?.paraId) return noChange("There is no adjacent paragraph to join.");
776
+ if (!isCanonicalJoinBoundary(state, direction)) return refuse("Join requires a caret at a paragraph boundary.");
777
+ return this.prepareIntent(state, {
778
+ type: "joinParagraphs",
779
+ story: at.value.story,
780
+ blockId: first.paraId,
781
+ nextBlockId: second.paraId
782
+ });
783
+ }
784
+ prepareResolve(state, { revisionIds, resolution }) {
785
+ const checked = this.checkState(state);
786
+ if (checked.isErr()) return checked;
787
+ const preSelection = this.projection.selectionAt(state);
788
+ if (preSelection.isErr()) return preSelection;
789
+ const ops = [{
790
+ type: DOCUMENT_OP_TYPES.RESOLVE_REVISION,
791
+ story: OP_STORIES.MAIN,
792
+ revisionIds,
793
+ decision: resolution
794
+ }];
795
+ const applied = applyDocumentOps(this.currentDocument, ops);
796
+ if (applied.isErr()) return refuse(applied.error.message);
797
+ const projected = project({
798
+ document: applied.value.document,
799
+ styles: this.styles
800
+ });
801
+ if (projected.isErr()) return projected;
802
+ const surviving = projected.value.paragraph(preSelection.value.head.blockId);
803
+ const first = applied.value.document.package.document.content.at(0);
804
+ if (first?.type !== "paragraph" || first.paraId === void 0) return refuse("Resolution left no paragraph.");
805
+ let offset = surviving ? Math.min(preSelection.value.head.offset, surviving.text.length) : 0;
806
+ if (surviving && splitsSurrogatePair(surviving.text, offset)) offset -= 1;
807
+ const caret = {
808
+ story: OP_STORIES.MAIN,
809
+ blockId: surviving?.blockId ?? first.paraId,
810
+ offset
811
+ };
812
+ return this.prepareJournalledOps({
813
+ state,
814
+ ops,
815
+ postSelection: {
816
+ type: "text",
817
+ anchor: caret,
818
+ head: caret
819
+ },
820
+ stagedApplied: applied.value
821
+ });
822
+ }
823
+ projectStory(story) {
824
+ if (story === OP_STORIES.MAIN) return Result.ok(this.projection);
825
+ const key = storyProjectionKey(story);
826
+ const body = findStoryBody(this.currentDocument, story);
827
+ if (body === void 0) {
828
+ this.storyProjections.delete(key);
829
+ return refuse("The canonical story no longer exists.");
830
+ }
831
+ const cached = this.storyProjections.get(key);
832
+ const theme = this.currentDocument.package.theme;
833
+ if (cached?.content === body.content && cached.theme === theme) return Result.ok(cached.projection);
834
+ const projected = project({
835
+ document: this.currentDocument,
836
+ styles: this.styles,
837
+ story
838
+ });
839
+ if (projected.isOk()) this.storyProjections.set(key, {
840
+ content: body.content,
841
+ theme,
842
+ projection: projected.value
843
+ });
844
+ return projected;
845
+ }
846
+ prepareOperations(state, ops) {
847
+ if (ops.length === 0) return refuse("The operation batch is empty.");
848
+ const selected = this.projection.selectionAt(state);
849
+ if (selected.isErr()) return selected;
850
+ const preDocument = this.currentDocument;
851
+ return this.stage({
852
+ state,
853
+ ops,
854
+ selection: selected.value,
855
+ origin: "input",
856
+ onPublish: (inverse, version) => {
857
+ this.applied.push({
858
+ entries: [{
859
+ type: "applied",
860
+ ops,
861
+ inverse,
862
+ preSelection: selected.value,
863
+ postSelection: selected.value,
864
+ preDocument,
865
+ postDocument: this.currentDocument,
866
+ version,
867
+ origin: "input",
868
+ semantic: "structure",
869
+ grouping: "isolated",
870
+ time: Date.now(),
871
+ boundary: this.groupingBoundary
872
+ }],
873
+ undo: { type: "entries" },
874
+ preSelection: selected.value,
875
+ postSelection: selected.value
876
+ });
877
+ this.undone.length = 0;
878
+ }
879
+ });
880
+ }
881
+ prepareUndo(state, story = OP_STORIES.MAIN) {
882
+ const group = this.applied.at(-1);
883
+ if (group === void 0) return refuse("There is no canonical edit to undo.");
884
+ const propertySourceDocument = group.entries.at(0)?.preDocument;
885
+ return this.stage({
886
+ state,
887
+ story,
888
+ ops: group.undo.type === "replayed" ? group.undo.inverse : group.entries.toReversed().flatMap((entry) => entry.inverse),
889
+ selection: group.preSelection,
890
+ origin: "undo",
891
+ ...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
892
+ onPublish: (redoOps) => {
893
+ this.applied.pop();
894
+ this.undone.push({
895
+ type: "undone",
896
+ group,
897
+ redoOps
898
+ });
899
+ this.breakUndoGroup();
900
+ }
901
+ });
902
+ }
903
+ prepareRedo(state, story = OP_STORIES.MAIN) {
904
+ const undone = this.undone.at(-1);
905
+ if (undone === void 0) return refuse("There is no canonical edit to redo.");
906
+ const propertySourceDocument = undone.group.entries.at(-1)?.postDocument;
907
+ return this.stage({
908
+ state,
909
+ story,
910
+ ops: undone.redoOps,
911
+ selection: undone.group.postSelection,
912
+ origin: "redo",
913
+ ...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
914
+ onPublish: (inverse) => {
915
+ this.undone.pop();
916
+ this.applied.push({
917
+ entries: undone.group.entries,
918
+ undo: {
919
+ type: "replayed",
920
+ inverse
921
+ },
922
+ preSelection: undone.group.preSelection,
923
+ postSelection: undone.group.postSelection
924
+ });
925
+ this.breakUndoGroup();
926
+ }
927
+ });
928
+ }
929
+ stage({ state, ops, selection, origin, onPublish, propertySourceDocument, stagedApplied, story = OP_STORIES.MAIN, previousProjection }) {
930
+ const previous = previousProjection === void 0 ? this.projectStory(story) : Result.ok(previousProjection);
931
+ if (previous.isErr()) return previous;
932
+ const checked = this.checkState(state, previous.value);
933
+ if (checked.isErr()) return checked;
934
+ const applied = stagedApplied === void 0 ? applyDocumentOps(this.currentDocument, ops) : Result.ok(stagedApplied);
935
+ if (applied.isErr()) return refuse(applied.error.message);
936
+ if (applied.value.inverse.length === 0) return noChange("The intent makes no document change.");
937
+ const changed = changedStories({
938
+ document: applied.value.document,
939
+ previous: this.currentDocument
940
+ });
941
+ const unsupported = unsupportedSeedReason(applied.value.document, changed);
942
+ if (unsupported !== null) return refuse(unsupported);
943
+ preservePropertySources({
944
+ target: applied.value.document,
945
+ source: this.currentDocument,
946
+ stories: changed
947
+ });
948
+ if (propertySourceDocument !== void 0) preservePropertySources({
949
+ target: applied.value.document,
950
+ source: propertySourceDocument
951
+ });
952
+ const stagedSources = new Map(this.sourceOwners);
953
+ for (const block of applied.value.document.package.document.content) {
954
+ if (block.type !== "paragraph" || block.paraId === void 0) continue;
955
+ const source = stagedSources.get(block.paraId);
956
+ if (source && block !== source) copyParagraphPropertySource(block, source);
957
+ }
958
+ const projectedStory = findStoryBody(applied.value.document, story) ? story : OP_STORIES.MAIN;
959
+ const projected = project({
960
+ document: applied.value.document,
961
+ styles: this.styles,
962
+ story: projectedStory
963
+ });
964
+ if (projected.isErr()) return projected;
965
+ const bodyProjection = (() => {
966
+ if (sameStory(projectedStory, OP_STORIES.MAIN)) return projected;
967
+ if (ops.some(operationChangesBodyProjection)) return project({
968
+ document: applied.value.document,
969
+ styles: this.styles
970
+ });
971
+ return Result.ok(this.currentProjection);
972
+ })();
973
+ if (bodyProjection.isErr()) return bodyProjection;
974
+ const transaction = state.tr;
975
+ const before = state.doc;
976
+ const after = projected.value.doc;
977
+ let prefix = 0;
978
+ let from = 0;
979
+ while (prefix < before.childCount && prefix < after.childCount && before.child(prefix).eq(after.child(prefix))) {
980
+ from += before.child(prefix).nodeSize;
981
+ prefix += 1;
982
+ }
983
+ let oldEnd = before.childCount;
984
+ let newEnd = after.childCount;
985
+ let to = before.content.size;
986
+ while (oldEnd > prefix && newEnd > prefix && before.child(oldEnd - 1).eq(after.child(newEnd - 1))) {
987
+ oldEnd -= 1;
988
+ newEnd -= 1;
989
+ to -= before.child(oldEnd).nodeSize;
990
+ }
991
+ if (oldEnd !== prefix || newEnd !== prefix) {
992
+ const replacement = [];
993
+ for (let index = prefix; index < newEnd; index += 1) replacement.push(after.child(index));
994
+ transaction.replaceWith(from, to, Fragment.fromArray(replacement));
995
+ }
996
+ if (!transaction.doc.eq(projected.value.doc)) transaction.replaceWith(0, transaction.doc.content.size, projected.value.doc.content);
997
+ if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
998
+ const anchor = projected.value.positionAt(selection.anchor);
999
+ const head = projected.value.positionAt(selection.head);
1000
+ switch (selection.type) {
1001
+ case "all":
1002
+ transaction.setSelection(new AllSelection(transaction.doc));
1003
+ break;
1004
+ case "text":
1005
+ if (anchor.isOk() && head.isOk()) {
1006
+ transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
1007
+ break;
1008
+ }
1009
+ transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
1010
+ break;
1011
+ default: {
1012
+ const exhaustive = selection.type;
1013
+ return panic(`Unknown canonical selection type: ${exhaustive}`);
1014
+ }
1015
+ }
1016
+ }
1017
+ transaction.setMeta(CANONICAL_PROJECTION_META, {
1018
+ type: "canonical",
1019
+ origin,
1020
+ version: this.version + 1
1021
+ });
1022
+ if (ops.some(operationChangesPackage)) markPackageChange(transaction);
1023
+ transaction.setMeta("addToHistory", false);
1024
+ const baseVersion = this.version;
1025
+ const baseBoundary = this.groupingBoundary;
1026
+ authorizedProjections.set(transaction, {
1027
+ session: this,
1028
+ version: baseVersion,
1029
+ doc: projected.value.doc
1030
+ });
1031
+ return Result.ok({
1032
+ transaction,
1033
+ document: applied.value.document,
1034
+ projection: projected.value,
1035
+ bodyProjection: bodyProjection.value,
1036
+ touched: applied.value.touched,
1037
+ version: baseVersion + 1,
1038
+ origin,
1039
+ publish: () => {
1040
+ if (this.isComposing || this.groupingBoundary !== baseBoundary) return refuse("The staged canonical commit crossed a composition or selection boundary.");
1041
+ if (this.version !== baseVersion) return refuse("The staged canonical commit is stale.");
1042
+ if (!isCanonicalProjectionTransaction(transaction, this)) return refuse("The staged canonical projection was changed after preparation.");
1043
+ for (const blockId of applied.value.touched.inserted) this.allocatedBlockIds.add(blockId.toUpperCase());
1044
+ for (const op of ops) if (op.type === DOCUMENT_OP_TYPES.SPLIT_BLOCK) this.allocatedBlockIds.add(op.newBlockId.toUpperCase());
1045
+ this.advanceBlockId();
1046
+ this.rememberSources(applied.value.document);
1047
+ if (projectedStory !== OP_STORIES.MAIN) {
1048
+ const body = findStoryBody(applied.value.document, projectedStory);
1049
+ if (body === void 0) panic("A staged story projection lost its source.");
1050
+ this.storyProjections.set(storyProjectionKey(projectedStory), {
1051
+ content: body.content,
1052
+ theme: applied.value.document.package.theme,
1053
+ projection: projected.value
1054
+ });
1055
+ }
1056
+ this.currentSelection = selection;
1057
+ this.currentDocument = applied.value.document;
1058
+ this.currentProjection = bodyProjection.value;
1059
+ for (const ids of [
1060
+ applied.value.touched.modified,
1061
+ applied.value.touched.inserted,
1062
+ applied.value.touched.removed
1063
+ ]) for (const id of ids) this.saveTouched.add(id);
1064
+ if (applied.value.touched.inserted.length > 0 || applied.value.touched.removed.length > 0 || ops.some(operationChangesPackage) || ops.some(operationChangesStructure)) this.saveStructure = "changed";
1065
+ this.currentVersion = baseVersion + 1;
1066
+ onPublish(applied.value.inverse, this.currentVersion);
1067
+ return Result.ok(void 0);
1068
+ }
1069
+ });
1070
+ }
1071
+ };
1072
+ const createCanonicalSession = (document, styles) => {
1073
+ const unsupported = unsupportedSeedReason(document);
1074
+ if (unsupported !== null) return refuse(unsupported);
1075
+ const owned = cloneDocumentWithParagraphPropertySources(document);
1076
+ const normalized = normalizeForOps(owned);
1077
+ preservePropertySources({
1078
+ target: normalized,
1079
+ source: owned
1080
+ });
1081
+ const validated = validateOpsDocument(normalized);
1082
+ if (validated.isErr()) return refuse(validated.error.message);
1083
+ const projected = project({
1084
+ document: normalized,
1085
+ styles
1086
+ });
1087
+ if (projected.isErr()) return projected;
1088
+ return Result.ok(new CanonicalSession({
1089
+ document: normalized,
1090
+ projection: projected.value,
1091
+ styles
1092
+ }));
1093
+ };
1094
+ const hasDeletedParagraphMark = (node) => {
1095
+ const mark = node.attrs["pPrMark"];
1096
+ return typeof mark === "object" && mark !== null && "kind" in mark && (mark.kind === "del" || mark.kind === "moveFrom");
1097
+ };
1098
+ /** Visible graphemes concatenate across retained deletions and deleted paragraph marks. */
1099
+ const visibleDeletionContext = (state) => {
1100
+ const { selection } = state;
1101
+ const index = selection.$from.index(0);
1102
+ let first = index;
1103
+ while (first > 0 && hasDeletedParagraphMark(state.doc.child(first - 1))) first -= 1;
1104
+ let last = index;
1105
+ while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
1106
+ let text = "";
1107
+ const physicalStarts = [];
1108
+ const physicalEnds = [];
1109
+ let visibleOffset = 0;
1110
+ let issue;
1111
+ state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
1112
+ if (paragraphIndex < first || paragraphIndex > last) return;
1113
+ const native = canonicalNativeCells(paragraph);
1114
+ if (native.isErr()) {
1115
+ issue = new CanonicalSessionError({
1116
+ gap: CANONICAL_GAP.dispatch,
1117
+ reason: "refused",
1118
+ message: native.error.message
1119
+ });
1120
+ return;
1121
+ }
1122
+ for (const cell of native.value) {
1123
+ if (cell.type === "zeroWidth" || cell.node.marks.some((mark) => mark.type.name === "deletion")) continue;
1124
+ const value = cell.type === "text" ? cell.text : "";
1125
+ const from = paragraphOffset + 1 + cell.from;
1126
+ const to = paragraphOffset + 1 + cell.to;
1127
+ if (cell.type !== "text" && selection.from > from && selection.from < to) {
1128
+ issue = new CanonicalSessionError({
1129
+ gap: CANONICAL_GAP.dispatch,
1130
+ reason: "refused",
1131
+ message: "Character deletion cannot start inside an inline atom."
1132
+ });
1133
+ return;
1134
+ }
1135
+ for (let unit = 0; unit < value.length; unit += 1) {
1136
+ const position = from + (cell.type === "text" ? unit : 0);
1137
+ physicalStarts.push(position);
1138
+ text += value.charAt(unit);
1139
+ physicalEnds.push(cell.type === "text" ? position + 1 : to);
1140
+ if (position < selection.from) visibleOffset += 1;
1141
+ }
1142
+ }
1143
+ });
1144
+ return issue === void 0 ? Result.ok({
1145
+ text,
1146
+ physicalStarts,
1147
+ physicalEnds,
1148
+ visibleOffset
1149
+ }) : Result.err(issue);
1150
+ };
1151
+ const isCanonicalJoinBoundary = (state, direction) => {
1152
+ if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
1153
+ const context = visibleDeletionContext(state);
1154
+ if (context.isErr()) return false;
1155
+ const { text, visibleOffset } = context.value;
1156
+ return visibleOffset === (direction === "backward" ? 0 : text.length);
1157
+ };
1158
+ /** Native character deletion consumes one whole visible grapheme. */
1159
+ const deletionRange = (state, direction) => {
1160
+ const { selection } = state;
1161
+ if (!(selection instanceof TextSelection)) return refuse("Canonical deletion requires a text selection.");
1162
+ if (!selection.empty) return Result.ok({
1163
+ from: selection.from,
1164
+ to: selection.to
1165
+ });
1166
+ const parent = selection.$from.parent;
1167
+ const offset = selection.$from.parentOffset;
1168
+ if ((direction === "backward" ? parent.childBefore(offset).node : parent.childAfter(offset).node)?.type.name === "noteMarker") return refuse("Automatic note marks cannot be deleted as characters.");
1169
+ const context = visibleDeletionContext(state);
1170
+ if (context.isErr()) return context;
1171
+ const { text, physicalStarts, physicalEnds, visibleOffset } = context.value;
1172
+ if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
1173
+ if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
1174
+ if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
1175
+ let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
1176
+ while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
1177
+ const from = physicalStarts.at(direction === "backward" ? boundary : visibleOffset);
1178
+ const to = physicalEnds.at((direction === "backward" ? visibleOffset : boundary) - 1);
1179
+ if (from === void 0 || to === void 0) panic("Canonical deletion lost a grapheme boundary.");
1180
+ return Result.ok({
1181
+ from,
1182
+ to
1183
+ });
1184
+ };
1185
+ //#endregion
1186
+ export { CANONICAL_PROJECTION_META, CanonicalSessionError, createCanonicalSession, deletionRange, isCanonicalJoinBoundary, isCanonicalProjectionTransaction, publishCanonicalProjection };