@stll/folio-core 0.57.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 (39) hide show
  1. package/dist/ai-edits/apply.js +77 -8
  2. package/dist/controller/canonicalClipboard.d.ts +3 -1
  3. package/dist/controller/canonicalClipboard.js +24 -12
  4. package/dist/controller/canonicalComposition.d.ts +13 -4
  5. package/dist/controller/canonicalComposition.js +82 -9
  6. package/dist/controller/canonicalInput.d.ts +1 -1
  7. package/dist/controller/canonicalInput.js +4 -3
  8. package/dist/controller/canonicalSelection.js +10 -1
  9. package/dist/controller/canonicalSession.d.ts +1 -1
  10. package/dist/controller/canonicalSession.js +59 -23
  11. package/dist/controller/canonicalStoryEditor.d.ts +1 -1
  12. package/dist/controller/canonicalStoryEditor.js +1 -1
  13. package/dist/controller/canonicalStructure.js +8 -3
  14. package/dist/controller/hiddenEditorApi.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +1 -1
  16. package/dist/docx/listRendering.d.ts +18 -0
  17. package/dist/{prosemirror/conversion/listRenderingDefinition.js → docx/listRendering.js} +17 -3
  18. package/dist/docx/server/materializeYjsDocx.js +1 -1
  19. package/dist/markdown/internals.js +7 -0
  20. package/dist/markdown/renderParagraph.js +11 -12
  21. package/dist/markdown/renderRuns.js +2 -2
  22. package/dist/markdown/types.d.ts +3 -0
  23. package/dist/prosemirror/attrs/index.js +4 -0
  24. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  25. package/dist/prosemirror/conversion/fromProseDoc.js +128 -27
  26. package/dist/prosemirror/conversion/toProseDoc.js +10 -23
  27. package/dist/prosemirror/extensions/nodes/ImageExtension.js +8 -1
  28. package/dist/prosemirror/imagePictureNames.d.ts +12 -0
  29. package/dist/prosemirror/imagePictureNames.js +20 -0
  30. package/dist/prosemirror/paragraphFormattingProvenance.js +1 -0
  31. package/dist/prosemirror/runStyleFormatting.d.ts +72 -2
  32. package/dist/prosemirror/runStyleFormatting.js +18 -1
  33. package/dist/prosemirror/schema/nodes.d.ts +1 -0
  34. package/dist/prosemirror/styles/styleResolver.d.ts +4 -1
  35. package/dist/prosemirror/styles/styleResolver.js +32 -11
  36. package/dist/types/canonicalCapabilities.d.ts +15 -1
  37. package/dist/types/canonicalCapabilities.js +15 -1
  38. package/package.json +2 -2
  39. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +0 -6
@@ -3,7 +3,7 @@ import { formattingEquals } from "../docx/runConsolidator.js";
3
3
  import { isStyleSourcedParagraphNumbering } from "../internal/paragraphFormattingSerialization.js";
4
4
  import { addTrackedDeletionMark } from "../prosemirror/addTrackedDeletionMark.js";
5
5
  import { deleteTextBoxesAnchoredAt, droppedTextBoxAnchorIds, endOfParagraphWithItsTextBoxes, gatherTextBoxesAfterParagraph, textBoxAnchorIdOf, textBoxAnchorIdsBetween } from "../prosemirror/anchoredTextBoxes.js";
6
- import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTableRowAttrs } from "../prosemirror/attrs/index.js";
6
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTableRowAttrs, mergeParagraphAttrs } from "../prosemirror/attrs/index.js";
7
7
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "../prosemirror/commands/propertyChangeScope.js";
8
8
  import { resolveParagraphChangeAttrs } from "../prosemirror/commands/resolveParagraphProperties.js";
9
9
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../prosemirror/containerFinalParagraph.js";
@@ -30,7 +30,7 @@ import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames
30
30
  import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "../prosemirror/runFormattingReconciliation.js";
31
31
  import { paragraphRunStyleContext, paragraphRunStyleContextAt, resolveParagraphBodyRunFormatting } from "../prosemirror/runStyleFormatting.js";
