@stll/folio-core 0.55.0 → 0.57.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 (225) hide show
  1. package/dist/ai-edits/apply.js +214 -52
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/headless.js +2 -2
  5. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  6. package/dist/ai-edits/types.d.ts +7 -1
  7. package/dist/ai-suggestions/apply.js +20 -1
  8. package/dist/compare/compare.js +1 -0
  9. package/dist/compare/inline-atoms.js +40 -16
  10. package/dist/compare/inline-provenance.js +12 -5
  11. package/dist/compare/reproducible-package.d.ts +0 -17
  12. package/dist/compare/reproducible-package.js +12 -18
  13. package/dist/compat/eigenpal.d.ts +2 -1
  14. package/dist/compat/eigenpal.js +2 -1
  15. package/dist/controller/canonicalClipboard.d.ts +23 -0
  16. package/dist/controller/canonicalClipboard.js +482 -0
  17. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  18. package/dist/controller/canonicalClipboardResources.js +382 -0
  19. package/dist/controller/canonicalComments.d.ts +41 -0
  20. package/dist/controller/canonicalComments.js +152 -0
  21. package/dist/controller/canonicalComposition.d.ts +24 -3
  22. package/dist/controller/canonicalComposition.js +208 -48
  23. package/dist/controller/canonicalInlineProjection.d.ts +118 -0
  24. package/dist/controller/canonicalInlineProjection.js +476 -0
  25. package/dist/controller/canonicalInput.d.ts +13 -4
  26. package/dist/controller/canonicalInput.js +62 -14
  27. package/dist/controller/canonicalInputTimer.d.ts +16 -0
  28. package/dist/controller/canonicalInputTimer.js +34 -0
  29. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  30. package/dist/controller/canonicalPublicOperations.js +648 -0
  31. package/dist/controller/canonicalSelection.d.ts +18 -0
  32. package/dist/controller/canonicalSelection.js +32 -0
  33. package/dist/controller/canonicalSession.d.ts +27 -6
  34. package/dist/controller/canonicalSession.js +298 -116
  35. package/dist/controller/canonicalStoryEditor.d.ts +7 -3
  36. package/dist/controller/canonicalStoryEditor.js +27 -2
  37. package/dist/controller/canonicalStructure.js +57 -4
  38. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  39. package/dist/controller/canonicalTocSelection.js +43 -0
  40. package/dist/controller/folioEditor.d.ts +2 -9
  41. package/dist/controller/folioEditor.js +18 -4
  42. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  43. package/dist/controller/headerFooterEditorManager.js +26 -11
  44. package/dist/controller/hiddenEditorApi.d.ts +24 -3
  45. package/dist/controller/hiddenEditorApi.js +8 -0
  46. package/dist/controller/hiddenEditorManager.d.ts +5 -1
  47. package/dist/controller/hiddenEditorManager.js +378 -43
  48. package/dist/controller/noteEditorManager.d.ts +2 -1
  49. package/dist/controller/noteEditorManager.js +29 -13
  50. package/dist/document-operations.d.ts +8 -1
  51. package/dist/document-operations.js +6 -4
  52. package/dist/docx/blockPlainText.d.ts +1 -1
  53. package/dist/docx/blockPlainText.js +11 -3
  54. package/dist/docx/builtInStyles.js +13 -6
  55. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  56. package/dist/docx/canonicalResourceSave.js +27 -0
  57. package/dist/docx/canonicalSave.d.ts +29 -0
  58. package/dist/docx/canonicalSave.js +101 -0
  59. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  60. package/dist/docx/canonicalStoryRepack.js +38 -5
  61. package/dist/docx/containerChildren.gen.d.ts +3 -1
  62. package/dist/docx/containerChildren.gen.js +33 -1
  63. package/dist/docx/documentParser.d.ts +1 -13
  64. package/dist/docx/documentParser.js +1 -47
  65. package/dist/docx/ensureParaIds.js +10 -4
  66. package/dist/docx/fieldParser.js +14 -2
  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 +26 -1
  72. package/dist/docx/headerFooterVerbatim.js +81 -3
  73. package/dist/docx/hyperlinkParser.d.ts +1 -1
  74. package/dist/docx/hyperlinkParser.js +20 -1
  75. package/dist/docx/imageParser.d.ts +13 -1
  76. package/dist/docx/imageParser.js +20 -11
  77. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  78. package/dist/docx/inlineWrapperContent.js +4 -4
  79. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  80. package/dist/docx/linkedSdtPreservation.js +37 -0
  81. package/dist/docx/metadataPrivacy.js +6 -1
  82. package/dist/docx/packageParts.js +9 -3
  83. package/dist/docx/paragraphParser.js +88 -67
  84. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  85. package/dist/docx/paragraphPropertySource.js +109 -2
  86. package/dist/docx/parser.js +2 -1
  87. package/dist/docx/preservedRunContent.js +2 -1
  88. package/dist/docx/relsParser.js +26 -2
  89. package/dist/docx/removeHeaderFooterParts.js +17 -6
  90. package/dist/docx/rezip.d.ts +49 -5
  91. package/dist/docx/rezip.js +754 -235
  92. package/dist/docx/saveDiagnostics.d.ts +11 -0
  93. package/dist/docx/selectiveSave.d.ts +2 -1
  94. package/dist/docx/selectiveSave.js +42 -11
  95. package/dist/docx/selectiveXmlPatch.js +7 -5
  96. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  97. package/dist/docx/serializer/commentSerializer.js +7 -3
  98. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  99. package/dist/docx/serializer/documentSerializer.js +29 -10
  100. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  101. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  102. package/dist/docx/server/materializeYjsDocx.js +14 -6
  103. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  104. package/dist/docx/storyBlockReplay.d.ts +21 -1
  105. package/dist/docx/storyBlockReplay.js +113 -9
  106. package/dist/docx/storyPlainText.d.ts +20 -0
  107. package/dist/docx/storyPlainText.js +23 -0
  108. package/dist/docx/structuralXmlPatch.js +10 -22
  109. package/dist/docx/styleParser.d.ts +5 -1
  110. package/dist/docx/styleParser.js +1 -1
  111. package/dist/docx/tableParser.d.ts +1 -8
  112. package/dist/docx/tableParser.js +1 -28
  113. package/dist/docx/textBoxParser.d.ts +1 -5
  114. package/dist/docx/textBoxParser.js +1 -20
  115. package/dist/docx/unzip.js +6 -1
  116. package/dist/i18n/messages/ar.gen.js +4 -0
  117. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  118. package/dist/i18n/messages/catalogs.gen.js +68 -0
  119. package/dist/i18n/messages/cs.gen.js +4 -0
  120. package/dist/i18n/messages/de.gen.js +4 -0
  121. package/dist/i18n/messages/en.gen.js +4 -0
  122. package/dist/i18n/messages/es.gen.js +4 -0
  123. package/dist/i18n/messages/et.gen.js +4 -0
  124. package/dist/i18n/messages/fr.gen.js +4 -0
  125. package/dist/i18n/messages/he.gen.js +4 -0
  126. package/dist/i18n/messages/hi.gen.js +4 -0
  127. package/dist/i18n/messages/hu.gen.js +4 -0
  128. package/dist/i18n/messages/lt.gen.js +4 -0
  129. package/dist/i18n/messages/lv.gen.js +4 -0
  130. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  131. package/dist/i18n/messages/pl.gen.js +4 -0
  132. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  133. package/dist/i18n/messages/sk.gen.js +4 -0
  134. package/dist/i18n/messages/tr.gen.js +4 -0
  135. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +2 -1
  138. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  139. package/dist/internal/acceptedBlockProjection.js +45 -0
  140. package/dist/internal/revisionResolutionInline.js +25 -3
  141. package/dist/managers/DocumentLoaderManager.js +2 -1
  142. package/dist/markdown/escape.d.ts +3 -1
  143. package/dist/markdown/escape.js +9 -2
  144. package/dist/markdown/renderBlock.js +5 -120
  145. package/dist/markdown/renderParagraph.js +4 -2
  146. package/dist/markdown/renderRuns.d.ts +7 -1
  147. package/dist/markdown/renderRuns.js +33 -24
  148. package/dist/panel-layout.d.ts +11 -9
  149. package/dist/panel-layout.js +13 -9
  150. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  151. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  152. package/dist/prosemirror/attrs/index.js +5 -0
  153. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  154. package/dist/prosemirror/canonicalCommands.js +11 -4
  155. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  156. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  157. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  158. package/dist/prosemirror/clipboardIntent.js +9 -0
  159. package/dist/prosemirror/commands/comments.js +60 -35
  160. package/dist/prosemirror/commands/pageBreak.js +2 -2
  161. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  162. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  163. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  164. package/dist/prosemirror/commentOps.js +15 -1
  165. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  166. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  167. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  168. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  169. package/dist/prosemirror/conversion/toProseDoc.js +124 -22
  170. package/dist/prosemirror/extensions/core/ParagraphExtension.js +61 -35
  171. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  172. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  173. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +65 -29
  174. package/dist/prosemirror/extensions/marks/markUtils.js +11 -8
  175. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  176. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  177. package/dist/prosemirror/fieldRepresentation.js +18 -0
  178. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  179. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  180. package/dist/prosemirror/insertOperations.js +2 -1
  181. package/dist/prosemirror/listNumbering.js +11 -5
  182. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  183. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  184. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  185. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  186. package/dist/prosemirror/noteReferenceReview.js +14 -1
  187. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  188. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  189. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  190. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  191. package/dist/prosemirror/plugins/suggestionMode.js +48 -48
  192. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  193. package/dist/prosemirror/preservedXmlReview.js +35 -0
  194. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  195. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  196. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  197. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  198. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  199. package/dist/prosemirror/runStyleFormatting.js +3 -1
  200. package/dist/prosemirror/schema/marks.d.ts +1 -0
  201. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  202. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  203. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  204. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  205. package/dist/prosemirror/textInput.d.ts +5 -1
  206. package/dist/prosemirror/textInput.js +3 -3
  207. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  208. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  209. package/dist/prosemirror/validation.js +3 -5
  210. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  211. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  212. package/dist/types/canonicalCapabilities.d.ts +201 -0
  213. package/dist/types/canonicalCapabilities.js +198 -0
  214. package/dist/types/canonicalComments.d.ts +51 -0
  215. package/dist/types/canonicalComments.js +0 -0
  216. package/dist/types/canonicalSave.d.ts +11 -0
  217. package/dist/types/canonicalSave.js +0 -0
  218. package/dist/types/canonicalSections.d.ts +12 -0
  219. package/dist/types/canonicalSections.js +0 -0
  220. package/dist/types/docxSerialization.d.ts +14 -0
  221. package/dist/types/docxSerialization.js +7 -0
  222. package/dist/utils/headingCollector.js +45 -4
  223. package/dist/utils/outlineDepth.d.ts +9 -0
  224. package/dist/utils/outlineDepth.js +9 -0
  225. package/package.json +4 -4
