@stll/folio-core 0.56.0 → 0.58.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 (158) hide show
  1. package/dist/ai-edits/apply.js +122 -22
  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/table-row-column-mutations.js +17 -3
  5. package/dist/ai-suggestions/apply.js +20 -1
  6. package/dist/compare/inline-atoms.js +40 -16
  7. package/dist/compare/inline-provenance.js +12 -5
  8. package/dist/compat/eigenpal.d.ts +2 -1
  9. package/dist/compat/eigenpal.js +2 -1
  10. package/dist/controller/canonicalClipboard.d.ts +3 -1
  11. package/dist/controller/canonicalClipboard.js +31 -13
  12. package/dist/controller/canonicalComments.d.ts +41 -0
  13. package/dist/controller/canonicalComments.js +152 -0
  14. package/dist/controller/canonicalComposition.d.ts +17 -4
  15. package/dist/controller/canonicalComposition.js +125 -20
  16. package/dist/controller/canonicalInlineProjection.d.ts +6 -1
  17. package/dist/controller/canonicalInlineProjection.js +86 -3
  18. package/dist/controller/canonicalInput.d.ts +1 -1
  19. package/dist/controller/canonicalInput.js +14 -4
  20. package/dist/controller/canonicalInputTimer.d.ts +10 -1
  21. package/dist/controller/canonicalInputTimer.js +24 -1
  22. package/dist/controller/canonicalPublicOperations.d.ts +2 -2
  23. package/dist/controller/canonicalPublicOperations.js +107 -15
  24. package/dist/controller/canonicalSelection.d.ts +18 -0
  25. package/dist/controller/canonicalSelection.js +41 -0
  26. package/dist/controller/canonicalSession.d.ts +4 -1
  27. package/dist/controller/canonicalSession.js +106 -36
  28. package/dist/controller/canonicalStoryEditor.d.ts +3 -1
  29. package/dist/controller/canonicalStoryEditor.js +21 -1
  30. package/dist/controller/canonicalStructure.js +8 -3
  31. package/dist/controller/folioEditor.js +13 -0
  32. package/dist/controller/headerFooterEditorManager.d.ts +1 -1
  33. package/dist/controller/headerFooterEditorManager.js +25 -11
  34. package/dist/controller/hiddenEditorApi.d.ts +13 -2
  35. package/dist/controller/hiddenEditorApi.js +5 -1
  36. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  37. package/dist/controller/hiddenEditorManager.js +167 -14
  38. package/dist/controller/noteEditorManager.d.ts +1 -1
  39. package/dist/controller/noteEditorManager.js +28 -13
  40. package/dist/docx/builtInStyles.js +13 -6
  41. package/dist/docx/containerChildren.gen.d.ts +3 -1
  42. package/dist/docx/containerChildren.gen.js +33 -1
  43. package/dist/docx/fieldParser.js +14 -2
  44. package/dist/docx/hyperlinkParser.d.ts +1 -1
  45. package/dist/docx/hyperlinkParser.js +20 -1
  46. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  47. package/dist/docx/inlineWrapperContent.js +4 -4
  48. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  49. package/dist/docx/linkedSdtPreservation.js +37 -0
  50. package/dist/docx/listRendering.d.ts +18 -0
  51. package/dist/{prosemirror/conversion/listRenderingDefinition.js → docx/listRendering.js} +17 -3
  52. package/dist/docx/paragraphParser.js +88 -67
  53. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  54. package/dist/docx/paragraphPropertySource.js +107 -1
  55. package/dist/docx/preservedRunContent.js +2 -1
  56. package/dist/docx/relsParser.js +26 -2
  57. package/dist/docx/rezip.js +9 -6
  58. package/dist/docx/selectiveSave.js +10 -8
  59. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  60. package/dist/docx/serializer/commentSerializer.js +7 -3
  61. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  62. package/dist/docx/server/materializeYjsDocx.js +15 -7
  63. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  64. package/dist/i18n/messages/ar.gen.js +4 -0
  65. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  66. package/dist/i18n/messages/catalogs.gen.js +68 -0
  67. package/dist/i18n/messages/cs.gen.js +4 -0
  68. package/dist/i18n/messages/de.gen.js +4 -0
  69. package/dist/i18n/messages/en.gen.js +4 -0
  70. package/dist/i18n/messages/es.gen.js +4 -0
  71. package/dist/i18n/messages/et.gen.js +4 -0
  72. package/dist/i18n/messages/fr.gen.js +4 -0
  73. package/dist/i18n/messages/he.gen.js +4 -0
  74. package/dist/i18n/messages/hi.gen.js +4 -0
  75. package/dist/i18n/messages/hu.gen.js +4 -0
  76. package/dist/i18n/messages/lt.gen.js +4 -0
  77. package/dist/i18n/messages/lv.gen.js +4 -0
  78. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  79. package/dist/i18n/messages/pl.gen.js +4 -0
  80. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  81. package/dist/i18n/messages/sk.gen.js +4 -0
  82. package/dist/i18n/messages/tr.gen.js +4 -0
  83. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  84. package/dist/index.d.ts +2 -1
  85. package/dist/index.js +2 -1
  86. package/dist/internal/revisionResolutionInline.js +25 -3
  87. package/dist/markdown/internals.js +7 -0
  88. package/dist/markdown/renderParagraph.js +11 -12
  89. package/dist/markdown/renderRuns.js +3 -13
  90. package/dist/markdown/types.d.ts +3 -0
  91. package/dist/panel-layout.d.ts +11 -9
  92. package/dist/panel-layout.js +13 -9
  93. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  94. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  95. package/dist/prosemirror/attrs/index.js +9 -0
  96. package/dist/prosemirror/commands/comments.js +60 -35
  97. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  98. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  99. package/dist/prosemirror/commentOps.js +15 -1
  100. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  101. package/dist/prosemirror/conversion/fromProseDoc.js +150 -30
  102. package/dist/prosemirror/conversion/toProseDoc.js +128 -40
  103. package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
  104. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  105. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  106. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
  107. package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
  108. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  109. package/dist/prosemirror/extensions/nodes/ImageExtension.js +8 -1
  110. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  111. package/dist/prosemirror/fieldRepresentation.js +18 -0
  112. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  113. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  114. package/dist/prosemirror/imagePictureNames.d.ts +12 -0
  115. package/dist/prosemirror/imagePictureNames.js +20 -0
  116. package/dist/prosemirror/listNumbering.js +11 -5
  117. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  118. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  119. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  120. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  121. package/dist/prosemirror/noteReferenceReview.js +14 -1
  122. package/dist/prosemirror/paragraphFormattingProvenance.js +1 -0
  123. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  124. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  125. package/dist/prosemirror/plugins/suggestionMode.js +47 -49
  126. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  127. package/dist/prosemirror/preservedXmlReview.js +35 -0
  128. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  129. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  130. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  131. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  132. package/dist/prosemirror/runStyleFormatting.d.ts +74 -2
  133. package/dist/prosemirror/runStyleFormatting.js +20 -1
  134. package/dist/prosemirror/schema/marks.d.ts +1 -0
  135. package/dist/prosemirror/schema/nodes.d.ts +3 -0
  136. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  137. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  138. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  139. package/dist/prosemirror/styles/styleResolver.d.ts +4 -1
  140. package/dist/prosemirror/styles/styleResolver.js +32 -11
  141. package/dist/prosemirror/textInput.d.ts +5 -1
  142. package/dist/prosemirror/textInput.js +3 -3
  143. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  144. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  145. package/dist/prosemirror/validation.js +3 -5
  146. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  147. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  148. package/dist/types/canonicalCapabilities.d.ts +18 -18
  149. package/dist/types/canonicalCapabilities.js +18 -18
  150. package/dist/types/canonicalComments.d.ts +51 -0
  151. package/dist/types/canonicalComments.js +0 -0
  152. package/dist/types/canonicalSections.d.ts +12 -0
  153. package/dist/types/canonicalSections.js +0 -0
  154. package/dist/utils/headingCollector.js +45 -4
  155. package/dist/utils/outlineDepth.d.ts +9 -0
  156. package/dist/utils/outlineDepth.js +9 -0
  157. package/package.json +2 -2
  158. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +0 -6