32
32
  import { sectionPropertiesOf } from "../prosemirror/sectionCarrier.js";
33
- import { listAttrsFromNumbering, listLevelAttrPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
33
+ import { listAttrsFromNumbering, listLevelAttrPatch, listLevelIndentRemovalPatch } from "../prosemirror/styles/resolvedStyleAttrs.js";
34
34
  import { removeRowsWithoutCellsAfterBatch } from "../prosemirror/tableGridMutation.js";
35
35
  import { isZeroWidthAnchor } from "../prosemirror/zeroWidthAnchors.js";
36
36
  import { getFolioParaIdFromBlockId } from "../types/block-id.js";
@@ -187,7 +187,7 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
187
187
  const resolvedIndentationFromStyle = paragraphIndentationFromFormatting(resolvedFormattingFromStyle);
188
188
  const resolvedSpacingFromStyle = paragraphSpacingFromFormatting(resolvedFormattingFromStyle);
189
189
  const patch = {};
190
- let originalFormatting = attrs._originalFormatting === void 0 || attrs._originalFormatting === null ? void 0 : { ...attrs._originalFormatting };
190
+ let originalFormatting = withDirectParagraphIndentation(attrs._originalFormatting, currentDirectIndentation);
191
191
  let originalFormattingChanged = false;
192
192
  const nextStyleId = properties.styleId;
193
193
  const styleChanged = nextStyleId !== void 0 && (attrs.styleId ?? null) !== nextStyleId;
@@ -227,11 +227,18 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
227
227
  }
228
228
  const numPrFromStyle = styleChanged ? readParagraphNumberingAttr(patch["numPrFromStyle"]) : attrs.numPrFromStyle;
229
229
  if (properties.numbering !== void 0) if (properties.numbering === null) {
230
+ Object.assign(patch, listLevelIndentRemovalPatch(mergeParagraphAttrs(node, {
231
+ ...attrs,
232
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(resolvedFormattingFromStyle)
233
+ }), numbering));
230
234
  patch["numPr"] = removedNumberingAttr(numPrFromStyle);
231
235
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
232
236
  } else {
233
237
  const listReference = concreteListReference(properties.numbering);
234
- Object.assign(patch, listLevelAttrPatch(attrs, {
238
+ Object.assign(patch, listLevelAttrPatch(mergeParagraphAttrs(node, {
239
+ ...attrs,
240
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(resolvedFormattingFromStyle)
241
+ }), {
235
242
  numId: listReference.numId,
236
243
  ilvl: listReference.level
237
244
  }, numbering));
@@ -241,6 +248,10 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
241
248
  }
242
249
  }
243
250
  else if (properties.listLevel !== void 0) if (properties.listLevel === null) {
251
+ Object.assign(patch, listLevelIndentRemovalPatch(mergeParagraphAttrs(node, {
252
+ ...attrs,
253
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(resolvedFormattingFromStyle)
254
+ }), numbering));
244
255
  patch["numPr"] = removedNumberingAttr(numPrFromStyle);
245
256
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
246
257
  } else {
@@ -251,7 +262,10 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
251
262
  ilvl: properties.listLevel
252
263
  });
253
264
  Object.assign(patch, CLEARED_LIST_RENDERING_ATTRS);
254
- } else Object.assign(patch, listLevelAttrPatch(attrs, {
265
+ } else Object.assign(patch, listLevelAttrPatch(mergeParagraphAttrs(node, {
266
+ ...attrs,
267
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(resolvedFormattingFromStyle)
268
+ }), {
255
269
  numId,
256
270
  ilvl: properties.listLevel
257
271
  }, numbering));
@@ -283,6 +297,28 @@ const paragraphPropertiesPatch = ({ node, properties, resolvedFormattingFromStyl
283
297
  originalFormattingChanged = true;
284
298
  }
285
299
  if (originalFormattingChanged) patch["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
300
+ if (styleChanged || properties.numbering !== void 0 || properties.listLevel !== void 0 || properties.indentation !== void 0) {
301
+ const nextNumbering = patch["numPr"] === void 0 ? attrs.numPr : readParagraphNumberingAttr(patch["numPr"]);
302
+ if (nextNumbering?.kind === "reference") {
303
+ const direct = properties.indentation === void 0 ? currentDirectIndentation : properties.indentation ?? void 0;
304
+ Object.assign(patch, listLevelAttrPatch(mergeParagraphAttrs(node, {
305
+ numPr: nextNumbering,
306
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(resolvedFormattingFromStyle),
307
+ numPrFromStyle: numPrFromStyle ?? void 0,
308
+ listImplicitChildLevelAdvances: attrs.listImplicitChildLevelAdvances,
309
+ _originalFormatting: withDirectParagraphIndentation(originalFormatting, direct),
310
+ _resolvedFormatting: withDirectParagraphIndentation(styleChanged ? styleResolvedParagraphFormatting(resolvedFormattingFromStyle) : attrs._resolvedFormatting, resolvedIndentationFromStyle),
311
+ indentLeft: direct?.indentLeft ?? resolvedFormattingFromStyle?.indentLeft ?? void 0,
312
+ indentRight: direct?.indentRight ?? resolvedFormattingFromStyle?.indentRight ?? void 0,
313
+ indentFirstLine: direct?.indentFirstLine ?? resolvedFormattingFromStyle?.indentFirstLine ?? void 0,
314
+ hangingIndent: direct?.hangingIndent ?? resolvedFormattingFromStyle?.hangingIndent ?? false
315
+ }), {
316
+ numId: nextNumbering.numId,
317
+ ilvl: nextNumbering.ilvl ?? 0
318
+ }, numbering));
319
+ patch["numPr"] = nextNumbering;
320
+ }
321
+ }
286
322
  return Object.keys(patch).length > 0 ? patch : null;
287
323
  };
288
324
  const REVISION_MARK_NAMES = /* @__PURE__ */ new Set(["insertion", "deletion"]);
@@ -2009,10 +2045,17 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2009
2045
  const listLevel = operation.listLevel;
2010
2046
  if (formatsParagraph) {
2011
2047
  if (explicitNumbering === null) {
2048
+ Object.assign(attrs, listLevelIndentRemovalPatch(mergeParagraphAttrs(schema.node("paragraph"), {
2049
+ ...operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode),
2050
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle)
2051
+ }), numbering));
2012
2052
  attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
2013
2053
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
2014
2054
  } else if (explicitNumbering !== void 0) {
2015
- Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
2055
+ Object.assign(attrs, listLevelAttrPatch(mergeParagraphAttrs(schema.node("paragraph"), {
2056
+ ...operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode),
2057
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle)
2058
+ }), {
2016
2059
  numId: explicitNumbering.numId,
2017
2060
  ilvl: explicitNumbering.level
2018
2061
  }, numbering));
@@ -2021,6 +2064,10 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2021
2064
  attrs["numPrFromStyle"] = null;
2022
2065
  }
2023
2066
  } else if (listLevel === null) {
2067
+ Object.assign(attrs, listLevelIndentRemovalPatch(mergeParagraphAttrs(schema.node("paragraph"), {
2068
+ ...operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode),
2069
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle)
2070
+ }), numbering));
2024
2071
  attrs["numPr"] = removedNumberingAttr(readParagraphNumberingAttr(attrs["numPrFromStyle"]));
2025
2072
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
2026
2073
  } else if (typeof listLevel === "number") {
@@ -2031,7 +2078,10 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2031
2078
  ilvl: listLevel
2032
2079
  });
2033
2080
  Object.assign(attrs, CLEARED_LIST_RENDERING_ATTRS);
2034
- } else Object.assign(attrs, listLevelAttrPatch(operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode), {
2081
+ } else Object.assign(attrs, listLevelAttrPatch(mergeParagraphAttrs(schema.node("paragraph"), {
2082
+ ...operation.inheritFormatting === false ? {} : expectParagraphAttrs(item.blockNode),
2083
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle)
2084
+ }), {
2035
2085
  numId,
2036
2086
  ilvl: listLevel
2037
2087
  }, numbering));
@@ -2083,7 +2133,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2083
2133
  else originalFormatting.alignment = directAlignment;
2084
2134
  }
2085
2135
  if (operation.spacing !== void 0) originalFormatting = withDirectParagraphSpacing(originalFormatting, directSpacing);
2086
- if (operation.indentation !== void 0) originalFormatting = withDirectParagraphIndentation(originalFormatting, directIndentation);
2136
+ originalFormatting = withDirectParagraphIndentation(originalFormatting, directIndentation);
2087
2137
  if (operation.styleId !== void 0 || operation.spacing !== void 0) Object.assign(attrs, paragraphSpacingAttrPatch({
2088
2138
  direct: directSpacing,
2089
2139
  inherited: paragraphSpacingFromFormatting(formattingFromStyle)
@@ -2093,6 +2143,25 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2093
2143
  inherited: paragraphIndentationFromFormatting(formattingFromStyle)
2094
2144
  }));
2095
2145
  attrs["_originalFormatting"] = originalFormatting && Object.keys(originalFormatting).length > 0 ? originalFormatting : null;
2146
+ const finalNumbering = readParagraphNumberingAttr(attrs["numPr"]);
2147
+ if (finalNumbering?.kind === "reference") {
2148
+ Object.assign(attrs, listLevelAttrPatch(mergeParagraphAttrs(schema.node("paragraph"), {
2149
+ numPr: finalNumbering,
2150
+ _styleResolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle),
2151
+ numPrFromStyle: readParagraphNumberingAttr(attrs["numPrFromStyle"]) ?? void 0,
2152
+ listImplicitChildLevelAdvances: operation.inheritFormatting === false ? void 0 : expectParagraphAttrs(item.blockNode).listImplicitChildLevelAdvances,
2153
+ _originalFormatting: originalFormatting,
2154
+ _resolvedFormatting: styleResolvedParagraphFormatting(formattingFromStyle),
2155
+ indentLeft: directIndentation?.indentLeft ?? formattingFromStyle?.indentLeft ?? void 0,
2156
+ indentRight: directIndentation?.indentRight ?? formattingFromStyle?.indentRight ?? void 0,
2157
+ indentFirstLine: directIndentation?.indentFirstLine ?? formattingFromStyle?.indentFirstLine ?? void 0,
2158
+ hangingIndent: directIndentation?.hangingIndent ?? formattingFromStyle?.hangingIndent ?? false
2159
+ }), {
2160
+ numId: finalNumbering.numId,
2161
+ ilvl: finalNumbering.ilvl ?? 0
2162
+ }, numbering));
2163
+ attrs["numPr"] = finalNumbering;
2164
+ }
2096
2165
  }