@@ -1,31 +1,52 @@
1
1
  import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
+ import { rangedCommentIds, visitCommentMarkers } from "../docx/commentAnchorIndex.js";
2
3
  import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
3
- import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
4
+ import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
5
+ import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
4
6
  import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
5
7
  import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
8
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
9
+ import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
10
+ import { restoreCanonicalSelection } from "./canonicalSelection.js";
11
+ import { mapTocSelection } from "./canonicalTocSelection.js";
6
12
  import { Result, TaggedError, panic } from "better-result";
7
13
  import { hasIllegalXmlCharacters } from "@stll/docx-core";
8
14
  import { Fragment } from "prosemirror-model";
9
- import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
10
- import { TextSelection } from "prosemirror-state";
15
+ import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, commentDocumentIssue, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
16
+ import { AllSelection, TextSelection } from "prosemirror-state";
11
17
  //#region src/controller/canonicalSession.ts
12
18
  var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
13
19
  const refuse = (message) => Result.err(new CanonicalSessionError({
20
+ gap: CANONICAL_GAP.dispatch,
14
21
  message,
15
22
  reason: "refused"
16
23
  }));
17
24
  const noChange = (message) => Result.err(new CanonicalSessionError({
25
+ gap: CANONICAL_GAP.dispatch,
18
26
  message,
19
27
  reason: "noChange"
20
28
  }));