@@ -1,7 +1,9 @@
1
1
  import { createNumberingIdAllocator } from "../docx/numberingIds.js";
2
+ import { copyParagraphIndentationProjection } from "../docx/paragraphPropertySource.js";
2
3
  import { visitInlineContentSlots, visitParagraphRuns } from "../docx/paragraphTraversal.js";
3
4
  import { captureVerbatimXml } from "../docx/verbatimCapture.js";
4
5
  import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, OOXML_NAMESPACE_SCOPE, findAttributeByNamespaceUri, getLocalName, parseXml } from "../docx/xmlParser.js";
6
+ import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
5
7
  import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
6
8
  import { completeNumberingForDoc } from "../prosemirror/listInstanceReferences.js";
7
9
  import { marksToTextFormatting } from "../prosemirror/runFormattingFromMarks.js";
@@ -173,6 +175,22 @@ const importNumbering = ({ destination, source, paragraphs, mode, sourceKind, ex
173
175
  numIds: nums
174
176
  });
175
177
  };
178
+ /** Imported comments and notes require copying their source story parts. */
179
+ const canonicalClipboardStoryPartRefusal = (paragraph) => {
180
+ let requiresStory = false;
181
+ visitInlineContentSlots(paragraph, ({ item }) => {
182
+ if (item.type === "commentRangeStart" || item.type === "commentRangeEnd" || item.type === "commentReference") requiresStory = true;
183
+ });
184
+ visitParagraphRuns(paragraph, (run) => {
185
+ if (run.content.some((item) => item.type === "footnoteRef" || item.type === "endnoteRef")) requiresStory = true;
186
+ });
187
+ if (!requiresStory) return void 0;
188
+ return new CanonicalSessionError({
189
+ gap: CANONICAL_GAP.dispatch,
190
+ reason: "refused",
191
+ message: "Clipboard comments and note references require importing their source story parts."
192
+ });
193
+ };
176
194
  const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarget, pasteTarget, sourceDocument, moveSource }) => {
177
195
  if (session.isComposing) return refuse("Composition must finish before using the clipboard.");
178
196
  if (slice.content.size === 0) return Result.err(new CanonicalSessionError({
@@ -203,12 +221,18 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
203
221
  const paragraphs = [];
204
222
  for (const block of converted.value) {
205
223
  if (block.type !== "paragraph") return refuse("The clipboard contains an unsupported block.");
206
- paragraphs.push(structuredClone(block));
224
+ const paragraph = structuredClone(block);
225
+ copyParagraphIndentationProjection({
226
+ target: paragraph,
227
+ source: block
228
+ });
229
+ paragraphs.push(paragraph);
207
230
  }
208
231
  if (moveTarget === void 0 && (plain || sourceDocument === void 0)) flattenClipboardStyleReferences(paragraphs);
209
- const from = session.projection.addressAt(pasteTarget ?? moveSource?.from ?? state.selection.from);
232
+ const selectionRange = canonicalSelectionRange(state);
233
+ const from = session.projection.addressAt(pasteTarget ?? moveSource?.from ?? selectionRange.from);
210
234
  if (from.isErr()) return from;
211
- const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ?? state.selection.to);
235
+ const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ?? selectionRange.to);
212
236
  if (to.isErr()) return to;
213
237
  let styles = session.document.package.styles;
214
238
  let styleIds;
@@ -309,14 +333,8 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
309
333
  };
310
334
  for (const paragraph of paragraphs) {
311
335
  if (moveTarget === void 0) {
312
- let requiresStory = false;
313
- visitInlineContentSlots(paragraph, ({ item }) => {
314
- if (item.type === "commentRangeStart" || item.type === "commentRangeEnd" || item.type === "commentReference") requiresStory = true;
315
- });
316
- visitParagraphRuns(paragraph, (run) => {
317
- if (run.content.some((item) => item.type === "footnoteRef" || item.type === "endnoteRef")) requiresStory = true;
318
- });
319
- if (requiresStory) return refuse("Clipboard comments and note references require importing their source story parts.");
336
+ const storyPartRefusal = canonicalClipboardStoryPartRefusal(paragraph);
337
+ if (storyPartRefusal !== void 0) return Result.err(storyPartRefusal);
320
338
  const links = [];
321
339
  visitInlineContentSlots(paragraph, ({ item }) => {
322
340
  if (item.type === "hyperlink") links.push(item);
@@ -335,7 +353,7 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
335
353
  const runs = [];
336
354
  visitParagraphRuns(paragraph, (run) => runs.push(run));
337
355
  for (const item of runs) {
338
- if (plain) item.formatting = marksToTextFormatting(state.storedMarks ?? state.doc.resolve(pasteTarget ?? moveSource?.from ?? state.selection.from).marks());
356
+ if (plain) item.formatting = marksToTextFormatting(state.storedMarks ?? state.doc.resolve(pasteTarget ?? moveSource?.from ?? selectionRange.from).marks());
339
357
  for (const child of item.content) {
340
358
  if (child.type !== "drawing") continue;
341
359
  if (child.rawXmlMode !== void 0) return refuse("Opaque drawings and embedded objects require their source package parts.");
@@ -473,4 +491,4 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
473
491
  });
474
492
  };
475
493
  //#endregion
476
- export { prepareCanonicalPaste };
494
+ export { canonicalClipboardStoryPartRefusal, prepareCanonicalPaste };
@@ -0,0 +1,41 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
3
+ import { Comment } from "../types/content.js";
4
+ import { CreateCommentOp, DocumentOp } from "@stll/docx-core/ops";
5
+ //#region src/controller/canonicalComments.d.ts
6
+ declare const CanonicalCommentError_base: import("better-result").TaggedErrorClass<"CanonicalCommentError">;
7
+ declare class CanonicalCommentError extends CanonicalCommentError_base<{
8
+ message: string;
9
+ gap: typeof CANONICAL_GAP.comments;
10
+ }> {}
11
+ type CanonicalCommentCommand = {
12
+ type: "create";
13
+ comment: CreateCommentOp["comment"];
14
+ anchor: CreateCommentOp["anchor"];
15
+ } | {
16
+ type: "update";
17
+ id: number;
18
+ content: Comment["content"];
19
+ } | {
20
+ type: "resolve";
21
+ id: number;
22
+ status: "open" | "resolved";
23
+ } | {
24
+ type: "delete";
25
+ id: number;
26
+ } | {
27
+ type: "replace";
28
+ comments: readonly Comment[];
29
+ };
30
+ type CompileCanonicalCommentsOptions = {
31
+ document: document_d_exports.Document;
32
+ command: CanonicalCommentCommand;
33
+ };
34
+ declare const compileCanonicalComments: ({ document, command }: CompileCanonicalCommentsOptions) => import("better-result").Err<never, CanonicalCommentError> | import("better-result").Ok<{
35
+ ops: DocumentOp[];
36
+ document: document_d_exports.Document;
37
+ commentId: number | undefined;
38
+ }, never>;
39
+ declare const canonicalCommentBody: (document: document_d_exports.Document, text: string) => Comment["content"];
40
+ //#endregion
41
+ export { CanonicalCommentCommand, CanonicalCommentError, canonicalCommentBody, compileCanonicalComments };
@@ -0,0 +1,152 @@
1
+ import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
2
+ import { canonicalJson } from "../utils/canonicalJson.js";
3
+ import { Result, TaggedError } from "better-result";
4
+ import { DOCUMENT_OP_TYPES, allocateCommentAnchorIds, applyDocumentOps, packageParagraphIds } from "@stll/docx-core/ops";
5
+ //#region src/controller/canonicalComments.ts
6
+ /** The one semantic compiler for canonical sidebar, controlled and public comments. */
7
+ var CanonicalCommentError = class extends TaggedError("CanonicalCommentError") {};
8
+ const refuse = (message) => Result.err(new CanonicalCommentError({
9
+ message,
10
+ gap: CANONICAL_GAP.comments
11
+ }));
12
+ const compileCanonicalComments = ({ document, command }) => {
13
+ const current = document.package.document.comments ?? [];
14
+ const byId = new Map(current.map((comment) => [comment.id, comment]));
15
+ const ops = [];
16
+ let commentId;
17
+ switch (command.type) {
18
+ case "create":
19
+ ops.push({
20
+ type: DOCUMENT_OP_TYPES.CREATE_COMMENT,
21
+ comment: command.comment,
22
+ anchor: command.anchor
23
+ });
24
+ commentId = command.comment.id;
25
+ break;
26
+ case "update":
27
+ ops.push({
28
+ type: DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT,
29
+ id: command.id,
30
+ content: command.content
31
+ });
32
+ break;
33
+ case "resolve":
34
+ ops.push({
35
+ type: DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION,
36
+ id: command.id,
37
+ status: command.status
38
+ });
39
+ break;
40
+ case "delete": {
41
+ const comment = byId.get(command.id);
42
+ if (!comment) return refuse("The comment no longer exists.");
43
+ ops.push({
44
+ type: DOCUMENT_OP_TYPES.DELETE_COMMENT,
45
+ id: command.id,
46
+ scope: comment.parentId !== void 0 && byId.has(comment.parentId) ? "reply" : "thread"
47
+ });
48
+ break;
49
+ }
50
+ case "replace": {
51
+ const nextById = new Map(command.comments.map((comment) => [comment.id, comment]));
52
+ if (nextById.size !== command.comments.length) return refuse("Comment ids must be unique.");
53
+ for (const comment of current) {
54
+ if (nextById.has(comment.id) && comment.parentId !== void 0 && byId.has(comment.parentId) && !nextById.has(comment.parentId)) return refuse("A retained reply requires its parent thread.");
55
+ if (nextById.has(comment.id)) continue;
56
+ if (comment.parentId !== void 0 && !nextById.has(comment.parentId) && byId.has(comment.parentId)) continue;
57
+ ops.push({
58
+ type: DOCUMENT_OP_TYPES.DELETE_COMMENT,
59
+ id: comment.id,
60
+ scope: comment.parentId !== void 0 && byId.has(comment.parentId) ? "reply" : "thread"
61
+ });
62
+ }
63
+ const pending = command.comments.filter((comment) => !byId.has(comment.id));
64
+ const available = new Set(current.filter((comment) => nextById.has(comment.id)).map((comment) => comment.id));
65
+ while (pending.length > 0) {
66
+ const index = pending.findIndex((comment) => comment.parentId !== void 0 && available.has(comment.parentId));
67
+ if (index < 0) return refuse("New controlled comments require an existing anchored parent without cycles.");
68
+ const next = pending.splice(index, 1).at(0);
69
+ if (next === void 0 || next.parentId === void 0) return refuse("The controlled reply is missing its parent.");
70
+ const { parentId, ...comment } = next;
71
+ ops.push({
72
+ type: DOCUMENT_OP_TYPES.CREATE_COMMENT,
73
+ comment,
74
+ anchor: {
75
+ kind: "reply",
76
+ parentId
77
+ }
78
+ });
79
+ available.add(next.id);
80
+ }
81
+ for (const next of command.comments) {
82
+ const previous = byId.get(next.id);
83
+ if (!previous) continue;
84
+ const { content: priorContent, done: priorDone, author: priorAuthor, initials: priorInitials, date: priorDate, ...priorMetadata } = previous;
85
+ const { content: nextContent, done: nextDone, author: nextAuthor, initials: nextInitials, date: nextDate, ...nextMetadata } = next;
86
+ if (canonicalJson(priorMetadata) !== canonicalJson(nextMetadata)) return refuse("Controlled comment identity and opaque metadata cannot be replaced.");
87
+ const patch = {
88
+ ...priorAuthor === nextAuthor ? {} : { author: nextAuthor },
89
+ ...priorInitials === nextInitials ? {} : { initials: Object.hasOwn(next, "initials") ? nextInitials : null },
90
+ ...priorDate === nextDate ? {} : { date: Object.hasOwn(next, "date") ? nextDate : null }
91
+ };
92
+ if (canonicalJson(priorContent) !== canonicalJson(nextContent) || Object.keys(patch).length > 0) ops.push({
93
+ type: DOCUMENT_OP_TYPES.UPDATE_COMMENT_CONTENT,
94
+ id: next.id,
95
+ content: nextContent,
96
+ patch
97
+ });
98
+ if (priorDone !== nextDone) {
99
+ if (nextDone === void 0) return refuse("Resolution changes must name an open or resolved state.");
100
+ ops.push({
101
+ type: DOCUMENT_OP_TYPES.SET_COMMENT_RESOLUTION,
102
+ id: next.id,
103
+ status: nextDone ? "resolved" : "open"
104
+ });
105
+ }
106
+ }
107
+ break;
108
+ }
109
+ default: return command;
110
+ }
111
+ const compiled = [];
112
+ let draft = document;
113
+ for (const op of ops) {
114
+ const materialized = (() => {
115
+ if (op.type !== DOCUMENT_OP_TYPES.CREATE_COMMENT) return Result.ok(op);
116
+ const allocation = allocateCommentAnchorIds(draft, op);
117
+ if (allocation.isErr()) return Result.err(allocation.error);
118
+ return Result.ok({
119
+ ...op,
120
+ newIds: allocation.value
121
+ });
122
+ })();
123
+ if (materialized.isErr()) return refuse(materialized.error.message);
124
+ const applied = applyDocumentOps(draft, [materialized.value]);
125
+ if (applied.isErr()) return refuse(applied.error.message);
126
+ compiled.push(materialized.value);
127
+ draft = applied.value.document;
128
+ }
129
+ return Result.ok({
130
+ ops: compiled,
131
+ document: draft,
132
+ commentId
133
+ });
134
+ };
135
+ const canonicalCommentBody = (document, text) => {
136
+ const occupied = new Set(packageParagraphIds(document.package).map((id) => id.toUpperCase()));
137
+ let id = 1;
138
+ while (occupied.has(id.toString(16).padStart(8, "0").toUpperCase())) id += 1;
139
+ return [{
140
+ type: "paragraph",
141
+ paraId: id.toString(16).padStart(8, "0").toUpperCase(),
142
+ content: [{
143
+ type: "run",
144
+ content: [{
145
+ type: "text",
146
+ text
147
+ }]
148
+ }]
149
+ }];
150
+ };
151
+ //#endregion
152
+ export { CanonicalCommentError, canonicalCommentBody, compileCanonicalComments };
@@ -13,18 +13,27 @@ type CompositionOptions = {
13
13
  }) => void;
14
14
  refuse: (reason: string) => void;
15
15
  };
16
+ type CompositionCompletionEvidence = "provisional" | "explicitFinal";
17
+ type CompositionPhase = {
18
+ type: "active";
19
+ completion: CompositionCompletionEvidence;
20
+ } | {
21
+ type: "ended";
22
+ data: string | null;
23
+ completion: CompositionCompletionEvidence;
24
+ };
16
25
  type CompositionState = {
17
26
  type: "committed";
18
27
  } | {
19
28
  type: "provisional";
20
29
  baseline: EditorState;
21
30
  view: EditorView;
22
- phase: "active" | "ended";
31
+ phase: CompositionPhase;
23
32
  } | {
24
33
  type: "refused";
25
34
  baseline: EditorState;
26
35
  view: EditorView;
27
- phase: "active" | "ended";
36
+ phase: CompositionPhase;
28
37
  text: string | null;
29
38
  };
30
39
  type CompositionFinish = {
@@ -37,12 +46,16 @@ type CompositionFinish = {
37
46
  declare const createCanonicalComposition: (options: CompositionOptions) => {
38
47
  readonly status: CompositionState["type"];
39
48
  readonly active: boolean;
49
+ matchesCancellationRange: (view: EditorView, range: {
50
+ from: number;
51
+ to: number;
52
+ }) => boolean;
40
53
  start: (view: EditorView) => void;
41
- ended: (view: EditorView) => void;
54
+ ended: (view: EditorView, data: string | null) => void;
42
55
  forgetCompleted: () => void;
43
56
  readonly pendingFinal: boolean;
44
57
  authorizeLateFinal: (view: EditorView, text: string | null) => boolean;
45
- authorizeNative: (view: EditorView) => void;
58
+ authorizeNative: (view: EditorView, completion: CompositionCompletionEvidence) => void;
46
59
  flushed: (view: EditorView) => void;
47
60
  recover: (view: EditorView) => CompositionFinish;
48
61
  cancel: (view: EditorView) => CompositionFinish;
@@ -1,11 +1,52 @@
1
1
  import { splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
+ import { RUN_IDENTITY_MARK_NAME } from "../prosemirror/runIdentity.js";
2
3
  import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
3
4
  import { setCanonicalInputTimer } from "./canonicalInputTimer.js";
4
5
  import { CanonicalSessionError } from "./canonicalSession.js";
5
6
  import { Result } from "better-result";
6
7
  import { EditorState, TextSelection } from "prosemirror-state";
7
8
  //#region src/controller/canonicalComposition.ts
8
- const COMPOSITION_FLUSH_DELAY_MS = 25;
9
+ const matchesNativeReplacement = ({ baseline, proposed, from, to, text }) => {
10
+ const expected = baseline.tr.insertText(text, from, to);
11
+ if (expected.doc.eq(proposed.doc)) return true;
12
+ if (!baseline.doc.resolve(from).sameParent(baseline.doc.resolve(to))) return false;
13
+ if (expected.doc.content.size !== proposed.doc.content.size) return false;
14
+ const markType = baseline.schema.marks[RUN_IDENTITY_MARK_NAME];
15
+ if (markType === void 0) return false;
16
+ const position = expected.doc.resolve(from);
17
+ const paragraph = position.parent;
18
+ const start = position.start();
19
+ const native = proposed.doc.nodeAt(start - 1);
20
+ if (native === null || native.type !== paragraph.type) return false;
21
+ let index = 0;
22
+ let offset = 0;
23
+ const missing = [];
24
+ native.forEach((node, nodeOffset) => {
25
+ const end = nodeOffset + node.nodeSize;
26
+ let cursor = nodeOffset;
27
+ while (cursor < end && index < paragraph.childCount) {
28
+ const authored = paragraph.child(index);
29
+ const authoredEnd = offset + authored.nodeSize;
30
+ const overlapEnd = Math.min(end, authoredEnd);
31
+ if (node.isText && authored.isText && markType.isInSet(authored.marks) !== void 0 && markType.isInSet(node.marks) === void 0) {
32
+ const previous = missing.at(-1);
33
+ if (previous?.to === start + cursor) previous.to = start + overlapEnd;
34
+ else missing.push({
35
+ type: "omittedRunIdentity",
36
+ from: start + cursor,
37
+ to: start + overlapEnd
38
+ });
39
+ }
40
+ cursor = overlapEnd;
41
+ if (cursor === authoredEnd) {
42
+ offset = authoredEnd;
43
+ index += 1;
44
+ }
45
+ }
46
+ });
47
+ for (const range of missing) expected.removeMark(range.from, range.to, markType);
48
+ return expected.doc.eq(proposed.doc);
49
+ };
9
50
  /** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
10
51
  const createCanonicalComposition = (options) => {
11
52
  let state = { type: "committed" };
@@ -26,7 +67,13 @@ const createCanonicalComposition = (options) => {
26
67
  const selectedReplacement = (baseline, proposed) => {
27
68
  const { from, to } = baseline.selection;
28
69
  const text = selectedText(baseline, proposed);
29
- if (text === null || !baseline.tr.insertText(text, from, to).doc.eq(proposed.doc)) return null;
70
+ if (text === null || !matchesNativeReplacement({
71
+ baseline,
72
+ proposed,
73
+ from,
74
+ to,
75
+ text
76
+ })) return null;
30
77
  return {
31
78
  from,
32
79
  to,
@@ -50,24 +97,51 @@ const createCanonicalComposition = (options) => {
50
97
  if (!$from.sameParent($to)) return null;
51
98
  const start = $from.start();
52
99
  const next = proposed.doc.nodeAt(start - 1);
53
- if (!next || next.type !== $from.parent.type || next.content.size !== next.textContent.length) return null;
54
- const before = $from.parent.textContent;
55
- const after = next.textContent;
56
- let from = 0;
57
- while (from < before.length && from < after.length && before[from] === after[from]) from++;
58
- while (splitsSurrogatePair(before, from) || splitsSurrogatePair(after, from)) from--;
59
- let oldEnd = before.length;
60
- let newEnd = after.length;
100
+ if (!next || next.type !== $from.parent.type) return null;
101
+ const previous = $from.parent.content;
102
+ const changedFrom = previous.findDiffStart(next.content);
103
+ if (changedFrom === null) {
104
+ if (!baseline.doc.eq(proposed.doc)) return null;
105
+ return {
106
+ from: baseline.selection.from,
107
+ to: baseline.selection.from,
108
+ text: "",
109
+ semantic: "composition"
110
+ };
111
+ }
112
+ const changedEnd = previous.findDiffEnd(next.content);
113
+ if (changedEnd === null) return null;
114
+ let from = changedFrom;
115
+ let oldEnd = changedEnd.a;
116
+ let newEnd = changedEnd.b;
117
+ const overlap = from - Math.min(oldEnd, newEnd);
118
+ if (overlap > 0) {
119
+ oldEnd += overlap;
120
+ newEnd += overlap;
121
+ }
122
+ const before = previous.textBetween(0, previous.size, "", "");
123
+ const after = next.content.textBetween(0, next.content.size, "", "");
124
+ while (from < oldEnd && from < newEnd && before[from] === after[from]) from++;
61
125
  while (oldEnd > from && newEnd > from && before[oldEnd - 1] === after[newEnd - 1]) {
62
126
  oldEnd--;
63
127
  newEnd--;
64
128
  }
129
+ while (splitsSurrogatePair(before, from) || splitsSurrogatePair(after, from)) from--;
65
130
  while (splitsSurrogatePair(before, oldEnd) || splitsSurrogatePair(after, newEnd)) {
66
131
  oldEnd++;
67
132
  newEnd++;
68
133
  }
69
- const text = after.slice(from, newEnd);
70
- if (!baseline.tr.insertText(text, start + from, start + oldEnd).doc.eq(proposed.doc)) return null;
134
+ const removed = previous.cut(from, oldEnd);
135
+ const inserted = next.content.cut(from, newEnd);
136
+ const text = inserted.textBetween(0, inserted.size, "", "");
137
+ if (removed.textBetween(0, removed.size, "", "").length !== removed.size || text.length !== inserted.size) return null;
138
+ if (!matchesNativeReplacement({
139
+ baseline,
140
+ proposed,
141
+ from: start + from,
142
+ to: start + oldEnd,
143
+ text
144
+ })) return null;
71
145
  return {
72
146
  from: start + from,
73
147
  to: start + oldEnd,
@@ -99,7 +173,7 @@ const createCanonicalComposition = (options) => {
99
173
  view.updateState(pending.baseline);
100
174
  }
101
175
  options.end();
102
- if (!commit || view.isDestroyed) return { type: "cancelled" };
176
+ if (!commit || view.isDestroyed || pending.phase.type === "ended" && pending.phase.data === "" && pending.phase.completion === "provisional" && committedText === "") return { type: "cancelled" };
103
177
  if (pending.type === "refused") return {
104
178
  type: "refused",
105
179
  text: pending.text
@@ -130,7 +204,7 @@ const createCanonicalComposition = (options) => {
130
204
  };
131
205
  const schedule = (view) => {
132
206
  clearTimer();
133
- timer = setCanonicalInputTimer(() => finish(view, true), COMPOSITION_FLUSH_DELAY_MS);
207
+ timer = setCanonicalInputTimer(() => finish(view, true), 25);
134
208
  };
135
209
  return {
136
210
  get status() {
@@ -139,6 +213,18 @@ const createCanonicalComposition = (options) => {
139
213
  get active() {
140
214
  return state.type !== "committed";
141
215
  },
216
+ matchesCancellationRange: (view, range) => {
217
+ if (state.type === "committed" || state.view !== view) return false;
218
+ const text = selectedText(state.baseline, view.state);
219
+ if (text === null || text.length === 0) return false;
220
+ const from = state.baseline.selection.from;
221
+ if (range.from !== from || range.to !== from + text.length) return false;
222
+ const selection = view.dom.ownerDocument.getSelection();
223
+ if (!selection || selection.rangeCount !== 1) return false;
224
+ const selected = selection.getRangeAt(0);
225
+ if (!view.dom.contains(selected.startContainer) || !view.dom.contains(selected.endContainer)) return false;
226
+ return view.posAtDOM(selected.startContainer, selected.startOffset) === range.from && view.posAtDOM(selected.endContainer, selected.endOffset) === range.to;
227
+ },
142
228
  start: (view) => {
143
229
  if (state.type !== "committed") return;
144
230
  if (!options.begin()) return;
@@ -146,10 +232,13 @@ const createCanonicalComposition = (options) => {
146
232
  type: "provisional",
147
233
  baseline: view.state,
148
234
  view,
149
- phase: "active"
235
+ phase: {
236
+ type: "active",
237
+ completion: "provisional"
238
+ }
150
239
  };
151
240
  },
152
- ended: (view) => {
241
+ ended: (view, data) => {
153
242
  if (state.type === "committed") return;
154
243
  switch (state.type) {
155
244
  case "provisional":
@@ -157,7 +246,11 @@ const createCanonicalComposition = (options) => {
157
246
  type: "provisional",
158
247
  baseline: state.baseline,
159
248
  view: state.view,
160
- phase: "ended"
249
+ phase: {
250
+ type: "ended",
251
+ data,
252
+ completion: state.phase.completion
253
+ }
161
254
  };
162
255
  break;
163
256
  case "refused":
@@ -165,7 +258,11 @@ const createCanonicalComposition = (options) => {
165
258
  type: "refused",
166
259
  baseline: state.baseline,
167
260
  view: state.view,
168
- phase: "ended",
261
+ phase: {
262
+ type: "ended",
263
+ data,
264
+ completion: state.phase.completion
265
+ },
169
266
  text: state.text
170
267
  };
171
268
  break;
@@ -193,15 +290,23 @@ const createCanonicalComposition = (options) => {
193
290
  };
194
291
  return true;
195
292
  },
196
- authorizeNative: (view) => {
293
+ authorizeNative: (view, completion) => {
197
294
  authorizedNativeState = view.state;
295
+ if (state.type !== "committed" && completion === "explicitFinal") state.phase = state.phase.type === "active" ? {
296
+ type: "active",
297
+ completion
298
+ } : {
299
+ type: "ended",
300
+ data: state.phase.data,
301
+ completion
302
+ };
198
303
  },
199
304
  flushed: (view) => {
200
305
  const authorized = authorizedNativeState;
201
306
  queueMicrotask(() => {
202
307
  if (authorizedNativeState === authorized) authorizedNativeState = null;
203
308
  });
204
- if (state.type !== "committed" && state.phase === "ended") schedule(view);
309
+ if (state.type !== "committed" && state.phase.type === "ended") schedule(view);
205
310
  },
206
311
  recover: (view) => finish(view, true),
207
312
  cancel: (view) => finish(view, false),
@@ -106,8 +106,13 @@ type CanonicalInlineProjectionArgs = {
106
106
  source: document_d_exports.Paragraph;
107
107
  paragraph: Node;
108
108
  pairedBookmarkIds: ReadonlySet<number>;
109
+ commentContext?: {
110
+ rangedIds: ReadonlySet<number>;
111
+ pointIds: ReadonlySet<number>;
112
+ openIds: ReadonlySet<number>;
113
+ };
109
114
  };
110
115
  /** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
111
- declare const projectCanonicalInline: ({ source, paragraph, pairedBookmarkIds }: CanonicalInlineProjectionArgs) => Result<InlineProjection, InlineProjectionError>;
116
+ declare const projectCanonicalInline: ({ source, paragraph, pairedBookmarkIds, commentContext }: CanonicalInlineProjectionArgs) => Result<InlineProjection, InlineProjectionError>;
112
117
  //#endregion
113
118
  export { CANONICAL_NATIVE_INLINE_POLICIES, CANONICAL_SOURCE_INLINE_POLICIES, CanonicalInlineGap, canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline };