@stll/folio-core 0.56.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 (142) hide show
  1. package/dist/ai-edits/apply.js +45 -14
  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.js +7 -1
  11. package/dist/controller/canonicalComments.d.ts +41 -0
  12. package/dist/controller/canonicalComments.js +152 -0
  13. package/dist/controller/canonicalComposition.d.ts +4 -0
  14. package/dist/controller/canonicalComposition.js +43 -11
  15. package/dist/controller/canonicalInlineProjection.d.ts +6 -1
  16. package/dist/controller/canonicalInlineProjection.js +86 -3
  17. package/dist/controller/canonicalInput.js +10 -1
  18. package/dist/controller/canonicalInputTimer.d.ts +10 -1
  19. package/dist/controller/canonicalInputTimer.js +24 -1
  20. package/dist/controller/canonicalPublicOperations.d.ts +2 -2
  21. package/dist/controller/canonicalPublicOperations.js +107 -15
  22. package/dist/controller/canonicalSelection.d.ts +18 -0
  23. package/dist/controller/canonicalSelection.js +32 -0
  24. package/dist/controller/canonicalSession.d.ts +3 -0
  25. package/dist/controller/canonicalSession.js +58 -24
  26. package/dist/controller/canonicalStoryEditor.d.ts +2 -0
  27. package/dist/controller/canonicalStoryEditor.js +20 -0
  28. package/dist/controller/folioEditor.js +13 -0
  29. package/dist/controller/headerFooterEditorManager.d.ts +1 -1
  30. package/dist/controller/headerFooterEditorManager.js +25 -11
  31. package/dist/controller/hiddenEditorApi.d.ts +13 -2
  32. package/dist/controller/hiddenEditorApi.js +4 -0
  33. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  34. package/dist/controller/hiddenEditorManager.js +166 -13
  35. package/dist/controller/noteEditorManager.d.ts +1 -1
  36. package/dist/controller/noteEditorManager.js +28 -13
  37. package/dist/docx/builtInStyles.js +13 -6
  38. package/dist/docx/containerChildren.gen.d.ts +3 -1
  39. package/dist/docx/containerChildren.gen.js +33 -1
  40. package/dist/docx/fieldParser.js +14 -2
  41. package/dist/docx/hyperlinkParser.d.ts +1 -1
  42. package/dist/docx/hyperlinkParser.js +20 -1
  43. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  44. package/dist/docx/inlineWrapperContent.js +4 -4
  45. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  46. package/dist/docx/linkedSdtPreservation.js +37 -0
  47. package/dist/docx/paragraphParser.js +88 -67
  48. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  49. package/dist/docx/paragraphPropertySource.js +107 -1
  50. package/dist/docx/preservedRunContent.js +2 -1
  51. package/dist/docx/relsParser.js +26 -2
  52. package/dist/docx/rezip.js +9 -6
  53. package/dist/docx/selectiveSave.js +10 -8
  54. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  55. package/dist/docx/serializer/commentSerializer.js +7 -3
  56. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  57. package/dist/docx/server/materializeYjsDocx.js +14 -6
  58. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  59. package/dist/i18n/messages/ar.gen.js +4 -0
  60. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  61. package/dist/i18n/messages/catalogs.gen.js +68 -0
  62. package/dist/i18n/messages/cs.gen.js +4 -0
  63. package/dist/i18n/messages/de.gen.js +4 -0
  64. package/dist/i18n/messages/en.gen.js +4 -0
  65. package/dist/i18n/messages/es.gen.js +4 -0
  66. package/dist/i18n/messages/et.gen.js +4 -0
  67. package/dist/i18n/messages/fr.gen.js +4 -0
  68. package/dist/i18n/messages/he.gen.js +4 -0
  69. package/dist/i18n/messages/hi.gen.js +4 -0
  70. package/dist/i18n/messages/hu.gen.js +4 -0
  71. package/dist/i18n/messages/lt.gen.js +4 -0
  72. package/dist/i18n/messages/lv.gen.js +4 -0
  73. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  74. package/dist/i18n/messages/pl.gen.js +4 -0
  75. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  76. package/dist/i18n/messages/sk.gen.js +4 -0
  77. package/dist/i18n/messages/tr.gen.js +4 -0
  78. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +2 -1
  81. package/dist/internal/revisionResolutionInline.js +25 -3
  82. package/dist/markdown/renderRuns.js +1 -11
  83. package/dist/panel-layout.d.ts +11 -9
  84. package/dist/panel-layout.js +13 -9
  85. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  86. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  87. package/dist/prosemirror/attrs/index.js +5 -0
  88. package/dist/prosemirror/commands/comments.js +60 -35
  89. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  90. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  91. package/dist/prosemirror/commentOps.js +15 -1
  92. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  93. package/dist/prosemirror/conversion/toProseDoc.js +119 -18
  94. package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
  95. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  96. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  97. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
  98. package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
  99. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  100. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  101. package/dist/prosemirror/fieldRepresentation.js +18 -0
  102. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  103. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  104. package/dist/prosemirror/listNumbering.js +11 -5
  105. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  106. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  107. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  108. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  109. package/dist/prosemirror/noteReferenceReview.js +14 -1
  110. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  111. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  112. package/dist/prosemirror/plugins/suggestionMode.js +47 -49
  113. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  114. package/dist/prosemirror/preservedXmlReview.js +35 -0
  115. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  116. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  117. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  118. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  119. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  120. package/dist/prosemirror/runStyleFormatting.js +3 -1
  121. package/dist/prosemirror/schema/marks.d.ts +1 -0
  122. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  123. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  124. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  125. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  126. package/dist/prosemirror/textInput.d.ts +5 -1
  127. package/dist/prosemirror/textInput.js +3 -3
  128. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  129. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  130. package/dist/prosemirror/validation.js +3 -5
  131. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  132. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  133. package/dist/types/canonicalCapabilities.d.ts +6 -20
  134. package/dist/types/canonicalCapabilities.js +6 -20
  135. package/dist/types/canonicalComments.d.ts +51 -0
  136. package/dist/types/canonicalComments.js +0 -0
  137. package/dist/types/canonicalSections.d.ts +12 -0
  138. package/dist/types/canonicalSections.js +0 -0
  139. package/dist/utils/headingCollector.js +45 -4
  140. package/dist/utils/outlineDepth.d.ts +9 -0
  141. package/dist/utils/outlineDepth.js +9 -0
  142. package/package.json +2 -2
@@ -37,6 +37,10 @@ type CompositionFinish = {
37
37
  declare const createCanonicalComposition: (options: CompositionOptions) => {
38
38
  readonly status: CompositionState["type"];
39
39
  readonly active: boolean;
40
+ matchesCancellationRange: (view: EditorView, range: {
41
+ from: number;
42
+ to: number;
43
+ }) => boolean;
40
44
  start: (view: EditorView) => void;
41
45
  ended: (view: EditorView) => void;
42
46
  forgetCompleted: () => void;
@@ -5,7 +5,6 @@ import { CanonicalSessionError } from "./canonicalSession.js";
5
5
  import { Result } from "better-result";
6
6
  import { EditorState, TextSelection } from "prosemirror-state";
7
7
  //#region src/controller/canonicalComposition.ts
8
- const COMPOSITION_FLUSH_DELAY_MS = 25;
9
8
  /** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
10
9
  const createCanonicalComposition = (options) => {
11
10
  let state = { type: "committed" };
@@ -50,23 +49,44 @@ const createCanonicalComposition = (options) => {
50
49
  if (!$from.sameParent($to)) return null;
51
50
  const start = $from.start();
52
51
  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;
52
+ if (!next || next.type !== $from.parent.type) return null;
53
+ const previous = $from.parent.content;
54
+ const changedFrom = previous.findDiffStart(next.content);
55
+ if (changedFrom === null) {
56
+ if (!baseline.doc.eq(proposed.doc)) return null;
57
+ return {
58
+ from: baseline.selection.from,
59
+ to: baseline.selection.from,
60
+ text: "",
61
+ semantic: "composition"
62
+ };
63
+ }
64
+ const changedEnd = previous.findDiffEnd(next.content);
65
+ if (changedEnd === null) return null;
66
+ let from = changedFrom;
67
+ let oldEnd = changedEnd.a;
68
+ let newEnd = changedEnd.b;
69
+ const overlap = from - Math.min(oldEnd, newEnd);
70
+ if (overlap > 0) {
71
+ oldEnd += overlap;
72
+ newEnd += overlap;
73
+ }
74
+ const before = previous.textBetween(0, previous.size, "", "");
75
+ const after = next.content.textBetween(0, next.content.size, "", "");
76
+ while (from < oldEnd && from < newEnd && before[from] === after[from]) from++;
61
77
  while (oldEnd > from && newEnd > from && before[oldEnd - 1] === after[newEnd - 1]) {
62
78
  oldEnd--;
63
79
  newEnd--;
64
80
  }
81
+ while (splitsSurrogatePair(before, from) || splitsSurrogatePair(after, from)) from--;
65
82
  while (splitsSurrogatePair(before, oldEnd) || splitsSurrogatePair(after, newEnd)) {
66
83
  oldEnd++;
67
84
  newEnd++;
68
85
  }
69
- const text = after.slice(from, newEnd);
86
+ const removed = previous.cut(from, oldEnd);
87
+ const inserted = next.content.cut(from, newEnd);
88
+ const text = inserted.textBetween(0, inserted.size, "", "");
89
+ if (removed.textBetween(0, removed.size, "", "").length !== removed.size || text.length !== inserted.size) return null;
70
90
  if (!baseline.tr.insertText(text, start + from, start + oldEnd).doc.eq(proposed.doc)) return null;
71
91
  return {
72
92
  from: start + from,
@@ -130,7 +150,7 @@ const createCanonicalComposition = (options) => {
130
150
  };
131
151
  const schedule = (view) => {
132
152
  clearTimer();
133
- timer = setCanonicalInputTimer(() => finish(view, true), COMPOSITION_FLUSH_DELAY_MS);
153
+ timer = setCanonicalInputTimer(() => finish(view, true), 25);
134
154
  };
135
155
  return {
136
156
  get status() {
@@ -139,6 +159,18 @@ const createCanonicalComposition = (options) => {
139
159
  get active() {
140
160
  return state.type !== "committed";
141
161
  },
162
+ matchesCancellationRange: (view, range) => {
163
+ if (state.type === "committed" || state.view !== view) return false;
164
+ const text = selectedText(state.baseline, view.state);
165
+ if (text === null || text.length === 0) return false;
166
+ const from = state.baseline.selection.from;
167
+ if (range.from !== from || range.to !== from + text.length) return false;
168
+ const selection = view.dom.ownerDocument.getSelection();
169
+ if (!selection || selection.rangeCount !== 1) return false;
170
+ const selected = selection.getRangeAt(0);
171
+ if (!view.dom.contains(selected.startContainer) || !view.dom.contains(selected.endContainer)) return false;
172
+ return view.posAtDOM(selected.startContainer, selected.startOffset) === range.from && view.posAtDOM(selected.endContainer, selected.endOffset) === range.to;
173
+ },
142
174
  start: (view) => {
143
175
  if (state.type !== "committed") return;
144
176
  if (!options.begin()) return;
@@ -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 };
@@ -1,5 +1,5 @@
1
1
  import { readNoteMarkerAttrs } from "../internal/noteMarkerAttrs.js";
2
- import { readFootnoteRefMarkAttrs, readPreservedXmlAttrs } from "../prosemirror/attrs/index.js";
2
+ import { readCommentMarkAttrs, readFootnoteRefMarkAttrs, readPreservedXmlAttrs } from "../prosemirror/attrs/index.js";
3
3
  import { bookmarkMarkerFromAttrs, readBookmarkBoundaryAttrs } from "../prosemirror/bookmarkBoundaryAttrs.js";
4
4
  import { HYPHEN_TEXT_CARRIERS } from "../prosemirror/conversion/hyphenTextCarriers.js";
5
5
  import { readMoveRangeBoundaryAttrs } from "../prosemirror/moveRangeBoundaryAttrs.js";
@@ -279,10 +279,62 @@ const sameSourceMarker = (left, right) => {
279
279
  default: return false;
280
280
  }
281
281
  };
282
+ /** Marks carry nonempty ranges; explicit range anchors carry empty ranges. */
283
+ const commentMarkTransitions = ({ source, paragraph, commentContext }) => {
284
+ const spans = inlineLeafSpans(source.content);
285
+ const rangedIds = new Set(commentContext?.rangedIds);
286
+ const pointIds = new Set(commentContext?.pointIds);
287
+ const finalIds = new Set(commentContext?.openIds);
288
+ for (const { node } of spans) {
289
+ if (node.type === "commentRangeStart" || node.type === "commentRangeEnd") rangedIds.add(node.id);
290
+ if (node.type === "commentReference") pointIds.add(node.id);
291
+ if (node.type === "commentRangeStart") finalIds.add(node.id);
292
+ if (node.type === "commentRangeEnd") finalIds.delete(node.id);
293
+ }
294
+ const transitions = /* @__PURE__ */ new Map();
295
+ let previous = new Set(commentContext?.openIds);
296
+ let issue;
297
+ const record = (position, current) => {
298
+ const starts = new Set([...current].filter((id) => !previous.has(id)));
299
+ const ends = new Set([...previous].filter((id) => !current.has(id)));
300
+ if (starts.size > 0 || ends.size > 0) transitions.set(position, {
301
+ starts,
302
+ ends
303
+ });
304
+ previous = current;
305
+ };
306
+ paragraph.forEach((node, position) => {
307
+ if (issue !== void 0) return;
308
+ const current = /* @__PURE__ */ new Set();
309
+ for (const mark of node.marks) {
310
+ if (mark.type.name !== "comment") continue;
311
+ const attrs = readCommentMarkAttrs(mark);
312
+ if (!attrs.ok) {
313
+ issue = "Canonical editing requires valid comment mark attributes.";
314
+ return;
315
+ }
316
+ const id = attrs.value.commentId;
317
+ if (rangedIds.has(id)) current.add(id);
318
+ else if (!pointIds.has(id)) {
319
+ issue = "Canonical editing cannot attribute a comment mark to its source.";
320
+ return;
321
+ }
322
+ }
323
+ record(position, current);
324
+ });
325
+ record(paragraph.content.size, finalIds);
326
+ return issue === void 0 ? Result.ok(transitions) : refuse(issue);
327
+ };
282
328
  /** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
283
- const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
329
+ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds, commentContext }) => {
284
330
  const native = canonicalNativeCells(paragraph);
285
331
  if (native.isErr()) return native;
332
+ const comments = commentMarkTransitions({
333
+ source,
334
+ paragraph,
335
+ ...commentContext === void 0 ? {} : { commentContext }
336
+ });
337
+ if (comments.isErr()) return comments;
286
338
  const spans = inlineLeafSpans(source.content);
287
339
  const erasedBookmarks = /* @__PURE__ */ new Set();
288
340
  const collectErasedBookmarks = (items, context) => {
@@ -296,6 +348,28 @@ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
296
348
  }
297
349
  };
298
350
  collectErasedBookmarks(source.content, "paragraph");
351
+ const markedCommentIds = /* @__PURE__ */ new Set();
352
+ paragraph.descendants((node) => {
353
+ for (const mark of node.marks) {
354
+ if (mark.type.name !== "comment") continue;
355
+ const attrs = readCommentMarkAttrs(mark);
356
+ if (attrs.ok) markedCommentIds.add(attrs.value.commentId);
357
+ }
358
+ });
359
+ const commentRangeOffsets = /* @__PURE__ */ new Map();
360
+ for (const { node, before } of spans) {
361
+ if (node.type !== "commentRangeStart" && node.type !== "commentRangeEnd") continue;
362
+ const offsets = commentRangeOffsets.get(node.id) ?? {};
363
+ if (node.type === "commentRangeStart") offsets.start = before.offset;
364
+ else offsets.end = before.offset;
365
+ commentRangeOffsets.set(node.id, offsets);
366
+ }
367
+ const erasedCommentMarkers = /* @__PURE__ */ new Set();
368
+ for (const { node } of spans) {
369
+ if (node.type !== "commentRangeStart" && node.type !== "commentRangeEnd") continue;
370
+ const offsets = commentRangeOffsets.get(node.id);
371
+ if (!(offsets?.start !== void 0 && offsets.end !== void 0 && offsets.start === offsets.end) && markedCommentIds.has(node.id)) erasedCommentMarkers.add(node);
372
+ }
299
373
  const text = paragraphLogicalText(source);
300
374
  const boundaries = Array.from({ length: text.length + 1 }, () => []);
301
375
  let index = 0;
@@ -314,7 +388,15 @@ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
314
388
  const consumeErasedLeaves = (position) => {
315
389
  while (consumed === 0) {
316
390
  const span = spans.at(index);
317
- if (!span || span.before.offset !== span.after.offset || !(erasedBookmarks.has(span.node) || CANONICAL_SOURCE_INLINE_POLICIES[span.node.type] === "container" && sourceContainerChildren(span.node).length === 0)) break;
391
+ if (position !== null && span && (span.node.type === "commentRangeStart" || span.node.type === "commentRangeEnd")) {
392
+ const transition = comments.value.get(position);
393
+ if ((span.node.type === "commentRangeStart" ? transition?.starts : transition?.ends)?.delete(span.node.id)) {
394
+ index += 1;
395
+ addGap(position, span.after);
396
+ continue;
397
+ }
398
+ }
399
+ if (!span || span.before.offset !== span.after.offset || !(erasedBookmarks.has(span.node) || erasedCommentMarkers.has(span.node) || CANONICAL_SOURCE_INLINE_POLICIES[span.node.type] === "container" && sourceContainerChildren(span.node).length === 0)) break;
318
400
  index += 1;
319
401
  if (position !== null) addGap(position, span.after);
320
402
  }
@@ -381,6 +463,7 @@ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
381
463
  }
382
464
  }