29
+ const COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set([
30
+ DOCUMENT_OP_TYPES.CREATE_COMMENT,
31
+ DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT,
32
+ DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION,
33
+ DOCUMENT_OP_TYPES.DELETE_COMMENT,
34
+ DOCUMENT_OP_TYPES.RESTORE_COMMENT_STATE
35
+ ]);
21
36
  const operationChangesPackage = (op) => {
22
37
  switch (op.type) {
38
+ case DOCUMENT_OP_TYPES.CREATE_COMMENT:
39
+ case DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT:
40
+ case DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION:
41
+ case DOCUMENT_OP_TYPES.DELETE_COMMENT:
42
+ case DOCUMENT_OP_TYPES.RESTORE_COMMENT_STATE:
23
43
  case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
24
44
  case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
25
45
  case DOCUMENT_OP_TYPES.ADD_NOTE:
26
46
  case DOCUMENT_OP_TYPES.REMOVE_NOTE:
27
47
  case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
28
48
  case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
49
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
29
50
  case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
30
51
  case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
31
52
  case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
@@ -36,14 +57,70 @@ const operationChangesPackage = (op) => {
36
57
  return op;
37
58
  }
38
59
  };
60
+ const operationChangesStructure = (op) => {
61
+ switch (op.type) {
62
+ case DOCUMENT_OP_TYPES.DELETE_BLOCKS:
63
+ case DOCUMENT_OP_TYPES.INSERT_BLOCKS:
64
+ case DOCUMENT_OP_TYPES.SPLIT_BLOCK:
65
+ case DOCUMENT_OP_TYPES.JOIN_BLOCKS:
66
+ case DOCUMENT_OP_TYPES.REPLACE_BLOCKS:
67
+ case DOCUMENT_OP_TYPES.INSERT_TABLE:
68
+ case DOCUMENT_OP_TYPES.DELETE_TABLE:
69
+ case DOCUMENT_OP_TYPES.SET_CONTAINER_BLOCKS:
70
+ case DOCUMENT_OP_TYPES.INSERT_ROW:
71
+ case DOCUMENT_OP_TYPES.DELETE_ROW:
72
+ case DOCUMENT_OP_TYPES.SET_TABLE_ROWS:
73
+ case DOCUMENT_OP_TYPES.INSERT_COLUMN:
74
+ case DOCUMENT_OP_TYPES.DELETE_COLUMN:
75
+ case DOCUMENT_OP_TYPES.MERGE_CELLS:
76
+ case DOCUMENT_OP_TYPES.SPLIT_CELL:
77
+ case DOCUMENT_OP_TYPES.SET_TABLE:
78
+ case DOCUMENT_OP_TYPES.RESOLVE_REVISION: return true;
79
+ case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
80
+ case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
81
+ case DOCUMENT_OP_TYPES.ADD_NOTE:
82
+ case DOCUMENT_OP_TYPES.REMOVE_NOTE:
83
+ case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
84
+ case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
85
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
86
+ case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
87
+ case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
88
+ case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
89
+ case DOCUMENT_OP_TYPES.INSERT_TEXT:
90
+ case DOCUMENT_OP_TYPES.INSERT_CONTENT:
91
+ case DOCUMENT_OP_TYPES.DELETE_RANGE:
92
+ case DOCUMENT_OP_TYPES.SPLIT_INLINE:
93
+ case DOCUMENT_OP_TYPES.JOIN_INLINE:
94
+ case DOCUMENT_OP_TYPES.SET_RUN_PROPS:
95
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS:
96
+ case DOCUMENT_OP_TYPES.SET_PARAGRAPH_REVIEW:
97
+ case DOCUMENT_OP_TYPES.REPLACE_INLINE:
98
+ case DOCUMENT_OP_TYPES.SET_TABLE_GRID:
99
+ case DOCUMENT_OP_TYPES.SET_CELL_PROPS:
100
+ case DOCUMENT_OP_TYPES.SET_ROW_PROPS:
101
+ case DOCUMENT_OP_TYPES.SET_TABLE_PROPS:
102
+ case DOCUMENT_OP_TYPES.CREATE_COMMENT:
103
+ case DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT:
104
+ case DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION:
105
+ case DOCUMENT_OP_TYPES.DELETE_COMMENT:
106
+ case DOCUMENT_OP_TYPES.RESTORE_COMMENT_STATE: return false;
107
+ default: return op;
108
+ }
109
+ };
39
110
  const operationChangesBodyProjection = (op) => {
40
111
  switch (op.type) {
112
+ case DOCUMENT_OP_TYPES.CREATE_COMMENT:
113
+ case DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT:
114
+ case DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION:
115
+ case DOCUMENT_OP_TYPES.DELETE_COMMENT:
116
+ case DOCUMENT_OP_TYPES.RESTORE_COMMENT_STATE:
41
117
  case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
42
118
  case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
43
119
  case DOCUMENT_OP_TYPES.ADD_NOTE:
44
120
  case DOCUMENT_OP_TYPES.REMOVE_NOTE:
45
121
  case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
46
122
  case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
123
+ case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
47
124
  case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
48
125
  case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
49
126
  case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
@@ -55,7 +132,7 @@ const operationChangesBodyProjection = (op) => {
55
132
  }
56
133
  };
57
134
  const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
58
- /** Paragraph gaps map logical UTF-16 units and zero-width note marks to PM positions. */
135
+ /** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
59
136
  var CanonicalProjection = class {
60
137
  doc;
61
138
  story;
@@ -72,15 +149,16 @@ var CanonicalProjection = class {
72
149
  const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
73
150
  if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
74
151
  const relative = position - paragraph.start;
75
- const offset = paragraph.boundaries.findIndex((gaps) => gaps.includes(relative));
76
- if (offset < 0) return refuse("The input splits a note reference.");
152
+ const offset = paragraph.boundaries.findIndex((gaps) => gaps.some((gap) => gap.position === relative));
153
+ if (offset < 0) return refuse("The input splits an indivisible inline element.");
77
154
  if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
78
155
  const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
156
+ const gap = gaps.find((entry) => entry.position === relative) ?? panic("Missing canonical native gap");
79
157
  return Result.ok({
80
158
  story: this.story,
81
159
  blockId: paragraph.blockId,
82
160
  offset,
83
- ...gaps.length > 1 ? { zeroWidthBefore: gaps.indexOf(relative) } : {}
161
+ ...gaps.length > 1 || gap.zeroWidthBefore > 0 ? { zeroWidthBefore: gap.zeroWidthBefore } : {}
84
162
  });
85
163
  }
86
164
  inputAddressAt(position) {
@@ -88,30 +166,32 @@ var CanonicalProjection = class {
88
166
  if (address.isErr()) return address;
89
167
  const paragraph = this.paragraph(address.value.blockId);
90
168
  if (paragraph === void 0) panic("Input lost its paragraph.");
91
- let offset = address.value.offset;
169
+ let destination = position;
92
170
  paragraph.node.forEach((node, start) => {
93
- if (node.marks.some((mark) => mark.type.name === "deletion") && offset > start && offset < start + node.nodeSize) offset = start + node.nodeSize;
94
- });
95
- return Result.ok({
96
- ...address.value,
97
- offset
171
+ 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;
98
172
  });
173
+ return destination === position ? address : this.addressAt(destination);
99
174
  }
100
175
  positionAt(address) {
101
176
  const paragraph = this.paragraph(address.blockId);
102
177
  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.");
103
178
  const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
104
- const gapIndex = address.zeroWidthBefore ?? gaps.length - 1;
105
- if (!Number.isInteger(gapIndex) || gapIndex < 0 || gapIndex >= gaps.length) return refuse("The canonical selection is outside a paragraph gap.");
106
- return Result.ok(paragraph.start + (gaps.at(gapIndex) ?? panic("Missing canonical gap")));
179
+ const ordinal = address.zeroWidthBefore ?? (gaps.at(-1) ?? panic("Missing canonical gap")).zeroWidthBefore;
180
+ const gap = gaps.find(({ zeroWidthBefore }) => zeroWidthBefore === ordinal);
181
+ if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal > (gaps.at(-1)?.zeroWidthBefore ?? 0)) return refuse("The canonical selection is outside a paragraph gap.");
182
+ if (gap === void 0) return refuse("This source gap is inside a collapsed or erased inline boundary and has no editor position.");
183
+ return Result.ok(paragraph.start + gap.position);
107
184
  }
108
185
  selectionAt(state) {
109
- if (!(state.selection instanceof TextSelection)) return refuse("Canonical input requires a text selection.");
110
- const anchor = this.addressAt(state.selection.anchor);
186
+ if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
187
+ const range = canonicalSelectionRange(state);
188
+ const type = state.selection instanceof AllSelection ? "all" : "text";
189
+ const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
111
190
  if (anchor.isErr()) return anchor;
112
- const head = this.addressAt(state.selection.head);
191
+ const head = this.addressAt(type === "all" ? range.to : state.selection.head);
113
192
  if (head.isErr()) return head;
114
193
  return Result.ok({
194
+ type,
115
195
  anchor: anchor.value,
116
196
  head: head.value
117
197
  });
@@ -162,31 +242,31 @@ const preservePropertySources = ({ target, source, stories = documentStories(sou
162
242
  }
163
243
  copyDocumentParagraphPropertySourceContract(target, source);
164
244
  };
165
- const supportsTextContent = (content) => content.every((item) => {
166
- switch (item.type) {
167
- case "run": return item.content.every((child) => child.type === "text" && !hasIllegalXmlCharacters(child.text) && !/[\t\r\n]/u.test(child.text) || child.type === "noteMarker" || child.type === "footnoteRef" || child.type === "endnoteRef" || child.type === "tab" || child.type === "break");
168
- case "insertion":
169
- case "deletion": return supportsTextContent(item.content);
170
- default: return false;
171
- }
172
- });
173
- const supportsSeed = (document, stories = documentStories(document)) => {
174
- if ((document.package.document.comments?.length ?? 0) > 0) return false;
175
- return stories.every((story) => {
245
+ const unsupportedSeedReason = (document, stories = documentStories(document)) => {
246
+ const commentIssue = commentDocumentIssue(document, "all");
247
+ if (commentIssue !== void 0) return commentIssue;
248
+ for (const story of stories) {
176
249
  const body = findStoryBody(document, story);
177
250
  if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
178
251
  const note = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id);
179
- if (note?.noteType !== void 0 && note.noteType !== "normal") return true;
252
+ if (note?.noteType !== void 0 && note.noteType !== "normal") continue;
180
253
  }
181
254
  const content = body?.content;
182
- return content !== void 0 && content.length > 0 && content.every((paragraph) => paragraph.type === "paragraph" && paragraph.reviewCarrier === void 0 && supportsTextContent(paragraph.content));
183
- });
255
+ if (!content || content.length === 0) return "Canonical sessions require a nonempty paragraph story.";
256
+ for (const paragraph of content) {
257
+ if (paragraph.type !== "paragraph" || paragraph.reviewCarrier !== void 0) return "Canonical sessions currently require plain paragraphs and supported inline atoms.";
258
+ const issue = canonicalInlineSourceIssue(paragraph.content);
259
+ if (issue !== null) return issue;
260
+ }
261
+ }
262
+ return null;
184
263
  };
185
264
  const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
186
265
  const body = findStoryBody(document, story);
187
266
  if (!body) return refuse("The canonical story no longer exists.");
267
+ const currentStyles = Object.hasOwn(document.package, "styles") ? document.package.styles : styles;
188
268
  const options = {
189
- ...styles == null ? {} : { styles },
269
+ ...currentStyles == null ? {} : { styles: currentStyles },
190
270
  ...document.package.theme === void 0 ? {} : { theme: document.package.theme }
191
271
  };
192
272
  const converted = Result.try({
@@ -196,70 +276,66 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
196
276
  return footnoteToProseDoc(body.content, options);
197
277
  },
198
278
  catch: (cause) => new CanonicalSessionError({
279
+ gap: CANONICAL_GAP.dispatch,
199
280
  reason: "refused",
200
281
  message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
201
282
  })
202
283
  });
203
284
  if (converted.isErr()) return converted;
285
+ const pairedBookmarkIds = collectPairedBookmarkIds(body.content);
286
+ const commentContext = {
287
+ rangedIds: rangedCommentIds(body.content),
288
+ pointIds: /* @__PURE__ */ new Set(),
289
+ openIds: /* @__PURE__ */ new Set()
290
+ };
291
+ visitCommentMarkers(body.content, ({ item }) => {
292
+ if (item.type === "commentReference" && !commentContext.rangedIds.has(item.id)) commentContext.pointIds.add(item.id);
293
+ });
204
294
  const paragraphs = [];
205
295
  let failure;
206
296
  converted.value.forEach((node, offset, index) => {
207
297
  const source = body.content.at(index);
208
298
  if (source?.type !== "paragraph" || source.paraId === void 0) {
209
299
  failure = new CanonicalSessionError({
300
+ gap: CANONICAL_GAP.dispatch,
210
301
  reason: "refused",
211
302
  message: "The projection changed paragraph structure."
212
303
  });
213
304
  return;
214
305
  }
215
- let text = "";
216
- let renderedText = "";
217
- let renderedSize = 0;
218
- const boundaries = [[0]];
219
- const appendContent = (content) => {
220
- for (const run of content) {
221
- if (run.type === "insertion" || run.type === "deletion") {
222
- appendContent(run.content);
223
- continue;
224
- }
225
- if (run.type !== "run") panic("Canonical projection encountered unsupported content");
226
- for (const child of run.content) if (child.type === "text") for (const unit of child.text.split("")) {
227
- text += unit;
228
- renderedText += unit;
229
- renderedSize += 1;
230
- boundaries.push([renderedSize]);
231
- }
232
- else if (child.type === "footnoteRef" || child.type === "endnoteRef") {
233
- text += "";
234
- renderedText += String(child.id);
235
- renderedSize += String(child.id).length;
236
- boundaries.push([renderedSize]);
237
- } else if (child.type === "tab" || child.type === "break") {
238
- text += "";
239
- renderedSize += 1;
240
- boundaries.push([renderedSize]);
241
- } else if (child.type === "noteMarker") {
242
- renderedSize += 1;
243
- (boundaries.at(-1) ?? panic("Missing canonical note gap")).push(renderedSize);
244
- }
245
- }
246
- };
247
- appendContent(source.content);
248
- if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId || node.textContent !== renderedText || node.content.size !== renderedSize) {
306
+ if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
307
+ failure = new CanonicalSessionError({
308
+ gap: CANONICAL_GAP.dispatch,
309
+ reason: "refused",
310
+ message: "The projection changed paragraph structure."
311
+ });
312
+ return;
313
+ }
314
+ const inline = projectCanonicalInline({
315
+ source,
316
+ paragraph: node,
317
+ pairedBookmarkIds,
318
+ commentContext
319
+ });
320
+ if (inline.isErr()) {
249
321
  failure = new CanonicalSessionError({
322
+ gap: CANONICAL_GAP.dispatch,
250
323
  reason: "refused",
251
- message: "The paragraph cannot be projected as plain text."
324
+ message: inline.error.message
252
325
  });
253
326
  return;
254
327
  }
255
328
  paragraphs.push({
256
329
  blockId: source.paraId,
257
330
  start: offset + 1,
258
- text,
259
- boundaries,
331
+ ...inline.value,
260
332
  node,
261
333
  source
262
334
  });
335
+ visitCommentMarkers([source], ({ item }) => {
336
+ if (item.type === "commentRangeStart") commentContext.openIds.add(item.id);
337
+ if (item.type === "commentRangeEnd") commentContext.openIds.delete(item.id);
338
+ });
263
339
  });
