@stll/folio-core 0.54.2 → 0.55.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 (199) hide show
  1. package/dist/ai-edits/apply.js +22 -1
  2. package/dist/ai-edits/headless.js +1 -0
  3. package/dist/compare/content-types.d.ts +3 -15
  4. package/dist/controller/canonicalComposition.d.ts +36 -0
  5. package/dist/controller/canonicalComposition.js +168 -0
  6. package/dist/controller/canonicalInput.d.ts +45 -0
  7. package/dist/controller/canonicalInput.js +338 -0
  8. package/dist/controller/canonicalOperations.d.ts +22 -0
  9. package/dist/controller/canonicalOperations.js +85 -0
  10. package/dist/controller/canonicalReview.d.ts +12 -0
  11. package/dist/controller/canonicalReview.js +19 -0
  12. package/dist/controller/canonicalSession.d.ts +149 -0
  13. package/dist/controller/canonicalSession.js +1038 -0
  14. package/dist/controller/canonicalStoryEditor.d.ts +34 -0
  15. package/dist/controller/canonicalStoryEditor.js +82 -0
  16. package/dist/controller/canonicalStructure.d.ts +16 -0
  17. package/dist/controller/canonicalStructure.js +269 -0
  18. package/dist/controller/folioEditor.d.ts +1 -1
  19. package/dist/controller/folioEditor.js +9 -0
  20. package/dist/controller/headerFooterEditorManager.d.ts +4 -0
  21. package/dist/controller/headerFooterEditorManager.js +71 -3
  22. package/dist/controller/hiddenEditorApi.d.ts +35 -0
  23. package/dist/controller/hiddenEditorApi.js +25 -8
  24. package/dist/controller/hiddenEditorManager.d.ts +11 -2
  25. package/dist/controller/hiddenEditorManager.js +456 -30
  26. package/dist/controller/layoutPipeline.d.ts +1 -1
  27. package/dist/controller/layoutPipeline.js +2 -0
  28. package/dist/controller/noteEditorManager.d.ts +4 -0
  29. package/dist/controller/noteEditorManager.js +113 -15
  30. package/dist/docx/attributeRemainder.js +1 -0
  31. package/dist/docx/canonicalSessionInput.d.ts +11 -0
  32. package/dist/docx/canonicalSessionInput.js +27 -0
  33. package/dist/docx/canonicalStoryRepack.d.ts +10 -0
  34. package/dist/docx/canonicalStoryRepack.js +41 -0
  35. package/dist/docx/contentTypeRegistry.d.ts +8 -0
  36. package/dist/docx/contentTypeRegistry.js +64 -0
  37. package/dist/docx/ensureParaIds.d.ts +7 -1
  38. package/dist/docx/ensureParaIds.js +163 -211
  39. package/dist/docx/foldedListNumberFields.d.ts +85 -0
  40. package/dist/docx/foldedListNumberFields.js +267 -0
  41. package/dist/docx/headerFooterVerbatim.d.ts +14 -1
  42. package/dist/docx/headerFooterVerbatim.js +95 -2
  43. package/dist/docx/noteIds.d.ts +5 -0
  44. package/dist/docx/noteIds.js +6 -2
  45. package/dist/docx/noteReferenceMark.d.ts +3 -1
  46. package/dist/docx/noteReferenceMark.js +15 -14
  47. package/dist/docx/numberingParser.d.ts +3 -1
  48. package/dist/docx/numberingParser.js +17 -3
  49. package/dist/docx/packageParts.d.ts +3 -1
  50. package/dist/docx/packageParts.js +65 -16
  51. package/dist/docx/paragraphMarkPropertyPresence.d.ts +9 -0
  52. package/dist/docx/paragraphMarkPropertyPresence.js +21 -0
  53. package/dist/docx/paragraphParser.js +101 -53
  54. package/dist/docx/paragraphPropertySource.d.ts +15 -1
  55. package/dist/docx/paragraphPropertySource.js +76 -22
  56. package/dist/docx/parser.js +2 -1
  57. package/dist/docx/reviewHistoryNamespace.d.ts +4 -0
  58. package/dist/docx/reviewHistoryNamespace.js +4 -0
  59. package/dist/docx/reviewResolutionProvenance.d.ts +25 -0
  60. package/dist/docx/reviewResolutionProvenance.js +153 -0
  61. package/dist/docx/rezip.d.ts +7 -3
  62. package/dist/docx/rezip.js +107 -45
  63. package/dist/docx/runConsolidator.d.ts +2 -58
  64. package/dist/docx/runConsolidator.js +3 -175
  65. package/dist/docx/runParser.d.ts +2 -1
  66. package/dist/docx/runParser.js +156 -116
  67. package/dist/docx/saveDiagnostics.d.ts +11 -0
  68. package/dist/docx/saveDiagnostics.js +0 -0
  69. package/dist/docx/selectiveSave.d.ts +2 -1
  70. package/dist/docx/selectiveSave.js +4 -1
  71. package/dist/docx/selectiveXmlPatch.d.ts +16 -8
  72. package/dist/docx/selectiveXmlPatch.js +74 -2
  73. package/dist/docx/serializer/headerFooterSerializer.d.ts +5 -1
  74. package/dist/docx/serializer/headerFooterSerializer.js +19 -11
  75. package/dist/docx/serializer/noteSerializer.d.ts +2 -2
  76. package/dist/docx/serializer/noteSerializer.js +24 -8
  77. package/dist/docx/serializer/paragraphSerializer.js +41 -4
  78. package/dist/docx/serializer/partNamespaces.js +2 -1
  79. package/dist/docx/serializer/runSerializer.js +3 -1
  80. package/dist/docx/settingsHeaderFooterUpdate.d.ts +5 -0
  81. package/dist/docx/settingsHeaderFooterUpdate.js +90 -0
  82. package/dist/docx/settingsParser.d.ts +1 -2
  83. package/dist/docx/settingsParser.js +5 -6
  84. package/dist/docx/storyBlockReplay.d.ts +11 -0
  85. package/dist/docx/storyBlockReplay.js +367 -0
  86. package/dist/docx/streamingXmlParser.d.ts +22 -1
  87. package/dist/docx/streamingXmlParser.js +49 -3
  88. package/dist/docx/structuralXmlPatch.js +8 -1
  89. package/dist/docx/verbatimCapture.d.ts +3 -1
  90. package/dist/docx/verbatimCapture.js +49 -6
  91. package/dist/i18n/messages/ar.gen.d.ts +5 -0
  92. package/dist/i18n/messages/ar.gen.js +656 -0
  93. package/dist/i18n/messages/cs.gen.d.ts +5 -0
  94. package/dist/i18n/messages/cs.gen.js +656 -0
  95. package/dist/i18n/messages/de.gen.d.ts +5 -0
  96. package/dist/i18n/messages/de.gen.js +656 -0
  97. package/dist/i18n/messages/en.gen.d.ts +5 -0
  98. package/dist/i18n/messages/en.gen.js +656 -0
  99. package/dist/i18n/messages/es.gen.d.ts +5 -0
  100. package/dist/i18n/messages/es.gen.js +656 -0
  101. package/dist/i18n/messages/et.gen.d.ts +5 -0
  102. package/dist/i18n/messages/et.gen.js +656 -0
  103. package/dist/i18n/messages/fr.gen.d.ts +5 -0
  104. package/dist/i18n/messages/fr.gen.js +656 -0
  105. package/dist/i18n/messages/he.gen.d.ts +5 -0
  106. package/dist/i18n/messages/he.gen.js +656 -0
  107. package/dist/i18n/messages/hi.gen.d.ts +5 -0
  108. package/dist/i18n/messages/hi.gen.js +656 -0
  109. package/dist/i18n/messages/hu.gen.d.ts +5 -0
  110. package/dist/i18n/messages/hu.gen.js +656 -0
  111. package/dist/i18n/messages/locales.d.ts +5 -0
  112. package/dist/i18n/messages/locales.js +22 -0
  113. package/dist/i18n/messages/lt.gen.d.ts +5 -0
  114. package/dist/i18n/messages/lt.gen.js +656 -0
  115. package/dist/i18n/messages/lv.gen.d.ts +5 -0
  116. package/dist/i18n/messages/lv.gen.js +656 -0
  117. package/dist/i18n/messages/pl.gen.d.ts +5 -0
  118. package/dist/i18n/messages/pl.gen.js +656 -0
  119. package/dist/i18n/messages/pt-BR.gen.d.ts +5 -0
  120. package/dist/i18n/messages/pt-BR.gen.js +656 -0
  121. package/dist/i18n/messages/sk.gen.d.ts +5 -0
  122. package/dist/i18n/messages/sk.gen.js +656 -0
  123. package/dist/i18n/messages/tr.gen.d.ts +5 -0
  124. package/dist/i18n/messages/tr.gen.js +656 -0
  125. package/dist/i18n/messages/zh-CN.gen.d.ts +5 -0
  126. package/dist/i18n/messages/zh-CN.gen.js +656 -0
  127. package/dist/i18n/messages.d.ts +2 -3
  128. package/dist/i18n/messages.js +1 -19
  129. package/dist/internal/noteMarkerAttrs.d.ts +8 -0
  130. package/dist/internal/noteMarkerAttrs.js +16 -0
  131. package/dist/internal/pageBreakRunSourceDescendantIndex.js +1 -0
  132. package/dist/internal/prosemirrorAttrBoundary.d.ts +21 -0
  133. package/dist/internal/prosemirrorAttrBoundary.js +50 -0
  134. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  135. package/dist/layout-bridge/convert/footnoteLayout.js +3 -3
  136. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  137. package/dist/layout-bridge/convert/headerFooterLayout.js +1 -0
  138. package/dist/layout-bridge/convert/noteReferences.d.ts +1 -3
  139. package/dist/layout-bridge/convert/noteReferences.js +1 -6
  140. package/dist/layout-bridge/convert/paragraphRuns.js +2 -4
  141. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -2
  142. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -2
  143. package/dist/managers/DocumentLoaderManager.d.ts +1 -0
  144. package/dist/managers/DocumentLoaderManager.js +8 -1
  145. package/dist/markdown/renderBlock.js +15 -1
  146. package/dist/markdown/renderParagraph.js +9 -13
  147. package/dist/markdown/renderRuns.js +3 -2
  148. package/dist/markdown/renderTable.js +2 -0
  149. package/dist/markdown/types.d.ts +3 -9
  150. package/dist/prosemirror/attrs/index.d.ts +2 -12
  151. package/dist/prosemirror/attrs/index.js +22 -48
  152. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +2 -1
  153. package/dist/prosemirror/canonicalCommands.d.ts +97 -0
  154. package/dist/prosemirror/canonicalCommands.js +28 -0
  155. package/dist/prosemirror/commands/pageBreak.js +15 -1
  156. package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
  157. package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
  158. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
  159. package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
  160. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -3
  161. package/dist/prosemirror/conversion/toProseDoc.js +51 -11
  162. package/dist/prosemirror/executeEditorCommand.d.ts +11 -0
  163. package/dist/prosemirror/executeEditorCommand.js +26 -0
  164. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  165. package/dist/prosemirror/extensions/core/ParagraphExtension.js +50 -19
  166. package/dist/prosemirror/extensions/features/ListExtension.js +18 -5
  167. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +5 -2
  168. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +13 -1
  169. package/dist/prosemirror/extensions/marks/TextColorExtension.js +3 -2
  170. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  171. package/dist/prosemirror/extensions/marks/markUtils.js +70 -8
  172. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.d.ts +6 -0
  173. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.js +36 -0
  174. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +4 -2
  175. package/dist/prosemirror/listMarker.d.ts +1 -0
  176. package/dist/prosemirror/listMarker.js +16 -7
  177. package/dist/prosemirror/listNumbering.js +26 -5
  178. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  179. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  180. package/dist/prosemirror/markupViewNotes.js +3 -2
  181. package/dist/prosemirror/moveRangeBoundaryAttrs.d.ts +2 -1
  182. package/dist/prosemirror/noteReferenceReview.js +5 -3
  183. package/dist/prosemirror/plugins/suggestionMode.js +50 -2
  184. package/dist/prosemirror/rangeAnchorAttrs.d.ts +2 -1
  185. package/dist/prosemirror/runFormattingFromMarks.d.ts +4 -1
  186. package/dist/prosemirror/runFormattingFromMarks.js +15 -1
  187. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +2 -1
  188. package/dist/prosemirror/runFormattingInlineCarriers.js +4 -1
  189. package/dist/prosemirror/schema/marks.d.ts +2 -0
  190. package/dist/prosemirror/schema/nodes.d.ts +10 -2
  191. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +2 -1
  192. package/dist/prosemirror/textInput.js +5 -1
  193. package/dist/prosemirror/trackedRevisionPath.js +2 -1
  194. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -0
  195. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -0
  196. package/dist/prosemirror/validation.js +4 -0
  197. package/dist/types/content.d.ts +2 -2
  198. package/dist/utils/createDocument.js +2 -2
  199. package/package.json +14 -2