383
465
  consumeErasedLeaves(paragraph.content.size);
466
+ if ([...comments.value.values()].some(({ starts, ends }) => starts.size > 0 || ends.size > 0)) return refuse("Canonical editing cannot attribute a comment mark transition to its source.");
384
467
  const missing = spans.at(index);
385
468
  if (missing || consumed !== 0) return refuse(`Canonical editing cannot display the source positions of ${missing?.node.type ?? "incomplete text"}.`);
386
469
  if (boundaries.some((gaps) => gaps.length === 0)) return refuse("Canonical editing lost a source character boundary.");
@@ -1,4 +1,4 @@
1
- import { handleEditorBeforeInput } from "../prosemirror/textInput.js";
1
+ import { getEditorInputTargetRange, handleEditorBeforeInput } from "../prosemirror/textInput.js";
2
2
  import { createCanonicalComposition } from "./canonicalComposition.js";
3
3
  import { deletionRange, isCanonicalJoinBoundary } from "./canonicalSession.js";
4
4
  import { ReplaceStep } from "prosemirror-transform";
@@ -237,6 +237,15 @@ const createCanonicalInputBoundary = (options) => {
237
237
  return refuseEvent(event, "Composition has no captured canonical baseline.");
238
238
  }
239
239
  if (composition.active) {
240
+ if (view.composing && (event.inputType === "deleteContentBackward" || event.inputType === "deleteContentForward")) {
241
+ const range = getEditorInputTargetRange(view, event);
242
+ if (range && composition.matchesCancellationRange(view, range)) {
243
+ event.preventDefault();
244
+ composition.cancel(view);
245
+ closeGroup();
246
+ return true;
247
+ }
248
+ }
240
249
  const nativeFinal = view.composing && (event.inputType === "insertText" || event.inputType === "insertReplacementText");
241
250
  const nativeStillComposing = view.composing;
242
251
  const recovered = composition.recover(view);
@@ -1,7 +1,16 @@
1
+ import { EditorView } from "prosemirror-view";
1
2
  //#region src/controller/canonicalInputTimer.d.ts
2
3
  /** Shared by timer producers and browser diagnostics, regardless of bundling. */
3
4
  declare const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
5
+ /** Delay after native composition end before the canonical session settles it. */
6
+ declare const CANONICAL_COMPOSITION_FLUSH_DELAY_MS = 25;
4
7
  /** Mark the scheduled wrapper, never the caller's potentially shared callback. */
5
8
  declare const setCanonicalInputTimer: (callback: () => void, delay: number) => NodeJS.Timeout;
9
+ /**
10
+ * Run `retry` once the view's canonical composition has settled. The session
11
+ * settles on an equal-delay timer armed at native composition end, so a retry
12
+ * armed at or after that point runs after it. Returns a disposer.
13
+ */
14
+ declare const afterCanonicalCompositionSettles: (view: EditorView, retry: () => void) => (() => void);
6
15
  //#endregion
7
- export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
16
+ export { CANONICAL_COMPOSITION_FLUSH_DELAY_MS, CANONICAL_INPUT_TIMER_OWNER, afterCanonicalCompositionSettles, setCanonicalInputTimer };
@@ -1,11 +1,34 @@
1
1
  //#region src/controller/canonicalInputTimer.ts
2
2
  /** Shared by timer producers and browser diagnostics, regardless of bundling. */
3
3
  const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
4
+ /** Delay after native composition end before the canonical session settles it. */
5
+ const CANONICAL_COMPOSITION_FLUSH_DELAY_MS = 25;
4
6
  /** Mark the scheduled wrapper, never the caller's potentially shared callback. */
5
7
  const setCanonicalInputTimer = (callback, delay) => {
6
8
  const ownedCallback = () => callback();
7
9
  Object.defineProperty(ownedCallback, Symbol.for(CANONICAL_INPUT_TIMER_OWNER), { value: true });
8
10
  return setTimeout(ownedCallback, delay);
9
11
  };
12
+ /**
13
+ * Run `retry` once the view's canonical composition has settled. The session
14
+ * settles on an equal-delay timer armed at native composition end, so a retry
15
+ * armed at or after that point runs after it. Returns a disposer.
16
+ */
17
+ const afterCanonicalCompositionSettles = (view, retry) => {
18
+ let timer;
19
+ const schedule = () => {
20
+ timer = setCanonicalInputTimer(retry, 25);
21
+ };
22
+ const onEnd = () => {
23
+ view.dom.removeEventListener("compositionend", onEnd);
24
+ schedule();
25
+ };
26
+ if (view.composing) view.dom.addEventListener("compositionend", onEnd);
27
+ else schedule();
28
+ return () => {
29
+ view.dom.removeEventListener("compositionend", onEnd);
30
+ if (timer !== void 0) clearTimeout(timer);
31
+ };
32
+ };
10
33
  //#endregion
11
- export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
34
+ export { CANONICAL_COMPOSITION_FLUSH_DELAY_MS, CANONICAL_INPUT_TIMER_OWNER, afterCanonicalCompositionSettles, setCanonicalInputTimer };
@@ -15,8 +15,8 @@ declare const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS: {
15
15
  readonly insertAfterBlock: "publicOps.unsupportedInline";
16
16
  readonly insertBeforeBlock: "publicOps.unsupportedInline";
17
17
  readonly deleteBlock: "publicOps.unsupportedInline";
18
- readonly commentOnBlock: "publicOps.comments";
19
- readonly commentOnRange: "publicOps.comments";
18
+ readonly commentOnBlock: "compile";
19
+ readonly commentOnRange: "compile";
20
20
  readonly insertTable: "publicOps.tableProjection";
21
21
  readonly insertSignatureTable: "publicOps.tableProjection";
22
22
  readonly deleteTable: "publicOps.tableProjection";
@@ -6,9 +6,10 @@ import { wordDiffSessionFromOptions } from "../ai-edits/word-diff.js";
6
6
  import { getFolioDocumentOperationIssues, getFolioDocumentOperationReceiptsForStory, parseFolioDocumentOperationBatch } from "../document-operations.js";
7
7
  import { maxAnnotationIdInDoc } from "../prosemirror/plugins/revisionIds.js";
8
8
  import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
9
+ import { canonicalCommentBody, compileCanonicalComments } from "./canonicalComments.js";
9
10
  import { Result, TaggedError, panic } from "better-result";
10
11
  import { Mark } from "prosemirror-model";
11
- import { OP_STORIES, applyDocumentOps, compileEditorIntent, createEditorIntentIdAllocator, documentStories } from "@stll/docx-core/ops";
12
+ import { OP_STORIES, applyDocumentOps, compileEditorIntent, createEditorIntentIdAllocator, documentStories, findStoryBody, freshCommentId, paragraphLength } from "@stll/docx-core/ops";
12
13
  //#region src/controller/canonicalPublicOperations.ts
13
14
  /** Public batches compile against the read projection; only the canonical journal publishes. */
14
15
  /** A new public kind cannot bypass a canonical compiler/refusal decision. */
@@ -23,8 +24,8 @@ const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS = {
23
24
  insertAfterBlock: CANONICAL_GAP.publicUnsupportedInline,
24
25
  insertBeforeBlock: CANONICAL_GAP.publicUnsupportedInline,
25
26
  deleteBlock: CANONICAL_GAP.publicUnsupportedInline,
26
- commentOnBlock: CANONICAL_GAP.publicComments,
27
- commentOnRange: CANONICAL_GAP.publicComments,
27
+ commentOnBlock: "compile",
28
+ commentOnRange: "compile",
28
29
  insertTable: CANONICAL_GAP.publicTableProjection,
29
30
  insertSignatureTable: CANONICAL_GAP.publicTableProjection,
30
31
  deleteTable: CANONICAL_GAP.publicTableProjection,
@@ -35,6 +36,21 @@ const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS = {
35
36
  mergeTableCells: CANONICAL_GAP.publicTableProjection,
36
37
  splitTableCell: CANONICAL_GAP.publicTableProjection
37
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
+ };
38
54
  const compilePublicIntents = ({ document, intents, mode, author, date, nextRevisionId, allocate }) => {
39
55
  const ops = [];
40
56
  const revisions = [];
@@ -183,8 +199,8 @@ var CanonicalPublicOperations = class {
183
199
  continue;
184
200
  }
185
201
  const disposition = CANONICAL_PUBLIC_OPERATION_DISPOSITIONS[operation.type];
186
- if (disposition !== "compile" || "comment" in operation && operation.comment !== void 0) {
187
- refusals.set(operation.id, { gap: disposition === "compile" ? CANONICAL_GAP.publicComments : disposition });
202
+ if (disposition !== "compile") {
203
+ refusals.set(operation.id, { gap: disposition });
188
204
  skip(operation.id, "unsupportedBlock");
189
205
  continue;
190
206
  }
@@ -217,7 +233,8 @@ var CanonicalPublicOperations = class {
217
233
  const appliedById = /* @__PURE__ */ new Map();
218
234
  const allocate = createEditorIntentIdAllocator();
219
235
  const date = options.revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
220
- for (const { operation, intents, normalizations: adjustments } of resolved) {
236
+ for (const resolvedOperation of resolved) {
237
+ const { operation, intents, normalizations: adjustments } = resolvedOperation;
221
238
  const compiled = compilePublicIntents({
222
239
  document,
223
240
  intents,
@@ -232,18 +249,71 @@ var CanonicalPublicOperations = class {
232
249
  skip(operation.id, "unsupportedBlock", compiled.error.message);
233
250
  continue;
234
251
  }
235
- if (compiled.value.ops.length === 0) {
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) {
236
305
  skip(operation.id, "noopOperation");
237
306
  continue;
238
307
  }
239
308
  normalizations.push(...adjustments);
240
- document = compiled.value.document;
241
- ops.push(...compiled.value.ops);
242
- nextRevisionId = compiled.value.nextRevisionId;
309
+ document = nextDocument;
310
+ ops.push(...operationOps);
311
+ nextRevisionId = Math.max(compiled.value.nextRevisionId, (commentId ?? -1) + 1);
243
312
  const revisionIds = compiled.value.revisions;
244
313
  const revisionId = revisionIds.at(0);
245
314
  appliedById.set(operation.id, {
246
315
  id: operation.id,
316
+ ...commentId === void 0 ? {} : { commentId },
247
317
  ...revisionId === void 0 ? {} : {
248
318
  revisionId,
249
319
  revisionIds
@@ -328,17 +398,18 @@ var CanonicalPublicOperations = class {
328
398
  reason,
329
399
  message: message ?? `The operation was refused: ${reason}.`
330
400
  }));
331
- if ("comment" in operation) return refusal("unsupportedBlock", "Canonical comment operations are unavailable.");
332
401
  switch (operation.type) {
333
402
  case "replaceInBlock":
334
403
  case "replaceRange":
335
404
  case "replaceBlock":
336
405
  case "splitBlock":
337
406
  case "formatRange":
338
- case "mergeBlockWithNext": break;
407
+ case "mergeBlockWithNext":
408
+ case "commentOnRange":
409
+ case "commentOnBlock": break;
339
410
  default: return refusal("unsupportedBlock", `Canonical public operation ${operation.type} is unavailable.`);
340
411
  }
341
- const blockId = operation.type === "replaceRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
412
+ const blockId = operation.type === "replaceRange" || operation.type === "formatRange" || operation.type === "commentOnRange" ? operation.range.blockId : operation.blockId;
342
413
  const anchor = Object.hasOwn(options.snapshot.anchors, blockId) ? options.snapshot.anchors[blockId] : void 0;
343
414
  const projection = this.options.session.projectStory(story);
344
415
  if (!anchor || projection.isErr()) return refusal("missingBlock");
@@ -364,8 +435,17 @@ var CanonicalPublicOperations = class {
364
435
  end = start + operation.find.length;
365
436
  text = operation.replace;
366
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;
367
446
  case "replaceRange":
368
447
  case "formatRange":
448
+ case "commentOnRange":
369
449
  start = operation.range.startOffset;
370
450
  end = operation.range.endOffset;
371
451
  if (hashFolioAIBlockText(clean.text.slice(start, end)) !== operation.range.selectedTextHash) return refusal("staleRange");
@@ -401,7 +481,19 @@ var CanonicalPublicOperations = class {
401
481
  const from = projection.value.addressAt(range.from);
402
482
  const to = projection.value.addressAt(range.to);
403
483
  if (from.isErr() || to.isErr()) return refusal("unsupportedBlock");
404
- if (operation.type !== "splitBlock" && operation.type !== "formatRange" && operation.type !== "mergeBlockWithNext" && clean.text.slice(start, end) === text) return refusal("noopOperation");
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");
405
497
  let intent;
406
498
  switch (operation.type) {
407
499
  case "splitBlock":
@@ -544,7 +636,7 @@ var CanonicalPublicOperations = class {
544
636
  if (structural) claimedTo = paragraph.start + paragraph.node.content.size;
545
637
  if (nextParagraph) claimedTo = nextParagraph.start + nextParagraph.node.content.size;
546
638
  return Result.ok({
547
- operation,
639
+ ...resolvedComment,
548
640
  intents,
549
641
  normalizations,
550
642
  from: structural ? paragraph.start : range.from,
@@ -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 };
@@ -59,6 +59,7 @@ type CanonicalCommit = {
59
59
  document: document_d_exports.Document;
60
60
  projection: CanonicalProjection;
61
61
  bodyProjection: CanonicalProjection;
62
+ selection: CanonicalSelection;
62
63
  touched: TouchedBlocks;
63
64
  version: number;
64
65
  origin: CanonicalOrigin;
@@ -116,6 +117,8 @@ declare class CanonicalSession {
116
117
  private readonly undone;
117
118
  private readonly styles;
118
119
  constructor({ document, projection, styles }: CanonicalSessionSeedOptions);
120
+ /** Render only the committed comment projection while provisional IME blocks document snapshots. */
121
+ getCommittedComments(): import("@stll/docx-core/model").Comment[];
119
122
  setMode(mode: CanonicalSessionMode): void;
120
123
  hasStyle(styleId: string): boolean;
121
124
  private advanceBlockId;