264
340
  if (failure !== void 0) return Result.err(failure);
265
341
  if (paragraphs.length !== body.content.length) return refuse("The projection changed paragraph structure.");
@@ -271,9 +347,15 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
271
347
  };
272
348
  const intentStory = (intent) => {
273
349
  switch (intent.type) {
350
+ case "setHyperlink":
351
+ case "removeHyperlink":
352
+ case "insertHyperlink":
353
+ case "replaceFragment":
354
+ case "moveFragment":
274
355
  case "replaceText":
275
356
  case "formatRun":
276
357
  case "insertAtom": return intent.from.story;
358
+ case "generateTOC":
277
359
  case "splitParagraph":
278
360
  case "formatParagraph": return intent.at.story;
279
361
  case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
@@ -284,14 +366,14 @@ const intentStory = (intent) => {
284
366
  };
285
367
  const UNDO_GROUP_WINDOW_MS = 500;
286
368
  const samePosition = (left, right) => sameStory(left.story, right.story) && left.blockId === right.blockId && left.offset === right.offset && (left.zeroWidthBefore ?? 0) === (right.zeroWidthBefore ?? 0);
287
- const sameSelection = (left, right) => samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
369
+ const sameSelection = (left, right) => left.type === right.type && samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
288
370
  const continuesGroup = (previous, next) => {
289
371
  switch (next.semantic) {
290
372
  case "typing":
291
373
  case "deleteBackward":
292
374
  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);
375
+ case "composition": return next.grouping === "compositionCorrection" && previous.semantic === "composition" && previous.boundary === next.boundary && sameSelection(previous.postSelection, next.preSelection);
293
376
  case "paste":
294
- case "composition":
295
377
  case "replacement":
296
378
  case "structure": return false;
297
379
  default: {
@@ -312,6 +394,7 @@ const publishCanonicalProjection = ({ state, commit, session }) => {
312
394
  const applied = Result.try({
313
395
  try: () => state.applyTransaction(commit.transaction),
314
396
  catch: (cause) => new CanonicalSessionError({
397
+ gap: CANONICAL_GAP.dispatch,
315
398
  reason: "refused",
316
399
  message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
317
400
  })
@@ -336,7 +419,10 @@ const storyProjectionKey = (story) => {
336
419
  var CanonicalSession = class {
337
420
  currentDocument;
338
421
  currentProjection;
422
+ sourceBlockIds;
339
423
  currentVersion = 0;
424
+ saveTouched = /* @__PURE__ */ new Set();
425
+ saveStructure = "stable";
340
426
  allocateIntentIds = createEditorIntentIdAllocator();
341
427
  mode = { type: "editing" };
342
428
  allocatedBlockIds = /* @__PURE__ */ new Set();
@@ -352,11 +438,16 @@ var CanonicalSession = class {
352
438
  constructor({ document, projection, styles }) {
353
439
  this.currentDocument = document;
354
440
  this.currentProjection = projection;
441
+ this.sourceBlockIds = new Set(packageParagraphIds(document.package));
355
442
  for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
356
443
  this.advanceBlockId();
357
444
  this.styles = styles == null ? styles : structuredClone(styles);
358
445
  this.rememberSources(document);
359
446
  }
447
+ /** Render only the committed comment projection while provisional IME blocks document snapshots. */
448
+ getCommittedComments() {
449
+ return structuredClone(this.currentDocument.package.document.comments ?? []);
450
+ }
360
451
  setMode(mode) {
361
452
  if (this.mode.type !== mode.type || this.mode.type === "suggesting" && mode.type === "suggesting" && this.mode.author !== mode.author) this.breakUndoGroup();
362
453
  this.mode = mode.type === "editing" ? { type: "editing" } : {
@@ -375,13 +466,18 @@ var CanonicalSession = class {
375
466
  return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
376
467
  }
377
468
  intentMode(document, intent) {
378
- if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return { type: "editing" };
469
+ if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
470
+ type: "editing",
471
+ reservedBlockIds: this.allocatedBlockIds
472
+ };
379
473
  const ids = this.allocateIntentIds(document, intent);
380
474
  return this.mode.type === "editing" ? {
381
475
  type: "editing",
382
- newIds: ids.newIds
476
+ newIds: ids.newIds,
477
+ reservedBlockIds: this.allocatedBlockIds
383
478
  } : {
384
479
  type: "suggesting",
480
+ reservedBlockIds: this.allocatedBlockIds,
385
481
  revision: {
386
482
  id: ids.revisionId,
387
483
  author: this.mode.author,
@@ -394,8 +490,14 @@ var CanonicalSession = class {
394
490
  let blockIds;
395
491
  switch (intent.type) {
396
492
  case "table": return true;
493
+ case "generateTOC":
397
494
  case "setList":
398
495
  case "formatParagraph": return false;
496
+ case "setHyperlink":
497
+ case "removeHyperlink":
498
+ case "insertHyperlink":
499
+ case "replaceFragment":
500
+ case "moveFragment": return true;
399
501
  case "replaceText":
400
502
  case "insertAtom":
401
503
  case "formatRun":
@@ -428,11 +530,26 @@ var CanonicalSession = class {
428
530
  }
429
531
  get document() {
430
532
  if (this.isComposing) throw new CanonicalSessionError({
533
+ gap: CANONICAL_GAP.dispatch,
431
534
  reason: "refused",
432
535
  message: "Composition must finish before taking a snapshot."
433
536
  });
434
537
  return this.currentDocument;
435
538
  }
539
+ captureSaveSnapshot() {
540
+ if (this.isComposing) throw new CanonicalSessionError({
541
+ gap: CANONICAL_GAP.save,
542
+ reason: "refused",
543
+ message: "Composition must finish before saving the canonical document."
544
+ });
545
+ const retainedBlockIdKeys = new Set([...this.sourceBlockIds, ...packageParagraphIds(this.currentDocument.package)].map((blockId) => blockId.toUpperCase()));
546
+ return {
547
+ document: cloneDocumentWithParagraphPropertySources(this.currentDocument),
548
+ version: this.currentVersion,
549
+ changedBlockIds: [...this.saveTouched].filter((blockId) => retainedBlockIdKeys.has(blockId.toUpperCase())),
550
+ structure: this.saveStructure
551
+ };
552
+ }
436
553
  get projection() {
437
554
  return this.currentProjection;
438
555
  }
@@ -469,7 +586,7 @@ var CanonicalSession = class {
469
586
  if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
470
587
  return Result.ok(void 0);
471
588
  }
472
- prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", time = Date.now() }) {
589
+ prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", compositionPhase, time = Date.now() }) {
473
590
  const projected = this.projectStory(story);
474
591
  if (projected.isErr()) return projected;
475
592
  const projection = projected.value;
@@ -498,11 +615,14 @@ var CanonicalSession = class {
498
615
  if (compiled.isErr()) return refuse(compiled.error.message);
499
616
  const { ops, selection: caret } = compiled.value;
500
617
  const postSelection = {
618
+ type: "text",
501
619
  anchor: caret,
502
620
  head: caret
503
621
  };
504
622
  const preDocument = this.currentDocument;
505
- const run = state.selection.empty && (semantic === "typing" && from === to && from === state.selection.head || semantic === "deleteBackward" && text.length === 0 && to === state.selection.head || semantic === "deleteForward" && text.length === 0 && from === state.selection.head);
623
+ const isCaret = state.selection.empty;
624
+ const deletion = isCaret && text.length === 0 && (semantic === "deleteBackward" || semantic === "deleteForward") ? deletionRange(state, semantic === "deleteBackward" ? "backward" : "forward") : null;
625
+ const grouping = isCaret && (semantic === "typing" && from === to && from === state.selection.head || deletion?.isOk() && deletion.value.from === from && deletion.value.to === to) ? "run" : "isolated";
506
626
  return this.stage({
507
627
  state,
508
628
  story,
@@ -521,7 +641,7 @@ var CanonicalSession = class {
521
641
  version,
522
642
  origin: "input",
523
643
  semantic,
524
- grouping: run ? "run" : "isolated",
644
+ grouping: compositionPhase === "correction" ? "compositionCorrection" : grouping,
525
645
  time,
526
646
  boundary: this.groupingBoundary
527
647
  };
@@ -541,9 +661,9 @@ var CanonicalSession = class {
541
661
  });
542
662
  }
543
663
  prepareIntent(state, intent) {
544
- return this.prepareIntents(state, [intent]);
664
+ return this.prepareIntents(state, { intents: [intent] });
545
665
  }
546
- prepareIntents(state, intents) {
666
+ prepareIntents(state, { intents, resourceOps = [], semantic = "structure" }) {
547
667
  const first = intents.at(0);
548
668
  if (first === void 0) return noChange("The intent batch is empty.");
549
669
  const story = intentStory(first);
@@ -556,12 +676,24 @@ var CanonicalSession = class {
556
676
  if (preSelection.isErr()) return preSelection;
557
677
  let document = this.currentDocument;
558
678
  let postSelection = preSelection.value;
559
- const ops = [];
679
+ const ops = [...resourceOps];
560
680
  const edits = [];
681
+ if (resourceOps.length > 0) {
682
+ const imported = applyDocumentOps(document, resourceOps);
683
+ if (imported.isErr()) return refuse(imported.error.message);
684
+ document = imported.value.document;
685
+ edits.push(imported.value);
686
+ }
561
687
  for (const intent of intents) {
688
+ if (this.mode.type === "suggesting" && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink" || intent.type === "generateTOC")) return Result.err(new CanonicalSessionError({
689
+ gap: CANONICAL_GAP.trackedHyperlinkResolution,
690
+ reason: "refused",
691
+ message: "Hyperlink and TOC suggestions require serializable wrapper review provenance."
692
+ }));
562
693
  const compiled = compileEditorIntent(document, {
563
694
  intent,
564
- mode: this.intentMode(document, intent)
695
+ mode: this.intentMode(document, intent),
696
+ firstBlockId: this.nextBlockId
565
697
  });
566
698
  if (compiled.isErr()) return refuse(compiled.error.message);
567
699
  const applied = applyDocumentOps(document, compiled.value.ops);
@@ -569,7 +701,18 @@ var CanonicalSession = class {
569
701
  document = applied.value.document;
570
702
  edits.push(applied.value);
571
703
  ops.push(...compiled.value.ops);
572
- if (intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
704
+ if (intent.type === "generateTOC") {
705
+ const mapping = {
706
+ at: intent.at,
707
+ after: compiled.value.selection,
708
+ ops: compiled.value.ops
709
+ };
710
+ postSelection = Object.assign({}, postSelection, {
711
+ anchor: mapTocSelection(postSelection.anchor, mapping),
712
+ head: mapTocSelection(postSelection.head, mapping)
713
+ });
714
+ } else if (intent.type !== "setHyperlink" && intent.type !== "removeHyperlink" && intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
715
+ type: "text",
573
716
  anchor: compiled.value.selection,
574
717
  head: compiled.value.selection
575
718
  };
@@ -582,7 +725,8 @@ var CanonicalSession = class {
582
725
  state,
583
726
  ops,
584
727
  postSelection,
585
- stagedApplied
728
+ stagedApplied,
729
+ semantic
586
730
  });
587
731
  }
588
732
  prepareOps(state, ops, postSelection) {
@@ -592,7 +736,7 @@ var CanonicalSession = class {
592
736
  postSelection
593
737
  });
594
738
  }
595
- prepareJournalledOps({ state, ops, postSelection, stagedApplied }) {
739
+ prepareJournalledOps({ state, ops, postSelection, stagedApplied, semantic = "structure" }) {
596
740
  const story = postSelection.anchor.story;
597
741
  const projected = this.projectStory(story);
598
742
  if (projected.isErr()) return projected;
@@ -618,7 +762,7 @@ var CanonicalSession = class {
618
762
  postDocument: this.currentDocument,
619
763
  version,
620
764
  origin: "input",
621
- semantic: "structure",
765
+ semantic,
622
766
  grouping: "isolated",
623
767
  time: Date.now(),
624
768
  boundary: this.groupingBoundary
@@ -656,7 +800,7 @@ var CanonicalSession = class {
656
800
  },
657
801
  patch: { styleId: nextStyle }
658
802
  });
659
- return this.prepareIntents(state, intents);
803
+ return this.prepareIntents(state, { intents });
660
804
  }
661
805
  prepareJoin(state, direction) {
662
806
  const checked = this.checkState(state);
@@ -712,6 +856,7 @@ var CanonicalSession = class {
712
856
  state,
713
857
  ops,
714
858
  postSelection: {
859
+ type: "text",
715
860
  anchor: caret,
716
861
  head: caret
717
862
  },
@@ -836,7 +981,8 @@ var CanonicalSession = class {
836
981
  document: applied.value.document,
837
982
  previous: this.currentDocument
838
983
  });
839
- if (!supportsSeed(applied.value.document, changed)) return refuse("The operations produce unsupported canonical story content.");
984
+ const unsupported = unsupportedSeedReason(applied.value.document, changed);
985
+ if (unsupported !== null) return refuse(unsupported);
840
986
  preservePropertySources({
841
987
  target: applied.value.document,
842
988
  source: this.currentDocument,
@@ -891,12 +1037,15 @@ var CanonicalSession = class {
891
1037
  transaction.replaceWith(from, to, Fragment.fromArray(replacement));
892
1038
  }
893
1039
  if (!transaction.doc.eq(projected.value.doc)) transaction.replaceWith(0, transaction.doc.content.size, projected.value.doc.content);
894
- if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
895
- const anchor = projected.value.positionAt(selection.anchor);
896
- const head = projected.value.positionAt(selection.head);
897
- if (anchor.isOk() && head.isOk()) transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
898
- else transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
899
- }
1040
+ if (sameStory(selection.anchor.story, projectedStory) && sameStory(selection.head.story, projectedStory)) restoreCanonicalSelection({
1041
+ transaction,
1042
+ projection: projected.value,
1043
+ selection,
1044
+ unavailable: {
1045
+ type: "near",
1046
+ anchor: state.selection.anchor
1047
+ }
1048
+ });
900
1049
  transaction.setMeta(CANONICAL_PROJECTION_META, {
901
1050
  type: "canonical",
902
1051
  origin,
@@ -916,6 +1065,7 @@ var CanonicalSession = class {
916
1065
  document: applied.value.document,
917
1066
  projection: projected.value,
918
1067
  bodyProjection: bodyProjection.value,
1068
+ selection,
919
1069
  touched: applied.value.touched,
920
1070
  version: baseVersion + 1,
921
1071
  origin,
@@ -939,6 +1089,13 @@ var CanonicalSession = class {
939
1089
  this.currentSelection = selection;
940
1090
  this.currentDocument = applied.value.document;
941
1091
  this.currentProjection = bodyProjection.value;
1092
+ for (const ids of [
1093
+ applied.value.touched.modified,
1094
+ applied.value.touched.inserted,
1095
+ applied.value.touched.removed
1096
+ ]) for (const id of ids) this.saveTouched.add(id);
1097
+ const bodyOps = ops.filter((op) => !COMMENT_OPERATION_TYPES.has(op.type));
1098
+ if (bodyOps.some(operationChangesStructure) || bodyOps.some(operationChangesPackage) || bodyOps.length > 0 && (applied.value.touched.inserted.length > 0 || applied.value.touched.removed.length > 0)) this.saveStructure = "changed";
942
1099
  this.currentVersion = baseVersion + 1;
943
1100
  onPublish(applied.value.inverse, this.currentVersion);
944
1101
  return Result.ok(void 0);
@@ -947,7 +1104,8 @@ var CanonicalSession = class {
947
1104
  }
948
1105
  };
949
1106
  const createCanonicalSession = (document, styles) => {
950
- if (!supportsSeed(document)) return refuse("Canonical sessions currently require plain paragraphs and supported inline atoms.");
1107
+ const unsupported = unsupportedSeedReason(document);
1108
+ if (unsupported !== null) return refuse(unsupported);
951
1109
  const owned = cloneDocumentWithParagraphPropertySources(document);
952
1110
  const normalized = normalizeForOps(owned);
953
1111
  preservePropertySources({
@@ -980,31 +1138,55 @@ const visibleDeletionContext = (state) => {
980
1138
  let last = index;
981
1139
  while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
982
1140
  let text = "";
983
- const physicalGaps = [];
1141
+ const physicalStarts = [];
1142
+ const physicalEnds = [];
984
1143
  let visibleOffset = 0;
1144
+ let issue;
985
1145
  state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
986
1146
  if (paragraphIndex < first || paragraphIndex > last) return;
987
- paragraph.forEach((node, offset) => {
988
- if (node.marks.some((mark) => mark.type.name === "deletion")) return;
989
- const value = node.isText ? node.text ?? "" : "";
1147
+ const native = canonicalNativeCells(paragraph);
1148
+ if (native.isErr()) {
1149
+ issue = new CanonicalSessionError({
1150
+ gap: CANONICAL_GAP.dispatch,
1151
+ reason: "refused",
1152
+ message: native.error.message
1153
+ });
1154
+ return;
1155
+ }
1156
+ for (const cell of native.value) {
1157
+ if (cell.type === "zeroWidth" || cell.node.marks.some((mark) => mark.type.name === "deletion")) continue;
1158
+ const value = cell.type === "text" ? cell.text : "";
1159
+ const from = paragraphOffset + 1 + cell.from;
1160
+ const to = paragraphOffset + 1 + cell.to;
1161
+ if (cell.type !== "text" && selection.from > from && selection.from < to) {
1162
+ issue = new CanonicalSessionError({
1163
+ gap: CANONICAL_GAP.dispatch,
1164
+ reason: "refused",
1165
+ message: "Character deletion cannot start inside an inline atom."
1166
+ });
1167
+ return;
1168
+ }
990
1169
  for (let unit = 0; unit < value.length; unit += 1) {
991
- const position = paragraphOffset + 1 + offset + unit;
992
- physicalGaps[text.length] = position;
1170
+ const position = from + (cell.type === "text" ? unit : 0);
1171
+ physicalStarts.push(position);
993
1172
  text += value.charAt(unit);
994
- physicalGaps.push(position + (node.isText ? 1 : node.nodeSize));
1173
+ physicalEnds.push(cell.type === "text" ? position + 1 : to);
995
1174
  if (position < selection.from) visibleOffset += 1;
996
1175
  }
997
- });
1176
+ }
998
1177
  });
999
- return {
1178
+ return issue === void 0 ? Result.ok({
1000
1179
  text,
1001
- physicalGaps,
1180
+ physicalStarts,
1181
+ physicalEnds,
1002
1182
  visibleOffset
1003
- };
1183
+ }) : Result.err(issue);
1004
1184
  };
1005
1185
  const isCanonicalJoinBoundary = (state, direction) => {
1006
1186
  if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
1007
- const { text, visibleOffset } = visibleDeletionContext(state);
1187
+ const context = visibleDeletionContext(state);
1188
+ if (context.isErr()) return false;
1189
+ const { text, visibleOffset } = context.value;
1008
1190
  return visibleOffset === (direction === "backward" ? 0 : text.length);
1009
1191
  };
1010
1192
  /** Native character deletion consumes one whole visible grapheme. */
@@ -1018,20 +1200,20 @@ const deletionRange = (state, direction) => {
1018
1200
  const parent = selection.$from.parent;
1019
1201
  const offset = selection.$from.parentOffset;
1020
1202
  if ((direction === "backward" ? parent.childBefore(offset).node : parent.childAfter(offset).node)?.type.name === "noteMarker") return refuse("Automatic note marks cannot be deleted as characters.");
1021
- const { text, physicalGaps, visibleOffset } = visibleDeletionContext(state);
1203
+ const context = visibleDeletionContext(state);
1204
+ if (context.isErr()) return context;
1205
+ const { text, physicalStarts, physicalEnds, visibleOffset } = context.value;
1022
1206
  if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
1023
1207
  if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
1024
1208
  if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
1025
1209
  let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
1026
1210
  while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
1027
- const position = physicalGaps.at(boundary);
1028
- if (position === void 0) panic("Canonical deletion lost a grapheme boundary.");
1029
- return Result.ok(direction === "backward" ? {
1030
- from: position,
1031
- to: selection.to
1032
- } : {
1033
- from: selection.from,
1034
- to: position
1211
+ const from = physicalStarts.at(direction === "backward" ? boundary : visibleOffset);
1212
+ const to = physicalEnds.at((direction === "backward" ? visibleOffset : boundary) - 1);
1213
+ if (from === void 0 || to === void 0) panic("Canonical deletion lost a grapheme boundary.");
1214
+ return Result.ok({
1215
+ from,
1216
+ to
1035
1217
  });
1036
1218
  };
1037
1219
  //#endregion