2097
2166
  if (formatsParagraph && operation.styleId !== void 0 && formattingFromStyle?.numPr?.kind === "reference" && operation.numbering === void 0 && operation.listLevel === void 0) {
2098
2167
  const { numId, ilvl = 0 } = formattingFromStyle.numPr;
@@ -4,6 +4,8 @@ import { Slice } from "prosemirror-model";
4
4
  import { EditorState } from "prosemirror-state";
5
5
  import { Result } from "better-result";
6
6
  //#region src/controller/canonicalClipboard.d.ts
7
+ /** Imported comments and notes require copying their source story parts. */
8
+ declare const canonicalClipboardStoryPartRefusal: (paragraph: document_d_exports.Paragraph) => CanonicalSessionError | undefined;
7
9
  /** Clipboard slices are input payloads; their paragraph identities and private captures are not owners. */
8
10
  type PrepareCanonicalPasteOptions = {
9
11
  session: CanonicalSession;
@@ -20,4 +22,4 @@ type PrepareCanonicalPasteOptions = {
20
22
  };
21
23
  declare const prepareCanonicalPaste: ({ session, state, slice, plain, moveTarget, pasteTarget, sourceDocument, moveSource }: PrepareCanonicalPasteOptions) => Result<CanonicalCommit, CanonicalSessionError>;
22
24
  //#endregion
23
- export { prepareCanonicalPaste };
25
+ export { canonicalClipboardStoryPartRefusal, prepareCanonicalPaste };
@@ -3,6 +3,7 @@ import { copyParagraphIndentationProjection } from "../docx/paragraphPropertySou
3
3
  import { visitInlineContentSlots, visitParagraphRuns } from "../docx/paragraphTraversal.js";
4
4
  import { captureVerbatimXml } from "../docx/verbatimCapture.js";
5
5
  import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, OOXML_NAMESPACE_SCOPE, findAttributeByNamespaceUri, getLocalName, parseXml } from "../docx/xmlParser.js";
6
+ import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
6
7
  import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
7
8
  import { completeNumberingForDoc } from "../prosemirror/listInstanceReferences.js";
8
9
  import { marksToTextFormatting } from "../prosemirror/runFormattingFromMarks.js";
@@ -174,6 +175,22 @@ const importNumbering = ({ destination, source, paragraphs, mode, sourceKind, ex
174
175
  numIds: nums
175
176
  });
176
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
+ };
177
194
  const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarget, pasteTarget, sourceDocument, moveSource }) => {
178
195
  if (session.isComposing) return refuse("Composition must finish before using the clipboard.");
179
196
  if (slice.content.size === 0) return Result.err(new CanonicalSessionError({
@@ -212,9 +229,10 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
212
229
  paragraphs.push(paragraph);
213
230
  }
214
231
  if (moveTarget === void 0 && (plain || sourceDocument === void 0)) flattenClipboardStyleReferences(paragraphs);
215
- 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);
216
234
  if (from.isErr()) return from;
217
- const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ?? state.selection.to);
235
+ const to = session.projection.addressAt(pasteTarget ?? moveSource?.to ?? selectionRange.to);
218
236
  if (to.isErr()) return to;
219
237
  let styles = session.document.package.styles;
220
238
  let styleIds;
@@ -315,14 +333,8 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
315
333
  };