@@ -1485,6 +1485,22 @@ const finalParagraphPredecessor = (doc, at) => {
1485
1485
  }
1486
1486
  return null;
1487
1487
  };
1488
+ /**
1489
+ * Whether a sibling between a final carrier's predecessor and the carrier
1490
+ * has its break deleted by this batch.
1491
+ */
1492
+ const crossesBatchDeletion = ({ doc, from, to, batchRevisionIds }) => {
1493
+ const $to = doc.resolve(to);
1494
+ let position = to;
1495
+ for (let index = $to.index() - 1; index >= 0 && position > from; index--) {
1496
+ const node = $to.parent.child(index);
1497
+ position -= node.nodeSize;
1498
+ const mark = node.attrs["pPrMark"];
1499
+ const revisionId = isPlainDeletedPPrMark(mark) ? addedOrDeletedBreakRevisionId(mark) : null;
1500
+ if (revisionId !== null && batchRevisionIds.has(revisionId)) return true;
1501
+ }
1502
+ return false;
1503
+ };
1488
1504
  /** Pending retirements, read after the merge has mapped any inserted separator. */
1489
1505
  const pendingRemovedFinalBreakPositions = (tr, deletions) => {
1490
1506
  const positions = /* @__PURE__ */ new Set();
@@ -1569,7 +1585,12 @@ const withRetiredFinalParagraphs = ({ tr, deletions, mergedPropertyTargets, appl
1569
1585
  date,
1570
1586
  ...revisionExtras
1571
1587
  },
1572
- keep: propertiesSetInBatch(emptied, batchRevisionIds)
1588
+ keep: crossesBatchDeletion({
1589
+ doc: tr.doc,
1590
+ from: position + previous.nodeSize,
1591
+ to: at,
1592
+ batchRevisionIds
1593
+ }) ? /* @__PURE__ */ new Set() : propertiesSetInBatch(emptied, batchRevisionIds)
1573
1594
  });
1574
1595
  resolvedOperationIds.add(operationId);
1575
1596
  addedRevisions.push({
@@ -1895,6 +1895,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1895
1895
  const source = story.type === "header" || story.type === "footer" ? this.getHeaderFooterStory(story) : this.getNoteStory(story);
1896
1896
  if (!source) return null;
1897
1897
  const conversionOptions = {
1898
+ ...this.baseDocument.package.numbering !== void 0 && { numbering: this.baseDocument.package.numbering },
1898
1899
  ...this.baseDocument.package.styles !== void 0 && { styles: this.baseDocument.package.styles },
1899
1900
  ...this.baseDocument.package.theme !== void 0 && { theme: this.baseDocument.package.theme }
1900
1901
  };
@@ -83,14 +83,9 @@ type FolioContentTableLocation = {
83
83
  */
84
84
  type FolioContentParagraphAlignment = document_d_exports.ParagraphAlignment;
85
85
  /** Line-height interpretation understood by the neutral comparison model. */
86
- type FolioContentLineSpacingRule = "auto" | "exact" | "atLeast";
86
+ type FolioContentLineSpacingRule = NonNullable<document_d_exports.ParagraphFormatting["lineSpacingRule"]>;
87
87
  /** The complete modeled attribute set of direct paragraph indentation. */
88
- type FolioContentParagraphIndentation = {
89
- indentLeft?: number;
90
- indentRight?: number;
91
- indentFirstLine?: number;
92
- hangingIndent?: boolean;
93
- };
88
+ type FolioContentParagraphIndentation = Pick<document_d_exports.ParagraphFormatting, "indentLeft" | "indentRight" | "indentFirstLine" | "hangingIndent">;
94
89
  /** A concrete numbering instance and its zero-based level. */
95
90
  type FolioContentListReference = {
96
91
  numId: number;
@@ -100,14 +95,7 @@ type FolioContentListReference = {
100
95
  * The complete modeled attribute set of direct paragraph spacing. Optional
101
96
  * fields distinguish an absent attribute from an explicit zero or false value.
102
97
  */
103
- type FolioContentParagraphSpacing = {
104
- spaceBefore?: number;
105
- spaceAfter?: number;
106
- lineSpacing?: number;
107
- lineSpacingRule?: FolioContentLineSpacingRule;
108
- beforeAutospacing?: boolean;
109
- afterAutospacing?: boolean;
110
- };
98
+ type FolioContentParagraphSpacing = Pick<document_d_exports.ParagraphFormatting, "spaceBefore" | "spaceAfter" | "lineSpacing" | "lineSpacingRule" | "beforeAutospacing" | "afterAutospacing">;
111
99
  /** A representation-neutral block in one ordered document story. */
112
100
  type FolioContentBlock<Kind extends string = string> = {
113
101
  id: string;
@@ -0,0 +1,36 @@
1
+ import { EditorState, Transaction } from "prosemirror-state";
2
+ import { EditorView } from "prosemirror-view";
3
+ //#region src/controller/canonicalComposition.d.ts
4
+ type CompositionOptions = {
5
+ begin: () => boolean;
6
+ end: () => void;
7
+ replace: (input: {
8
+ from: number;
9
+ to: number;
10
+ text: string;
11
+ semantic: "composition";
12
+ }) => void;
13
+ refuse: (reason: string) => void;
14
+ };
15
+ type CompositionState = {
16
+ type: "committed";
17
+ } | {
18
+ type: "provisional" | "refused";
19
+ baseline: EditorState;
20
+ phase: "active" | "ended";
21
+ };
22
+ /** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
23
+ declare const createCanonicalComposition: (options: CompositionOptions) => {
24
+ readonly status: CompositionState["type"];
25
+ readonly active: boolean;
26
+ start: (view: EditorView) => void;
27
+ ended: (view: EditorView) => void;
28
+ authorizeNative: (view: EditorView) => void;
29
+ flushed: (view: EditorView) => void;
30
+ recover: (view: EditorView) => void;
31
+ cancel: (view: EditorView) => void;
32
+ reset: () => void;
33
+ accept: (view: EditorView, transaction: Transaction) => boolean;
34
+ };
35
+ //#endregion
36
+ export { createCanonicalComposition };
@@ -0,0 +1,168 @@
1
+ import { splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
2
+ import { CanonicalSessionError } from "./canonicalSession.js";
3
+ import { Result } from "better-result";
4
+ //#region src/controller/canonicalComposition.ts
5
+ const COMPOSITION_FLUSH_DELAY_MS = 25;
6
+ /** Native IME owns the provisional view; the captured canonical projection stays unchanged. */
7
+ const createCanonicalComposition = (options) => {
8
+ let state = { type: "committed" };
9
+ let authorizedNativeState = null;
10
+ let timer;
11
+ const clearTimer = () => {
12
+ if (timer !== void 0) clearTimeout(timer);
13
+ timer = void 0;
14
+ };
15
+ const replacement = (baseline, proposed) => {
16
+ const { $from, $to } = baseline.selection;
17
+ if (!$from.parent.isTextblock || !$to.parent.isTextblock) return null;
18
+ if (!$from.sameParent($to)) {
19
+ const from = baseline.selection.from;
20
+ const to = baseline.selection.to;
21
+ const insertedSize = proposed.doc.content.size - (baseline.doc.content.size - (to - from));
22
+ if (insertedSize < 0 || from + insertedSize > proposed.doc.content.size) return null;
23
+ const text = proposed.doc.textBetween(from, from + insertedSize, "", "");
24
+ if (text.length !== insertedSize) return null;
25
+ if (!baseline.tr.insertText(text, from, to).doc.eq(proposed.doc)) return null;
26
+ return {
27
+ from,
28
+ to,
29
+ text,
30
+ semantic: "composition"
31
+ };
32
+ }
33
+ const start = $from.start();
34
+ const next = proposed.doc.nodeAt(start - 1);
35
+ if (!next || next.type !== $from.parent.type || next.content.size !== next.textContent.length) return null;
36
+ const before = $from.parent.textContent;
37
+ const after = next.textContent;
38
+ let from = 0;
39
+ while (from < before.length && from < after.length && before[from] === after[from]) from++;
40
+ while (splitsSurrogatePair(before, from) || splitsSurrogatePair(after, from)) from--;
41
+ let oldEnd = before.length;
42
+ let newEnd = after.length;
43
+ while (oldEnd > from && newEnd > from && before[oldEnd - 1] === after[newEnd - 1]) {
44
+ oldEnd--;
45
+ newEnd--;
46
+ }
47
+ while (splitsSurrogatePair(before, oldEnd) || splitsSurrogatePair(after, newEnd)) {
48
+ oldEnd++;
49
+ newEnd++;
50
+ }
51
+ const text = after.slice(from, newEnd);
52
+ if (!baseline.tr.insertText(text, start + from, start + oldEnd).doc.eq(proposed.doc)) return null;
53
+ return {
54
+ from: start + from,
55
+ to: start + oldEnd,
56
+ text,
57
+ semantic: "composition"
58
+ };
59
+ };
60
+ const finish = (view, commit) => {
61
+ clearTimer();
62
+ authorizedNativeState = null;
63
+ const pending = state;
64
+ if (pending.type === "committed") return;
65
+ const input = commit && pending.type === "provisional" ? replacement(pending.baseline, view.state) : null;
66
+ state = { type: "committed" };
67
+ if (!view.isDestroyed) view.updateState(pending.baseline);
68
+ options.end();
69
+ if (!commit || view.isDestroyed || pending.type === "refused") return;
70
+ if (input?.from === input?.to && input?.text === "") return;
71
+ if (!input) {
72
+ options.refuse("Composition changed unsupported content; the canonical document was restored.");
73
+ return;
74
+ }
75
+ options.replace(input);
76
+ };
77
+ const schedule = (view) => {
78
+ clearTimer();
79
+ timer = setTimeout(() => finish(view, true), COMPOSITION_FLUSH_DELAY_MS);
80
+ };
81
+ return {
82
+ get status() {
83
+ return state.type;
84
+ },
85
+ get active() {
86
+ return state.type !== "committed";
87
+ },
88
+ start: (view) => {
89
+ if (state.type !== "committed") return;
90
+ if (!options.begin()) return;
91
+ state = {
92
+ type: "provisional",
93
+ baseline: view.state,
94
+ phase: "active"
95
+ };
96
+ },
97
+ ended: (view) => {
98
+ if (state.type === "committed") return;
99
+ state = {
100
+ type: state.type,
101
+ baseline: state.baseline,
102
+ phase: "ended"
103
+ };
104
+ schedule(view);
105
+ },
106
+ authorizeNative: (view) => {
107
+ authorizedNativeState = view.state;
108
+ },
109
+ flushed: (view) => {
110
+ const authorized = authorizedNativeState;
111
+ queueMicrotask(() => {
112
+ if (authorizedNativeState === authorized) authorizedNativeState = null;
113
+ });
114
+ if (state.type !== "committed" && state.phase === "ended") schedule(view);
115
+ },
116
+ recover: (view) => finish(view, true),
117
+ cancel: (view) => finish(view, false),
118
+ reset: () => {
119
+ clearTimer();
120
+ authorizedNativeState = null;
121
+ if (state.type !== "committed") options.end();
122
+ state = { type: "committed" };
123
+ },
124
+ accept: (view, transaction) => {
125
+ if (state.type === "committed") return false;
126
+ if (state.type === "refused") {
127
+ view.updateState(state.baseline);
128
+ return true;
129
+ }
130
+ if (typeof transaction.getMeta("composition") !== "number" && authorizedNativeState !== view.state) {
131
+ options.refuse("Other edits must wait until composition finishes.");
132
+ return true;
133
+ }
134
+ authorizedNativeState = null;
135
+ const applied = Result.try({
136
+ try: () => view.state.applyTransaction(transaction),
137
+ catch: (cause) => new CanonicalSessionError({
138
+ reason: "refused",
139
+ message: `Composition projection failed: ${String(cause)}`
140
+ })
141
+ });
142
+ if (applied.isErr()) {
143
+ view.updateState(state.baseline);
144
+ state = {
145
+ type: "refused",
146
+ baseline: state.baseline,
147
+ phase: state.phase
148
+ };
149
+ options.refuse(applied.error.message);
150
+ return true;
151
+ }
152
+ if (!replacement(state.baseline, applied.value.state)) {
153
+ view.updateState(state.baseline);
154
+ state = {
155
+ type: "refused",
156
+ baseline: state.baseline,
157
+ phase: state.phase
158
+ };
159
+ options.refuse("Composition changed unsupported content; the canonical document was restored.");
160
+ return true;
161
+ }
162
+ view.updateState(applied.value.state);
163
+ return true;
164
+ }
165
+ };
166
+ };
167
+ //#endregion
168
+ export { createCanonicalComposition };
@@ -0,0 +1,45 @@
1
+ import { EditorState, Transaction } from "prosemirror-state";
2
+ import { EditorView } from "prosemirror-view";
3
+ //#region src/controller/canonicalInput.d.ts
4
+ type ReplaceTextInput = {
5
+ from: number;
6
+ to: number;
7
+ text: string;
8
+ semantic?: "typing" | "deleteBackward" | "deleteForward" | "composition";
9
+ };
10
+ type CanonicalInputOptions = {
11
+ replace: (input: ReplaceTextInput) => void;
12
+ structure?: (type: "split" | "joinBackward" | "joinForward" | "lineBreak" | "pageBreak" | "tab" | "indent" | "outdent") => void;
13
+ command?: (name: "toggleBold" | "toggleItalic" | "toggleUnderline") => void;
14
+ breakUndoGroup?: () => void;
15
+ beginComposition?: () => boolean;
16
+ endComposition?: () => void;
17
+ undo: () => boolean;
18
+ redo: () => boolean;
19
+ refuse: (reason: string) => void;
20
+ };
21
+ /** Native changes require a preceding, classified input event at the same state. */
22
+ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) => {
23
+ reset: () => void;
24
+ readonly isComposing: boolean;
25
+ acceptComposition: (view: EditorView, transaction: Transaction) => boolean;
26
+ handleTextInput: (view: EditorView, from: number, to: number, text: string) => boolean;
27
+ handleKeyDown: (view: EditorView, event: KeyboardEvent) => boolean;
28
+ handleDOMEvents: {
29
+ compositionstart: (view: EditorView) => boolean;
30
+ compositionend: (view: EditorView) => boolean;
31
+ mousedown: (view: EditorView) => boolean;
32
+ blur: (view: EditorView) => boolean;
33
+ beforeinput: (view: EditorView, event: InputEvent) => boolean;
34
+ input: (view: EditorView) => boolean;
35
+ paste: (_view: EditorView, event: Event) => boolean;
36
+ cut: (_view: EditorView, event: Event) => boolean;
37
+ drop: (_view: EditorView, event: Event) => boolean;
38
+ };
39
+ /** Returns a classified intent only; the proposed PM document is never authoritative. */
40
+ takeNativeProposal: (state: EditorState, transaction: Transaction) => ReplaceTextInput | null;
41
+ refuseNativeMutation: (view: EditorView) => void;
42
+ commitNativeProposal: (view: EditorView, transaction: Transaction) => boolean;
43
+ };
44
+ //#endregion
45
+ export { createCanonicalInputBoundary };
@@ -0,0 +1,338 @@
1
+ import { handleEditorBeforeInput } from "../prosemirror/textInput.js";
2
+ import { createCanonicalComposition } from "./canonicalComposition.js";
3
+ import { deletionRange, isCanonicalJoinBoundary } from "./canonicalSession.js";
4
+ import { ReplaceStep } from "prosemirror-transform";
5
+ import { Selection, TextSelection } from "prosemirror-state";
6
+ //#region src/controller/canonicalInput.ts
7
+ /** Native changes require a preceding, classified input event at the same state. */
8
+ const createCanonicalInputBoundary = (options) => {
9
+ const composition = createCanonicalComposition({
10
+ begin: () => options.beginComposition?.() ?? true,
11
+ end: () => options.endComposition?.(),
12
+ replace: options.replace,
13
+ refuse: options.refuse
14
+ });
15
+ let proposal = { type: "idle" };
16
+ let authorizedInput = null;
17
+ let groupClosed = false;
18
+ const beginGesture = () => {
19
+ proposal = { type: "idle" };
20
+ authorizedInput = null;
21
+ groupClosed = false;
22
+ };
23
+ const closeGroup = () => {
24
+ if (groupClosed) return;
25
+ groupClosed = true;
26
+ options.breakUndoGroup?.();
27
+ };
28
+ const repaint = (view) => {
29
+ queueMicrotask(() => {
30
+ if (!view.isDestroyed) view.updateState(view.state);
31
+ });
32
+ };
33
+ const refuseEvent = (event, reason) => {
34
+ event.preventDefault();
35
+ closeGroup();
36
+ if (!composition.active && proposal.type !== "refused") options.refuse(reason);
37
+ if (!composition.active) proposal = { type: "refused" };
38
+ return true;
39
+ };
40
+ const refuseNativeMutation = (view) => {
41
+ if (!composition.active && proposal.type !== "refused") {
42
+ options.refuse("Unclassified native text is unavailable in this session.");
43
+ proposal = { type: "refused" };
44
+ }
45
+ repaint(view);
46
+ };
47
+ const takeNativeProposal = (state, transaction) => {
48
+ const pending = proposal;
49
+ if (pending.type !== "replacement") return null;
50
+ proposal = { type: "idle" };
51
+ if (pending.state !== state || transaction.steps.length !== 1) return null;
52
+ const step = transaction.steps.at(0);
53
+ if (!(step instanceof ReplaceStep) || "structure" in step && step.structure === true || step.from !== pending.input.from || step.to !== pending.input.to) return null;
54
+ if (step.slice.openStart !== 0 || step.slice.openEnd !== 0) return null;
55
+ let text = "";
56
+ let plain = true;
57
+ step.slice.content.forEach((node) => {
58
+ if (!node.isText) plain = false;
59
+ text += node.text ?? "";
60
+ });
61
+ if (!plain || text !== pending.input.text) return null;
62
+ return state.tr.insertText(text, step.from, step.to).doc.eq(transaction.doc) ? pending.input : null;
63
+ };
64
+ return {
65
+ reset: () => {
66
+ proposal = { type: "idle" };
67
+ authorizedInput = null;
68
+ composition.reset();
69
+ },
70
+ get isComposing() {
71
+ return composition.active;
72
+ },
73
+ acceptComposition: composition.accept,
74
+ handleTextInput: (view, from, to, text) => {
75
+ if (composition.active) return false;
76
+ const pending = proposal;
77
+ if (authorizedInput?.view === view && authorizedInput.state === view.state) {
78
+ options.replace({
79
+ from,
80
+ to,
81
+ text,
82
+ semantic: "typing"
83
+ });
84
+ return true;
85
+ }
86
+ if (pending.type === "replacement" && pending.state === view.state && pending.input.from === from && pending.input.to === to && pending.input.text === text) {
87
+ proposal = { type: "idle" };
88
+ options.replace(pending.input);
89
+ return true;
90
+ }
91
+ refuseNativeMutation(view);
92
+ return true;
93
+ },
94
+ handleKeyDown: (view, event) => {
95
+ beginGesture();
96
+ if (composition.active) {
97
+ if (event.key === "Escape") {
98
+ event.preventDefault();
99
+ composition.cancel(view);
100
+ return true;
101
+ }
102
+ if (event.metaKey || event.ctrlKey) {
103
+ event.preventDefault();
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ const modifier = event.metaKey || event.ctrlKey;
109
+ if (modifier || event.altKey || /^(?:Arrow|Home$|End$|Page|Escape$|Tab$)/u.test(event.key)) closeGroup();
110
+ if (modifier && event.key.toLowerCase() === "z") {
111
+ event.preventDefault();
112
+ if (event.shiftKey) options.redo();
113
+ else options.undo();
114
+ return true;
115
+ }
116
+ if (modifier && event.key.toLowerCase() === "y") {
117
+ event.preventDefault();
118
+ options.redo();
119
+ return true;
120
+ }
121
+ if (modifier && event.key.toLowerCase() === "a") {
122
+ event.preventDefault();
123
+ view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, Selection.atStart(view.state.doc).from, Selection.atEnd(view.state.doc).to)));
124
+ return true;
125
+ }
126
+ if (modifier && (event.key.toLowerCase() === "b" || event.key.toLowerCase() === "i" || event.key.toLowerCase() === "u") && options.command !== void 0) {
127
+ event.preventDefault();
128
+ switch (event.key.toLowerCase()) {
129
+ case "b":
130
+ options.command("toggleBold");
131
+ break;
132
+ case "i":
133
+ options.command("toggleItalic");
134
+ break;
135
+ case "u":
136
+ options.command("toggleUnderline");
137
+ break;
138
+ }
139
+ return true;
140
+ }
141
+ if (event.key === "Enter") {
142
+ if (view.composing) return refuseEvent(event, "Paragraph structure edits are unavailable during composition.");
143
+ event.preventDefault();
144
+ closeGroup();
145
+ if (options.structure === void 0) return refuseEvent(event, "Paragraph structure edits are unavailable in this session.");
146
+ if (modifier) options.structure("pageBreak");
147
+ else options.structure(event.shiftKey ? "lineBreak" : "split");
148
+ return true;
149
+ }
150
+ if (event.key === "Tab" && !modifier && !event.altKey && options.structure !== void 0) {
151
+ event.preventDefault();
152
+ if (view.state.selection.$from.parent.attrs["numPr"]?.kind === "reference") options.structure(event.shiftKey ? "outdent" : "indent");
153
+ else if (!event.shiftKey) options.structure("tab");
154
+ return true;
155
+ }
156
+ if (event.key !== "Backspace" && event.key !== "Delete") return false;
157
+ event.preventDefault();
158
+ if (modifier || event.altKey || view.composing || composition.active) {
159
+ closeGroup();
160
+ if (!composition.active) options.refuse("Only plain character deletion is available in this session.");
161
+ return true;
162
+ }
163
+ if (view.state.selection.empty && options.structure !== void 0 && isCanonicalJoinBoundary(view.state, event.key === "Backspace" ? "backward" : "forward")) {
164
+ closeGroup();
165
+ options.structure(event.key === "Backspace" ? "joinBackward" : "joinForward");
166
+ return true;
167
+ }
168
+ const range = deletionRange(view.state, event.key === "Backspace" ? "backward" : "forward");
169
+ if (range.isErr()) {
170
+ closeGroup();
171
+ options.refuse(range.error.message);
172
+ } else options.replace({
173
+ ...range.value,
174
+ text: "",
175
+ semantic: event.key === "Backspace" ? "deleteBackward" : "deleteForward"
176
+ });
177
+ return true;
178
+ },
179
+ handleDOMEvents: {
180
+ compositionstart: (view) => {
181
+ beginGesture();
182
+ composition.start(view);
183
+ return false;
184
+ },
185
+ compositionend: (view) => {
186
+ composition.ended(view);
187
+ return false;
188
+ },
189
+ mousedown: (view) => {
190
+ composition.recover(view);
191
+ beginGesture();
192
+ closeGroup();
193
+ return false;
194
+ },
195
+ blur: (view) => {
196
+ composition.recover(view);
197
+ beginGesture();
198
+ closeGroup();
199
+ repaint(view);
200
+ return false;
201
+ },
202
+ beforeinput: (view, event) => {
203
+ beginGesture();
204
+ if (event.isComposing || event.inputType === "insertCompositionText" || event.inputType === "insertFromComposition" || event.inputType === "deleteCompositionText" || event.inputType === "deleteByComposition") {
205
+ if (composition.active) {
206
+ composition.authorizeNative(view);
207
+ return false;
208
+ }
209
+ return refuseEvent(event, "Composition has no captured canonical baseline.");
210
+ }
211
+ if (composition.active) {
212
+ const refusedNativeCommit = composition.status === "refused" && view.composing;
213
+ composition.recover(view);
214
+ proposal = { type: "idle" };
215
+ if (refusedNativeCommit) {
216
+ event.preventDefault();
217
+ return true;
218
+ }
219
+ }
220
+ if (event.inputType === "insertText" || event.inputType === "insertReplacementText") {
221
+ if (event.cancelable) {
222
+ if (view.composing && event.inputType === "insertText" && event.data !== null) {
223
+ event.preventDefault();
224
+ proposal = { type: "idle" };
225
+ options.replace({
226
+ from: view.state.selection.from,
227
+ to: view.state.selection.to,
228
+ text: event.data,
229
+ semantic: "typing"
230
+ });
231
+ return true;
232
+ }
233
+ proposal = { type: "idle" };
234
+ authorizedInput = {
235
+ view,
236
+ state: view.state
237
+ };
238
+ try {
239
+ if (handleEditorBeforeInput(view, event)) return true;
240
+ return refuseEvent(event, "This native replacement cannot be addressed safely.");
241
+ } finally {
242
+ authorizedInput = null;
243
+ }
244
+ }
245
+ if (event.inputType !== "insertText" || event.data === null) return refuseEvent(event, "This native replacement cannot be addressed safely.");
246
+ proposal = {
247
+ type: "replacement",
248
+ state: view.state,
249
+ input: {
250
+ from: view.state.selection.from,
251
+ to: view.state.selection.to,
252
+ text: event.data,
253
+ semantic: "typing"
254
+ }
255
+ };
256
+ return false;
257
+ }
258
+ if ((event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") && event.cancelable && options.structure !== void 0) {
259
+ event.preventDefault();
260
+ closeGroup();
261
+ options.structure(event.inputType === "insertParagraph" ? "split" : "lineBreak");
262
+ return true;
263
+ }
264
+ if (event.inputType === "deleteContentBackward" || event.inputType === "deleteContentForward") {
265
+ const selection = view.state.selection;
266
+ const backward = event.inputType === "deleteContentBackward";
267
+ if (event.cancelable && selection.empty && options.structure !== void 0 && isCanonicalJoinBoundary(view.state, backward ? "backward" : "forward")) {
268
+ event.preventDefault();
269
+ closeGroup();
270
+ options.structure(backward ? "joinBackward" : "joinForward");
271
+ return true;
272
+ }
273
+ const range = deletionRange(view.state, event.inputType === "deleteContentBackward" ? "backward" : "forward");
274
+ if (range.isErr()) return refuseEvent(event, range.error.message);
275
+ const input = {
276
+ ...range.value,
277
+ text: "",
278
+ semantic: event.inputType === "deleteContentBackward" ? "deleteBackward" : "deleteForward"
279
+ };
280
+ if (event.cancelable) {
281
+ event.preventDefault();
282
+ options.replace(input);
283
+ return true;
284
+ }
285
+ proposal = {
286
+ type: "replacement",
287
+ state: view.state,
288
+ input
289
+ };
290
+ return false;
291
+ }
292
+ if (event.inputType === "historyUndo" || event.inputType === "historyRedo") {
293
+ event.preventDefault();
294
+ if (event.inputType === "historyUndo") options.undo();
295
+ else options.redo();
296
+ return true;
297
+ }
298
+ return refuseEvent(event, `Input ${event.inputType} is unavailable in this session.`);
299
+ },
300
+ input: (view) => {
301
+ if (composition.active) {
302
+ composition.flushed(view);
303
+ return false;
304
+ }
305
+ const pending = proposal;
306
+ queueMicrotask(() => {
307
+ if (view.isDestroyed) return;
308
+ view.updateState(view.state);
309
+ if (proposal === pending && !composition.active) proposal = { type: "idle" };
310
+ });
311
+ return false;
312
+ },
313
+ paste: (_view, event) => {
314
+ beginGesture();
315
+ return refuseEvent(event, "Paste is unavailable in this session.");
316
+ },
317
+ cut: (_view, event) => {
318
+ beginGesture();
319
+ return refuseEvent(event, "Cut is unavailable in this session.");
320
+ },
321
+ drop: (_view, event) => {
322
+ beginGesture();
323
+ return refuseEvent(event, "Drop is unavailable in this session.");
324
+ }
325
+ },
326
+ /** Returns a classified intent only; the proposed PM document is never authoritative. */
327
+ takeNativeProposal,
328
+ refuseNativeMutation,
329
+ commitNativeProposal: (view, transaction) => {
330
+ const input = takeNativeProposal(view.state, transaction);
331
+ if (!input) return false;
332
+ options.replace(input);
333
+ return true;
334
+ }
335
+ };
336
+ };
337
+ //#endregion
338
+ export { createCanonicalInputBoundary };
@@ -0,0 +1,22 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { CreateHeaderFooterOp, DOCUMENT_OP_TYPES, DocumentOp, FormattingPatch, FormattingPatch as FormattingPatch$1, OpStory, RemoveHeaderFooterOp, SetSectionPropsOp, TextPosition } from "@stll/docx-core/ops";
3
+ //#region src/controller/canonicalOperations.d.ts
4
+ declare const canonicalFinalSectionIndex: (document: document_d_exports.Document) => number;
5
+ declare const withCanonicalParagraphIds: (content: document_d_exports.BlockContent[], document: document_d_exports.Document) => document_d_exports.BlockContent[];
6
+ type CreateCanonicalHeaderFooterOptions = {
7
+ document: document_d_exports.Document;
8
+ position: "header" | "footer";
9
+ referenceType: document_d_exports.HeaderFooterType;
10
+ sectionIndex?: number;
11
+ };
12
+ declare const createCanonicalHeaderFooterOperation: ({ document, position, referenceType, sectionIndex }: CreateCanonicalHeaderFooterOptions) => CreateHeaderFooterOp;
13
+ type RemoveCanonicalHeaderFooterOptions = {
14
+ document: document_d_exports.Document;
15
+ position: "header" | "footer";
16
+ rId: string;
17
+ };
18
+ /** Remove each explicit binding, leaving inherited sections to inherit the remaining parts. */
19
+ declare const removeCanonicalHeaderFooterOperations: ({ document, position, rId }: RemoveCanonicalHeaderFooterOptions) => RemoveHeaderFooterOp[];
20
+ declare const createCanonicalSectionPropertiesOperation: (document: document_d_exports.Document, patch: FormattingPatch$1<document_d_exports.SectionProperties>) => SetSectionPropsOp;
21
+ //#endregion
22
+ export { DOCUMENT_OP_TYPES, type DocumentOp, type FormattingPatch, type OpStory, type TextPosition, canonicalFinalSectionIndex, createCanonicalHeaderFooterOperation, createCanonicalSectionPropertiesOperation, removeCanonicalHeaderFooterOperations, withCanonicalParagraphIds };