@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
@@ -0,0 +1,648 @@
1
+ import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
+ import { buildCleanBlockText, resolveCleanTextRange } from "../ai-edits/clean-text.js";
3
+ import { stripInlineEmphasisMarkers } from "../ai-edits/inline-emphasis.js";
4
+ import { collectNoteReferenceLabels, hashFolioAIBlockText } from "../ai-edits/snapshot.js";
5
+ import { wordDiffSessionFromOptions } from "../ai-edits/word-diff.js";
6
+ import { getFolioDocumentOperationIssues, getFolioDocumentOperationReceiptsForStory, parseFolioDocumentOperationBatch } from "../document-operations.js";
7
+ import { maxAnnotationIdInDoc } from "../prosemirror/plugins/revisionIds.js";
8
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
9
+ import { canonicalCommentBody, compileCanonicalComments } from "./canonicalComments.js";
10
+ import { Result, TaggedError, panic } from "better-result";
11
+ import { Mark } from "prosemirror-model";
12
+ import { OP_STORIES, applyDocumentOps, compileEditorIntent, createEditorIntentIdAllocator, documentStories, findStoryBody, freshCommentId, paragraphLength } from "@stll/docx-core/ops";
13
+ //#region src/controller/canonicalPublicOperations.ts
14
+ /** Public batches compile against the read projection; only the canonical journal publishes. */
15
+ /** A new public kind cannot bypass a canonical compiler/refusal decision. */
16
+ const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS = {
17
+ replaceInBlock: "compile",
18
+ replaceRange: "compile",
19
+ replaceBlock: "compile",
20
+ splitBlock: "compile",
21
+ formatRange: "compile",
22
+ mergeBlockWithNext: "compile",
23
+ setBlockParagraphProperties: CANONICAL_GAP.publicUnsupportedInline,
24
+ insertAfterBlock: CANONICAL_GAP.publicUnsupportedInline,
25
+ insertBeforeBlock: CANONICAL_GAP.publicUnsupportedInline,
26
+ deleteBlock: CANONICAL_GAP.publicUnsupportedInline,
27
+ commentOnBlock: "compile",
28
+ commentOnRange: "compile",
29
+ insertTable: CANONICAL_GAP.publicTableProjection,
30
+ insertSignatureTable: CANONICAL_GAP.publicTableProjection,
31
+ deleteTable: CANONICAL_GAP.publicTableProjection,
32
+ insertTableRow: CANONICAL_GAP.publicTableProjection,
33
+ deleteTableRow: CANONICAL_GAP.publicTableProjection,
34
+ insertTableColumn: CANONICAL_GAP.publicTableProjection,
35
+ deleteTableColumn: CANONICAL_GAP.publicTableProjection,
36
+ mergeTableCells: CANONICAL_GAP.publicTableProjection,
37
+ splitTableCell: CANONICAL_GAP.publicTableProjection
38
+ };
39
+ const resolveComment = ({ operation, from, to }) => {
40
+ if ("comment" in operation && operation.comment !== void 0) return {
41
+ type: "commented",
42
+ operation,
43
+ comment: {
44
+ text: operation.comment.text,
45
+ from,
46
+ to
47
+ }
48
+ };
49
+ return {
50
+ type: "plain",
51
+ operation
52
+ };
53
+ };
54
+ const compilePublicIntents = ({ document, intents, mode, author, date, nextRevisionId, allocate }) => {
55
+ const ops = [];
56
+ const revisions = [];
57
+ for (const sourceIntent of intents) {
58
+ const ids = allocate(document, sourceIntent);
59
+ const intent = sourceIntent.type === "splitParagraph" ? {
60
+ type: "splitParagraph",
61
+ at: sourceIntent.at,
62
+ ...sourceIntent.to === void 0 ? {} : { to: sourceIntent.to },
63
+ newBlockId: ids.newBlockId
64
+ } : sourceIntent;
65
+ const firstRevisionId = nextRevisionId;
66
+ const compiled = compileEditorIntent(document, {
67
+ intent,
68
+ mode: mode === "direct" ? {
69
+ type: "editing",
70
+ newIds: ids.newIds
71
+ } : {
72
+ type: "suggesting",
73
+ revision: {
74
+ id: firstRevisionId,
75
+ author,
76
+ date
77
+ },
78
+ newIds: {
79
+ control: ids.newIds.control,
80
+ revision: ids.newIds.revision.map((_, index) => firstRevisionId + index + 1)
81
+ }
82
+ }
83
+ });
84
+ if (compiled.isErr()) return Result.err(compiled.error);
85
+ const applied = applyDocumentOps(document, compiled.value.ops);
86
+ if (applied.isErr()) return Result.err(applied.error);
87
+ document = applied.value.document;
88
+ ops.push(...compiled.value.ops);
89
+ revisions.push(...applied.value.revisions);
90
+ for (const id of applied.value.revisions) nextRevisionId = Math.max(nextRevisionId, id + 1);
91
+ }
92
+ return Result.ok({
93
+ document,
94
+ ops,
95
+ revisions,
96
+ nextRevisionId
97
+ });
98
+ };
99
+ var CanonicalPublicOperationResolutionError = class extends TaggedError("CanonicalPublicOperationResolutionError") {};
100
+ const canonicalPublicStory = (story) => {
101
+ if (story === "main") return story;
102
+ switch (story.type) {
103
+ case "header":
104
+ case "footer": return {
105
+ kind: story.type,
106
+ rId: story.relationshipId
107
+ };
108
+ case "footnote":
109
+ case "endnote": return {
110
+ kind: story.type,
111
+ id: story.noteId
112
+ };
113
+ default: return panic(`Unknown public operation story ${story}`);
114
+ }
115
+ };
116
+ const batchStatus = (batch, skippedCount) => {
117
+ if (batch.dryRun) return "previewed";
118
+ if (batch.atomic && skippedCount > 0) return "rejected";
119
+ return "committed";
120
+ };
121
+ /** Handles address isolated journal groups; no second document or PM history is retained. */
122
+ var CanonicalPublicOperations = class {
123
+ options;
124
+ handles = [];
125
+ nextHandle = 1;
126
+ handleNamespace = crypto.randomUUID();
127
+ constructor(options) {
128
+ this.options = options;
129
+ }
130
+ apply(options) {
131
+ const batch = parseFolioDocumentOperationBatch(options.batch);
132
+ const { session } = this.options;
133
+ const publicStory = options.story ?? "main";
134
+ const story = canonicalPublicStory(publicStory);
135
+ const state = this.options.getState(story);
136
+ const projected = session.projectStory(story);
137
+ const skipped = [];
138
+ const normalizations = [];
139
+ const refusals = /* @__PURE__ */ new Map();
140
+ const resolved = [];
141
+ const { diff } = wordDiffSessionFromOptions(options.wordDiff);
142
+ let nextRevisionId = options.revisionStamp?.idSeed ?? 1;
143
+ if (!session.isComposing && options.revisionStamp === void 0) for (const currentStory of documentStories(session.document)) {
144
+ const currentProjection = session.projectStory(currentStory);
145
+ if (currentProjection.isOk()) nextRevisionId = Math.max(nextRevisionId, maxAnnotationIdInDoc(currentProjection.value.doc) + 1);
146
+ }
147
+ const skip = (id, reason, message) => {
148
+ skipped.push({
149
+ id,
150
+ reason,
151
+ ...message === void 0 ? {} : { message }
152
+ });
153
+ };
154
+ const result = (applied, undoHandle) => {
155
+ const failures = new Map(skipped.map((item) => [item.id, item]));
156
+ const ordered = batch.operations.flatMap(({ id }) => {
157
+ const failure = failures.get(id);
158
+ if (!failure) return [];
159
+ const canonicalRefusal = refusals.get(id);
160
+ return [canonicalRefusal ? {
161
+ ...failure,
162
+ canonicalRefusal
163
+ } : failure];
164
+ });
165
+ return {
166
+ version: 1,
167
+ status: batchStatus(batch, ordered.length),
168
+ applied,
169
+ skipped: ordered,
170
+ issues: getFolioDocumentOperationIssues(batch.operations, ordered),
171
+ receipts: getFolioDocumentOperationReceiptsForStory({
172
+ operations: batch.operations,
173
+ applied,
174
+ story: publicStory
175
+ }),
176
+ undoHandle,
177
+ nextRevisionId,
178
+ ...normalizations.length === 0 ? {} : { normalizations }
179
+ };
180
+ };
181
+ for (const operation of batch.operations) {
182
+ if (story !== OP_STORIES.MAIN) {
183
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicSecondaryStories });
184
+ skip(operation.id, "unsupportedBlock", "Canonical public secondary-story routing is unavailable.");
185
+ continue;
186
+ }
187
+ if (!state || projected.isErr() || session.isComposing) {
188
+ skip(operation.id, "documentNotEditable");
189
+ continue;
190
+ }
191
+ if (batch.mode === "suggested") {
192
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicSuggestedMode });
193
+ skip(operation.id, "unsupportedMode", "Canonical pending suggestions are unavailable.");
194
+ continue;
195
+ }
196
+ if (options.tableTemplates !== void 0 || options.undefinedStyles === "keep") {
197
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
198
+ skip(operation.id, "unsupportedBlock", "This canonical compiler does not support the supplied execution options.");
199
+ continue;
200
+ }
201
+ const disposition = CANONICAL_PUBLIC_OPERATION_DISPOSITIONS[operation.type];
202
+ if (disposition !== "compile") {
203
+ refusals.set(operation.id, { gap: disposition });
204
+ skip(operation.id, "unsupportedBlock");
205
+ continue;
206
+ }
207
+ const compiled = this.resolve({
208
+ operation,
209
+ options,
210
+ state,
211
+ story,
212
+ diff
213
+ });
214
+ if (compiled.isErr()) {
215
+ if (compiled.error.reason === "unsupportedBlock") refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
216
+ skip(operation.id, compiled.error.reason, compiled.error.message);
217
+ continue;
218
+ }
219
+ if (resolved.some(({ from, to }) => compiled.value.from < to && compiled.value.to > from || from === to && compiled.value.from <= from && compiled.value.to >= from || compiled.value.from === compiled.value.to && compiled.value.from >= from && compiled.value.from <= to)) {
220
+ skip(operation.id, "overlappingOperation");
221
+ continue;
222
+ }
223
+ resolved.push(compiled.value);
224
+ }
225
+ if (batch.atomic && skipped.length > 0) {
226
+ for (const { operation } of resolved) skip(operation.id, "atomicBatchRejected");
227
+ return result([], null);
228
+ }
229
+ if (!state || projected.isErr() || session.isComposing) return result([], null);
230
+ resolved.sort((left, right) => right.from - left.from);
231
+ let document = session.document;
232
+ const ops = [];
233
+ const appliedById = /* @__PURE__ */ new Map();
234
+ const allocate = createEditorIntentIdAllocator();
235
+ const date = options.revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
236
+ for (const resolvedOperation of resolved) {
237
+ const { operation, intents, normalizations: adjustments } = resolvedOperation;
238
+ const compiled = compilePublicIntents({
239
+ document,
240
+ intents,
241
+ allocate,
242
+ nextRevisionId,
243
+ mode: batch.mode === "direct" ? "direct" : "tracked-changes",
244
+ author: options.author ?? "AI",
245
+ date
246
+ });
247
+ if (compiled.isErr()) {
248
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
249
+ skip(operation.id, "unsupportedBlock", compiled.error.message);
250
+ continue;
251
+ }
252
+ let nextDocument = compiled.value.document;
253
+ const operationOps = [...compiled.value.ops];
254
+ let commentId;
255
+ switch (resolvedOperation.type) {
256
+ case "plain": break;
257
+ case "commented": {
258
+ const { comment } = resolvedOperation;
259
+ const id = freshCommentId(nextDocument);
260
+ const before = findStoryBody(document, story)?.content.find((block) => block.type === "paragraph" && block.paraId === comment.from.blockId);
261
+ const after = findStoryBody(nextDocument, story)?.content.find((block) => block.type === "paragraph" && block.paraId === comment.from.blockId);
262
+ if (id.isErr() || before?.type !== "paragraph" || after?.type !== "paragraph") {
263
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicComments });
264
+ skip(operation.id, "unsupportedBlock", id.isErr() ? id.error.message : "The comment anchor no longer exists.");
265
+ continue;
266
+ }
267
+ const to = {
268
+ ...comment.to,
269
+ offset: comment.to.offset + paragraphLength(after) - paragraphLength(before)
270
+ };
271
+ const commented = compileCanonicalComments({
272
+ document: nextDocument,
273
+ command: {
274
+ type: "create",
275
+ comment: {
276
+ id: id.value,
277
+ author: options.author ?? "AI",
278
+ date,
279
+ done: false,
280
+ content: canonicalCommentBody(nextDocument, comment.text)
281
+ },
282
+ anchor: to.offset === comment.from.offset ? {
283
+ kind: "point",
284
+ at: comment.from
285
+ } : {
286
+ kind: "range",
287
+ from: comment.from,
288
+ to
289
+ }
290
+ }
291
+ });
292
+ if (commented.isErr()) {
293
+ refusals.set(operation.id, { gap: CANONICAL_GAP.publicComments });
294
+ skip(operation.id, "unsupportedBlock", commented.error.message);
295
+ continue;
296
+ }
297
+ nextDocument = commented.value.document;
298
+ operationOps.push(...commented.value.ops);
299
+ commentId = id.value;
300
+ break;
301
+ }
302
+ default: panic(`Unknown resolved operation ${resolvedOperation}`);
303
+ }
304
+ if (operationOps.length === 0) {
305
+ skip(operation.id, "noopOperation");
306
+ continue;
307
+ }
308
+ normalizations.push(...adjustments);
309
+ document = nextDocument;
310
+ ops.push(...operationOps);
311
+ nextRevisionId = Math.max(compiled.value.nextRevisionId, (commentId ?? -1) + 1);
312
+ const revisionIds = compiled.value.revisions;
313
+ const revisionId = revisionIds.at(0);
314
+ appliedById.set(operation.id, {
315
+ id: operation.id,
316
+ ...commentId === void 0 ? {} : { commentId },
317
+ ...revisionId === void 0 ? {} : {
318
+ revisionId,
319
+ revisionIds
320
+ }
321
+ });
322
+ }
323
+ if (batch.atomic && skipped.length > 0) {
324
+ for (const { id } of appliedById.values()) skip(id, "atomicBatchRejected");
325
+ return result([], null);
326
+ }
327
+ if (ops.length === 0) return result([], null);
328
+ const selection = projected.value.selectionAt(state);
329
+ const prepared = selection.isErr() ? selection : session.prepareOps(state, ops, selection.value);
330
+ if (prepared.isErr()) {
331
+ for (const { id } of appliedById.values()) {
332
+ refusals.set(id, { gap: CANONICAL_GAP.publicUnsupportedInline });
333
+ skip(id, prepared.error.reason === "noChange" ? "noopOperation" : "unsupportedBlock", prepared.error.message);
334
+ }
335
+ return result([], null);
336
+ }
337
+ const applied = batch.operations.flatMap(({ id }) => {
338
+ const entry = appliedById.get(id);
339
+ return entry ? [batch.dryRun ? { id } : entry] : [];
340
+ });
341
+ if (batch.dryRun) return result(applied, null);
342
+ if (!this.options.publish(prepared.value)) {
343
+ for (const { id } of applied) skip(id, "documentNotEditable");
344
+ return result([], null);
345
+ }
346
+ const handle = {
347
+ type: "documentOperationUndo",
348
+ id: `canonical-${this.handleNamespace}-${String(this.nextHandle++)}`
349
+ };
350
+ this.handles.push({
351
+ handle,
352
+ version: session.version,
353
+ story
354
+ });
355
+ return result(applied, handle);
356
+ }
357
+ undo(handle) {
358
+ const index = this.handles.findIndex(({ handle: existing }) => existing.id === handle.id && existing.type === handle.type);
359
+ if (index < 0) return {
360
+ status: "rejected",
361
+ undoHandle: handle,
362
+ reason: "unknownHandle"
363
+ };
364
+ if (index !== this.handles.length - 1) return {
365
+ status: "rejected",
366
+ undoHandle: handle,
367
+ reason: "notLatest"
368
+ };
369
+ const entry = this.handles.at(-1);
370
+ const { session } = this.options;
371
+ if (!entry || entry.version !== session.version) return {
372
+ status: "rejected",
373
+ undoHandle: handle,
374
+ reason: "documentChanged"
375
+ };
376
+ const state = this.options.getState(entry.story);
377
+ if (!state) return {
378
+ status: "rejected",
379
+ undoHandle: handle,
380
+ reason: "documentChanged"
381
+ };
382
+ const prepared = session.prepareUndo(state, entry.story);
383
+ if (prepared.isErr() || !this.options.publish(prepared.value)) return {
384
+ status: "rejected",
385
+ undoHandle: handle,
386
+ reason: "documentChanged"
387
+ };
388
+ this.handles.pop();
389
+ const previous = this.handles.at(-1);
390
+ if (previous && previous.version === entry.version - 1) previous.version = session.version;
391
+ return {
392
+ status: "undone",
393
+ undoHandle: handle
394
+ };
395
+ }
396
+ resolve({ operation, options, state, story, diff }) {
397
+ const refusal = (reason, message) => Result.err(new CanonicalPublicOperationResolutionError({
398
+ reason,
399
+ message: message ?? `The operation was refused: ${reason}.`
400
+ }));
401
+ switch (operation.type) {
402
+ case "replaceInBlock":
403
+ case "replaceRange":
404
+ case "replaceBlock":
405
+ case "splitBlock":
406
+ case "formatRange":
407
+ case "mergeBlockWithNext":
408
+ case "commentOnRange":
409
+ case "commentOnBlock": break;
410
+ default: return refusal("unsupportedBlock", `Canonical public operation ${operation.type} is unavailable.`);
411
+ }
412
+ const blockId = operation.type === "replaceRange" || operation.type === "formatRange" || operation.type === "commentOnRange" ? operation.range.blockId : operation.blockId;
413
+ const anchor = Object.hasOwn(options.snapshot.anchors, blockId) ? options.snapshot.anchors[blockId] : void 0;
414
+ const projection = this.options.session.projectStory(story);
415
+ if (!anchor || projection.isErr()) return refusal("missingBlock");
416
+ const paragraph = projection.value.paragraph(blockId);
417
+ if (!paragraph) return refusal("missingBlock");
418
+ if (paragraph.source.pPrMark?.kind === "del" || paragraph.source.pPrMark?.kind === "moveFrom") return refusal("pendingDeletion");
419
+ const clean = buildCleanBlockText(paragraph.node, paragraph.start - 1, {
420
+ fieldResults: "text",
421
+ noteReferences: collectNoteReferenceLabels(state.doc)
422
+ });
423
+ const hash = hashFolioAIBlockText(clean.text);
424
+ if (operation.precondition && hash !== operation.precondition.blockTextHash) return refusal("preconditionFailed");
425
+ if (hash !== anchor.textHash) return refusal("changedBlock");
426
+ let start = 0;
427
+ let end = clean.text.length;
428
+ let text = "";
429
+ switch (operation.type) {
430
+ case "replaceInBlock":
431
+ if (operation.find === "") return refusal("emptyOperation");
432
+ start = clean.text.indexOf(operation.find);
433
+ if (start < 0) return refusal("missingFind");
434
+ if (clean.text.indexOf(operation.find, start + 1) >= 0) return refusal("ambiguousFind");
435
+ end = start + operation.find.length;
436
+ text = operation.replace;
437
+ break;
438
+ case "commentOnBlock":
439
+ if (operation.quote !== void 0) {
440
+ start = clean.text.indexOf(operation.quote);
441
+ if (start < 0) return refusal("missingFind");
442
+ if (clean.text.indexOf(operation.quote, start + 1) >= 0) return refusal("ambiguousFind");
443
+ end = start + operation.quote.length;
444
+ }
445
+ break;
446
+ case "replaceRange":
447
+ case "formatRange":
448
+ case "commentOnRange":
449
+ start = operation.range.startOffset;
450
+ end = operation.range.endOffset;
451
+ if (hashFolioAIBlockText(clean.text.slice(start, end)) !== operation.range.selectedTextHash) return refusal("staleRange");
452
+ text = operation.type === "replaceRange" ? operation.replace : "";
453
+ break;
454
+ case "replaceBlock":
455
+ if (operation.styleId !== void 0 || operation.preserveFormatting === false) return refusal("unsupportedBlock", "Canonical replacement paragraph properties are unavailable.");
456
+ text = operation.text;
457
+ break;
458
+ case "splitBlock":
459
+ if (operation.firstParagraphProperties || operation.secondParagraphProperties) return refusal("unsupportedBlock", "Canonical split paragraph properties are unavailable.");
460
+ start = operation.offset;
461
+ end = start + (operation.separator?.length ?? 0);
462
+ if (start <= 0 || start >= clean.text.length || clean.text.slice(start, end) !== (operation.separator ?? "")) return refusal("staleRange");
463
+ break;
464
+ case "mergeBlockWithNext":
465
+ if (operation.mergedParagraphProperties || operation.separator) return refusal("unsupportedBlock", "Canonical public join properties and separators are unavailable.");
466
+ start = clean.text.length;
467
+ end = start;
468
+ break;
469
+ default: return operation;
470
+ }
471
+ if ([start, end].some((offset) => splitsSurrogatePair(clean.text, offset) || splitsGraphemeCluster(clean.text, offset))) return refusal("splitsCharacter");
472
+ if (clean.structuralBoundaries.some((boundary) => boundary.type === "noteReference" && start < boundary.offset + boundary.length && end > boundary.offset)) return refusal("protectedReference");
473
+ if (stripInlineEmphasisMarkers(text) !== text) return refusal("unsupportedBlock", "Canonical public emphasis replacements are unavailable.");
474
+ if (/\[\^(?:e)?\d+\]/u.test(text) || /[\t\r\n]/u.test(text)) return refusal("unsupportedBlock", "Canonical public replacements require plain text.");
475
+ const range = resolveCleanTextRange({
476
+ cleanBlock: clean,
477
+ startOffset: start,
478
+ endOffset: end
479
+ });
480
+ if (!range) return refusal("staleRange");
481
+ const from = projection.value.addressAt(range.from);
482
+ const to = projection.value.addressAt(range.to);
483
+ if (from.isErr() || to.isErr()) return refusal("unsupportedBlock");
484
+ const resolvedComment = resolveComment({
485
+ operation,
486
+ from: from.value,
487
+ to: to.value
488
+ });
489
+ if (operation.type === "commentOnBlock" || operation.type === "commentOnRange") return Result.ok({
490
+ ...resolvedComment,
491
+ intents: [],
492
+ normalizations: [],
493
+ from: range.from,
494
+ to: range.to
495
+ });
496
+ if (resolvedComment.type === "plain" && operation.type !== "splitBlock" && operation.type !== "formatRange" && operation.type !== "mergeBlockWithNext" && clean.text.slice(start, end) === text) return refusal("noopOperation");
497
+ let intent;
498
+ switch (operation.type) {
499
+ case "splitBlock":
500
+ intent = {
501
+ type: "splitParagraph",
502
+ at: from.value,
503
+ to: to.value,
504
+ newBlockId: createEditorIntentIdAllocator()(this.options.session.document, {
505
+ type: "splitParagraph",
506
+ at: from.value,
507
+ to: to.value
508
+ }).newBlockId
509
+ };
510
+ break;
511
+ case "mergeBlockWithNext": {
512
+ const body = this.options.session.document.package.document.content;
513
+ if (story !== "main") return refusal("unsupportedBlock", "Canonical public joins currently require the main story.");
514
+ const next = body.at(body.findIndex((block) => block.type === "paragraph" && block.paraId === blockId) + 1);
515
+ if (next?.type !== "paragraph" || !next.paraId) return refusal("missingBlock");
516
+ intent = {
517
+ type: "joinParagraphs",
518
+ story,
519
+ blockId,
520
+ nextBlockId: next.paraId
521
+ };
522
+ break;
523
+ }
524
+ case "formatRange": {
525
+ const formatting = operation.formatting;
526
+ const patch = {
527
+ ...formatting.bold === void 0 ? {} : { bold: formatting.bold },
528
+ ...formatting.italic === void 0 ? {} : { italic: formatting.italic },
529
+ ...formatting.underline === void 0 ? {} : { underline: formatting.underline === null ? null : { style: formatting.underline ? "single" : "none" } },
530
+ ...formatting.strike === void 0 ? {} : {
531
+ strike: formatting.strike,
532
+ doubleStrike: null
533
+ },
534
+ ...formatting.fontSizePt === void 0 ? {} : { fontSize: formatting.fontSizePt === null ? null : formatting.fontSizePt * 2 },
535
+ ...formatting.fontFamily === void 0 ? {} : { fontFamily: formatting.fontFamily === null ? null : {
536
+ ascii: formatting.fontFamily,
537
+ hAnsi: formatting.fontFamily
538
+ } },
539
+ ...formatting.color === void 0 ? {} : { color: formatting.color === null ? null : { rgb: formatting.color.replace(/^#/u, "").toUpperCase() } }
540
+ };
541
+ intent = {
542
+ type: "formatRun",
543
+ from: from.value,
544
+ to: to.value,
545
+ patch
546
+ };
547
+ break;
548
+ }
549
+ case "replaceInBlock":
550
+ case "replaceRange":
551
+ case "replaceBlock":
552
+ intent = {
553
+ type: "replaceText",
554
+ from: from.value,
555
+ to: to.value,
556
+ text,
557
+ ...options.replacementBackground === "keep" ? {} : { runPropsPatch: {
558
+ highlight: null,
559
+ shading: null
560
+ } }
561
+ };
562
+ break;
563
+ default: return panic(`Unknown canonical public operation ${operation}`);
564
+ }
565
+ const intents = [];
566
+ const normalizations = [];
567
+ if (intent.type === "replaceText") {
568
+ let offset = start;
569
+ let pending = null;
570
+ const changes = [];
571
+ for (const segment of diff(clean.text.slice(start, end), text)) switch (segment.type) {
572
+ case "equal":
573
+ if (pending) changes.push(pending);
574
+ pending = null;
575
+ offset += segment.text.length;
576
+ break;
577
+ case "del":
578
+ pending ??= {
579
+ start: offset,
580
+ end: offset,
581
+ text: ""
582
+ };
583
+ offset += segment.text.length;
584
+ pending.end = offset;
585
+ break;
586
+ case "ins":
587
+ pending ??= {
588
+ start: offset,
589
+ end: offset,
590
+ text: ""
591
+ };
592
+ pending.text += segment.text;
593
+ break;
594
+ default: {
595
+ const unreachable = segment.type;
596
+ return panic(`Unknown replacement diff ${unreachable}`);
597
+ }
598
+ }
599
+ if (pending) changes.push(pending);
600
+ for (const change of changes.toReversed()) {
601
+ const gap = resolveCleanTextRange({
602
+ cleanBlock: clean,
603
+ startOffset: change.start,
604
+ endOffset: change.end
605
+ });
606
+ if (!gap) return refusal("unsupportedBlock");
607
+ if (change.text.length > 0 && gap.from < gap.to) {
608
+ let first = null;
609
+ let uniform = true;
610
+ paragraph.node.nodesBetween(gap.from - paragraph.start, gap.to - paragraph.start, (node) => {
611
+ if (!node.isText || node.marks.some((mark) => mark.type.name === "deletion")) return;
612
+ const marks = node.marks.filter((mark) => mark.type.name !== "insertion");
613
+ if (first === null) first = marks;
614
+ else if (!Mark.sameSet(first, marks)) uniform = false;
615
+ });
616
+ if (!uniform && normalizations.length === 0) normalizations.push({
617
+ id: operation.id,
618
+ code: "uniformReplacementFormatting"
619
+ });
620
+ }
621
+ const changeFrom = projection.value.addressAt(gap.from);
622
+ const changeTo = projection.value.addressAt(gap.to);
623
+ if (changeFrom.isErr() || changeTo.isErr()) return refusal("unsupportedBlock");
624
+ intents.push({
625
+ type: "replaceText",
626
+ from: changeFrom.value,
627
+ to: changeTo.value,
628
+ text: change.text,
629
+ ...intent.runPropsPatch === void 0 ? {} : { runPropsPatch: intent.runPropsPatch }
630
+ });
631
+ }
632
+ } else intents.push(intent);
633
+ const structural = operation.type === "splitBlock" || operation.type === "mergeBlockWithNext";
634
+ const nextParagraph = operation.type === "mergeBlockWithNext" && intent.type === "joinParagraphs" ? projection.value.paragraph(intent.nextBlockId) : void 0;
635
+ let claimedTo = range.to;
636
+ if (structural) claimedTo = paragraph.start + paragraph.node.content.size;
637
+ if (nextParagraph) claimedTo = nextParagraph.start + nextParagraph.node.content.size;
638
+ return Result.ok({
639
+ ...resolvedComment,
640
+ intents,
641
+ normalizations,
642
+ from: structural ? paragraph.start : range.from,
643
+ to: claimedTo
644
+ });
645
+ }
646
+ };
647
+ //#endregion
648
+ export { CANONICAL_PUBLIC_OPERATION_DISPOSITIONS, CanonicalPublicOperations };
@@ -0,0 +1,18 @@
1
+ import { CanonicalProjection, CanonicalSelection } from "./canonicalSession.js";
2
+ import { Transaction } from "prosemirror-state";
3
+ //#region src/controller/canonicalSelection.d.ts
4
+ type RestoreCanonicalSelectionOptions = {
5
+ transaction: Transaction;
6
+ projection: CanonicalProjection;
7
+ selection: CanonicalSelection;
8
+ unavailable: {
9
+ type: "refuse";
10
+ } | {
11
+ type: "near";
12
+ anchor: number;
13
+ };
14
+ };
15
+ /** Restore the admitted selection kind after a canonical projection changes. */
16
+ declare const restoreCanonicalSelection: ({ transaction, projection, selection, unavailable }: RestoreCanonicalSelectionOptions) => boolean;
17
+ //#endregion
18
+ export { restoreCanonicalSelection };
@@ -0,0 +1,32 @@
1
+ import { panic } from "better-result";
2
+ import { AllSelection, TextSelection } from "prosemirror-state";
3
+ //#region src/controller/canonicalSelection.ts
4
+ /** Restore the admitted selection kind after a canonical projection changes. */
5
+ const restoreCanonicalSelection = ({ transaction, projection, selection, unavailable }) => {
6
+ switch (selection.type) {
7
+ case "all":
8
+ transaction.setSelection(new AllSelection(transaction.doc));
9
+ return true;
10
+ case "text": {
11
+ const anchor = projection.positionAt(selection.anchor);
12
+ const head = projection.positionAt(selection.head);
13
+ if (anchor.isOk() && head.isOk()) {
14
+ transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
15
+ return true;
16
+ }
17
+ switch (unavailable.type) {
18
+ case "refuse": return false;
19
+ case "near":
20
+ transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(unavailable.anchor, transaction.doc.content.size))));
21
+ return true;
22
+ default: return panic(`Unknown selection restoration policy: ${unavailable}`);
23
+ }
24
+ }
25
+ default: {
26
+ const exhaustive = selection.type;
27
+ return panic(`Unknown canonical selection type: ${exhaustive}`);
28
+ }
29
+ }
30
+ };
31
+ //#endregion
32
+ export { restoreCanonicalSelection };