316
334
  for (const paragraph of paragraphs) {
317
335
  if (moveTarget === void 0) {
318
- let requiresStory = false;
319
- visitInlineContentSlots(paragraph, ({ item }) => {
320
- if (item.type === "commentRangeStart" || item.type === "commentRangeEnd" || item.type === "commentReference") requiresStory = true;
321
- });
322
- visitParagraphRuns(paragraph, (run) => {
323
- if (run.content.some((item) => item.type === "footnoteRef" || item.type === "endnoteRef")) requiresStory = true;
324
- });
325
- 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);
326
338
  const links = [];
327
339
  visitInlineContentSlots(paragraph, ({ item }) => {
328
340
  if (item.type === "hyperlink") links.push(item);
@@ -341,7 +353,7 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
341
353
  const runs = [];
342
354
  visitParagraphRuns(paragraph, (run) => runs.push(run));
343
355
  for (const item of runs) {
344
- 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());
345
357
  for (const child of item.content) {
346
358
  if (child.type !== "drawing") continue;
347
359
  if (child.rawXmlMode !== void 0) return refuse("Opaque drawings and embedded objects require their source package parts.");
@@ -479,4 +491,4 @@ const prepareCanonicalPaste = ({ session, state, slice, plain = false, moveTarge
479
491
  });
480
492
  };
481
493
  //#endregion
482
- export { prepareCanonicalPaste };
494
+ export { canonicalClipboardStoryPartRefusal, prepareCanonicalPaste };
@@ -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 = {
@@ -42,11 +51,11 @@ declare const createCanonicalComposition: (options: CompositionOptions) => {
42
51
  to: number;
43
52
  }) => boolean;
44
53
  start: (view: EditorView) => void;
45
- ended: (view: EditorView) => void;
54
+ ended: (view: EditorView, data: string | null) => void;
46
55
  forgetCompleted: () => void;
47
56
  readonly pendingFinal: boolean;
48
57
  authorizeLateFinal: (view: EditorView, text: string | null) => boolean;
49
- authorizeNative: (view: EditorView) => void;
58
+ authorizeNative: (view: EditorView, completion: CompositionCompletionEvidence) => void;
50
59
  flushed: (view: EditorView) => void;
51
60
  recover: (view: EditorView) => CompositionFinish;
52
61
  cancel: (view: EditorView) => CompositionFinish;
@@ -1,10 +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
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
+ };
8
50
  /** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
9
51
  const createCanonicalComposition = (options) => {
10
52
  let state = { type: "committed" };
@@ -25,7 +67,13 @@ const createCanonicalComposition = (options) => {
25
67
  const selectedReplacement = (baseline, proposed) => {
26
68
  const { from, to } = baseline.selection;
27
69
  const text = selectedText(baseline, proposed);
28
- 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;
29
77
  return {
30
78
  from,
31
79
  to,
@@ -87,7 +135,13 @@ const createCanonicalComposition = (options) => {
87
135
  const inserted = next.content.cut(from, newEnd);
88
136
  const text = inserted.textBetween(0, inserted.size, "", "");
89
137
  if (removed.textBetween(0, removed.size, "", "").length !== removed.size || text.length !== inserted.size) return null;
90
- if (!baseline.tr.insertText(text, start + from, start + oldEnd).doc.eq(proposed.doc)) return null;
138
+ if (!matchesNativeReplacement({
139
+ baseline,
140
+ proposed,
141
+ from: start + from,
142
+ to: start + oldEnd,
143
+ text
144
+ })) return null;
91
145
  return {
92
146
  from: start + from,
93
147
  to: start + oldEnd,
@@ -119,7 +173,7 @@ const createCanonicalComposition = (options) => {
119
173
  view.updateState(pending.baseline);
120
174
  }
121
175
  options.end();
122
- 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" };
123
177
  if (pending.type === "refused") return {
124
178
  type: "refused",
125
179
  text: pending.text
@@ -178,10 +232,13 @@ const createCanonicalComposition = (options) => {
178
232
  type: "provisional",
179
233
  baseline: view.state,
180
234
  view,
181
- phase: "active"
235
+ phase: {
236
+ type: "active",
237
+ completion: "provisional"
238
+ }
182
239
  };
183
240
  },
184
- ended: (view) => {
241
+ ended: (view, data) => {
185
242
  if (state.type === "committed") return;
186
243
  switch (state.type) {
187
244
  case "provisional":
@@ -189,7 +246,11 @@ const createCanonicalComposition = (options) => {
189
246
  type: "provisional",
190
247
  baseline: state.baseline,
191
248
  view: state.view,
192
- phase: "ended"
249
+ phase: {
250
+ type: "ended",
251
+ data,
252
+ completion: state.phase.completion
253
+ }
193
254
  };
194
255
  break;
195
256
  case "refused":
@@ -197,7 +258,11 @@ const createCanonicalComposition = (options) => {
197
258
  type: "refused",
198
259
  baseline: state.baseline,
199
260
  view: state.view,
200
- phase: "ended",
261
+ phase: {
262
+ type: "ended",
263
+ data,
264
+ completion: state.phase.completion
265
+ },
201
266
  text: state.text
202
267
  };
203
268
  break;
@@ -225,15 +290,23 @@ const createCanonicalComposition = (options) => {
225
290
  };
226
291
  return true;
227
292
  },
228
- authorizeNative: (view) => {
293
+ authorizeNative: (view, completion) => {
229
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
+ };
230
303
  },
231
304
  flushed: (view) => {
232
305
  const authorized = authorizedNativeState;
233
306
  queueMicrotask(() => {
234
307
  if (authorizedNativeState === authorized) authorizedNativeState = null;
235
308
  });
236
- if (state.type !== "committed" && state.phase === "ended") schedule(view);
309
+ if (state.type !== "committed" && state.phase.type === "ended") schedule(view);
237
310
  },
238
311
  recover: (view) => finish(view, true),
239
312
  cancel: (view) => finish(view, false),
@@ -36,7 +36,7 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
36
36
  handleDOMEvents: {
37
37
  keydown: (view: EditorView, event: KeyboardEvent) => boolean;
38
38
  compositionstart: (view: EditorView) => boolean;
39
- compositionend: (view: EditorView) => boolean;
39
+ compositionend: (view: EditorView, event: Event) => boolean;
40
40
  mousedown: (view: EditorView) => boolean;
41
41
  blur: (view: EditorView) => boolean;
42
42
  beforeinput: (view: EditorView, event: InputEvent) => boolean;
@@ -209,8 +209,8 @@ const createCanonicalInputBoundary = (options) => {
209
209
  composition.start(view);
210
210
  return false;
211
211
  },
212
- compositionend: (view) => {
213
- composition.ended(view);
212
+ compositionend: (view, event) => {
213
+ composition.ended(view, event instanceof CompositionEvent && typeof event.data === "string" ? event.data : null);
214
214
  return false;
215
215
  },
216
216
  mousedown: (view) => {
@@ -230,7 +230,7 @@ const createCanonicalInputBoundary = (options) => {
230
230
  beginGesture(event.inputType === "insertFromComposition" ? "compositionFinal" : "new");
231
231
  if (event.isComposing || CANONICAL_COMPOSITION_INPUT_TYPES.some((inputType) => inputType === event.inputType)) {
232
232
  if (composition.active) {
233
- composition.authorizeNative(view);
233
+ composition.authorizeNative(view, event.inputType === "insertFromComposition" ? "explicitFinal" : "provisional");
234
234
  return false;
235
235
  }
236
236
  if (event.inputType === "insertFromComposition" && composition.authorizeLateFinal(view, event.data)) return false;
@@ -248,6 +248,7 @@ const createCanonicalInputBoundary = (options) => {
248
248
  }
249
249
  const nativeFinal = view.composing && (event.inputType === "insertText" || event.inputType === "insertReplacementText");
250
250
  const nativeStillComposing = view.composing;
251
+ if (nativeFinal) composition.authorizeNative(view, "explicitFinal");
251
252
  const recovered = composition.recover(view);
252
253
  composition.forgetCompleted();
253
254
  proposal = { type: "idle" };
@@ -1,9 +1,18 @@
1
1
  import { panic } from "better-result";
2
- import { AllSelection, TextSelection } from "prosemirror-state";
2
+ import { AllSelection, NodeSelection, TextSelection } from "prosemirror-state";
3
3
  //#region src/controller/canonicalSelection.ts
4
4
  /** Restore the admitted selection kind after a canonical projection changes. */
5
5
  const restoreCanonicalSelection = ({ transaction, projection, selection, unavailable }) => {
6
6
  switch (selection.type) {
7
+ case "inlineNode": {
8
+ const anchor = projection.positionAt(selection.anchor);
9
+ const head = projection.positionAt(selection.head);
10
+ if (anchor.isErr() || head.isErr()) return false;
11
+ const node = transaction.doc.nodeAt(anchor.value);
12
+ if (!node?.isInline || !node.isAtom || !NodeSelection.isSelectable(node) || anchor.value + node.nodeSize !== head.value) return false;
13
+ transaction.setSelection(NodeSelection.create(transaction.doc, anchor.value));
14
+ return true;
15
+ }
7
16
  case "all":
8
17
  transaction.setSelection(new AllSelection(transaction.doc));
9
18
  return true;
@@ -14,7 +14,7 @@ declare class CanonicalSessionError extends CanonicalSessionError_base<{
14
14
  reason: "refused" | "noChange";
15
15
  }> {}
16
16
  type CanonicalSelection = {
17
- type: "text" | "all";
17
+ type: "text" | "inlineNode" | "all";
18
18
  anchor: TextPosition;
19
19
  head: TextPosition;
20
20
  };