@stll/folio-core 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/ai-edits/apply.js +214 -52
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/headless.js +2 -2
  5. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  6. package/dist/ai-edits/types.d.ts +7 -1
  7. package/dist/ai-suggestions/apply.js +20 -1
  8. package/dist/compare/compare.js +1 -0
  9. package/dist/compare/inline-atoms.js +40 -16
  10. package/dist/compare/inline-provenance.js +12 -5
  11. package/dist/compare/reproducible-package.d.ts +0 -17
  12. package/dist/compare/reproducible-package.js +12 -18
  13. package/dist/compat/eigenpal.d.ts +2 -1
  14. package/dist/compat/eigenpal.js +2 -1
  15. package/dist/controller/canonicalClipboard.d.ts +23 -0
  16. package/dist/controller/canonicalClipboard.js +482 -0
  17. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  18. package/dist/controller/canonicalClipboardResources.js +382 -0
  19. package/dist/controller/canonicalComments.d.ts +41 -0
  20. package/dist/controller/canonicalComments.js +152 -0
  21. package/dist/controller/canonicalComposition.d.ts +24 -3
  22. package/dist/controller/canonicalComposition.js +208 -48
  23. package/dist/controller/canonicalInlineProjection.d.ts +118 -0
  24. package/dist/controller/canonicalInlineProjection.js +476 -0
  25. package/dist/controller/canonicalInput.d.ts +13 -4
  26. package/dist/controller/canonicalInput.js +62 -14
  27. package/dist/controller/canonicalInputTimer.d.ts +16 -0
  28. package/dist/controller/canonicalInputTimer.js +34 -0
  29. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  30. package/dist/controller/canonicalPublicOperations.js +648 -0
  31. package/dist/controller/canonicalSelection.d.ts +18 -0
  32. package/dist/controller/canonicalSelection.js +32 -0
  33. package/dist/controller/canonicalSession.d.ts +27 -6
  34. package/dist/controller/canonicalSession.js +298 -116
  35. package/dist/controller/canonicalStoryEditor.d.ts +7 -3
  36. package/dist/controller/canonicalStoryEditor.js +27 -2
  37. package/dist/controller/canonicalStructure.js +57 -4
  38. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  39. package/dist/controller/canonicalTocSelection.js +43 -0
  40. package/dist/controller/folioEditor.d.ts +2 -9
  41. package/dist/controller/folioEditor.js +18 -4
  42. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  43. package/dist/controller/headerFooterEditorManager.js +26 -11
  44. package/dist/controller/hiddenEditorApi.d.ts +24 -3
  45. package/dist/controller/hiddenEditorApi.js +8 -0
  46. package/dist/controller/hiddenEditorManager.d.ts +5 -1
  47. package/dist/controller/hiddenEditorManager.js +378 -43
  48. package/dist/controller/noteEditorManager.d.ts +2 -1
  49. package/dist/controller/noteEditorManager.js +29 -13
  50. package/dist/document-operations.d.ts +8 -1
  51. package/dist/document-operations.js +6 -4
  52. package/dist/docx/blockPlainText.d.ts +1 -1
  53. package/dist/docx/blockPlainText.js +11 -3
  54. package/dist/docx/builtInStyles.js +13 -6
  55. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  56. package/dist/docx/canonicalResourceSave.js +27 -0
  57. package/dist/docx/canonicalSave.d.ts +29 -0
  58. package/dist/docx/canonicalSave.js +101 -0
  59. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  60. package/dist/docx/canonicalStoryRepack.js +38 -5
  61. package/dist/docx/containerChildren.gen.d.ts +3 -1
  62. package/dist/docx/containerChildren.gen.js +33 -1
  63. package/dist/docx/documentParser.d.ts +1 -13
  64. package/dist/docx/documentParser.js +1 -47
  65. package/dist/docx/ensureParaIds.js +10 -4
  66. package/dist/docx/fieldParser.js +14 -2
  67. package/dist/docx/footnoteParser.d.ts +1 -11
  68. package/dist/docx/footnoteParser.js +1 -16
  69. package/dist/docx/headerFooterParser.d.ts +1 -9
  70. package/dist/docx/headerFooterParser.js +1 -12
  71. package/dist/docx/headerFooterVerbatim.d.ts +26 -1
  72. package/dist/docx/headerFooterVerbatim.js +81 -3
  73. package/dist/docx/hyperlinkParser.d.ts +1 -1
  74. package/dist/docx/hyperlinkParser.js +20 -1
  75. package/dist/docx/imageParser.d.ts +13 -1
  76. package/dist/docx/imageParser.js +20 -11
  77. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  78. package/dist/docx/inlineWrapperContent.js +4 -4
  79. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  80. package/dist/docx/linkedSdtPreservation.js +37 -0
  81. package/dist/docx/metadataPrivacy.js +6 -1
  82. package/dist/docx/packageParts.js +9 -3
  83. package/dist/docx/paragraphParser.js +88 -67
  84. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  85. package/dist/docx/paragraphPropertySource.js +109 -2
  86. package/dist/docx/parser.js +2 -1
  87. package/dist/docx/preservedRunContent.js +2 -1
  88. package/dist/docx/relsParser.js +26 -2
  89. package/dist/docx/removeHeaderFooterParts.js +17 -6
  90. package/dist/docx/rezip.d.ts +49 -5
  91. package/dist/docx/rezip.js +754 -235
  92. package/dist/docx/saveDiagnostics.d.ts +11 -0
  93. package/dist/docx/selectiveSave.d.ts +2 -1
  94. package/dist/docx/selectiveSave.js +42 -11
  95. package/dist/docx/selectiveXmlPatch.js +7 -5
  96. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  97. package/dist/docx/serializer/commentSerializer.js +7 -3
  98. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  99. package/dist/docx/serializer/documentSerializer.js +29 -10
  100. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  101. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  102. package/dist/docx/server/materializeYjsDocx.js +14 -6
  103. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  104. package/dist/docx/storyBlockReplay.d.ts +21 -1
  105. package/dist/docx/storyBlockReplay.js +113 -9
  106. package/dist/docx/storyPlainText.d.ts +20 -0
  107. package/dist/docx/storyPlainText.js +23 -0
  108. package/dist/docx/structuralXmlPatch.js +10 -22
  109. package/dist/docx/styleParser.d.ts +5 -1
  110. package/dist/docx/styleParser.js +1 -1
  111. package/dist/docx/tableParser.d.ts +1 -8
  112. package/dist/docx/tableParser.js +1 -28
  113. package/dist/docx/textBoxParser.d.ts +1 -5
  114. package/dist/docx/textBoxParser.js +1 -20
  115. package/dist/docx/unzip.js +6 -1
  116. package/dist/i18n/messages/ar.gen.js +4 -0
  117. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  118. package/dist/i18n/messages/catalogs.gen.js +68 -0
  119. package/dist/i18n/messages/cs.gen.js +4 -0
  120. package/dist/i18n/messages/de.gen.js +4 -0
  121. package/dist/i18n/messages/en.gen.js +4 -0
  122. package/dist/i18n/messages/es.gen.js +4 -0
  123. package/dist/i18n/messages/et.gen.js +4 -0
  124. package/dist/i18n/messages/fr.gen.js +4 -0
  125. package/dist/i18n/messages/he.gen.js +4 -0
  126. package/dist/i18n/messages/hi.gen.js +4 -0
  127. package/dist/i18n/messages/hu.gen.js +4 -0
  128. package/dist/i18n/messages/lt.gen.js +4 -0
  129. package/dist/i18n/messages/lv.gen.js +4 -0
  130. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  131. package/dist/i18n/messages/pl.gen.js +4 -0
  132. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  133. package/dist/i18n/messages/sk.gen.js +4 -0
  134. package/dist/i18n/messages/tr.gen.js +4 -0
  135. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +2 -1
  138. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  139. package/dist/internal/acceptedBlockProjection.js +45 -0
  140. package/dist/internal/revisionResolutionInline.js +25 -3
  141. package/dist/managers/DocumentLoaderManager.js +2 -1
  142. package/dist/markdown/escape.d.ts +3 -1
  143. package/dist/markdown/escape.js +9 -2
  144. package/dist/markdown/renderBlock.js +5 -120
  145. package/dist/markdown/renderParagraph.js +4 -2
  146. package/dist/markdown/renderRuns.d.ts +7 -1
  147. package/dist/markdown/renderRuns.js +33 -24
  148. package/dist/panel-layout.d.ts +11 -9
  149. package/dist/panel-layout.js +13 -9
  150. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  151. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  152. package/dist/prosemirror/attrs/index.js +5 -0
  153. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  154. package/dist/prosemirror/canonicalCommands.js +11 -4
  155. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  156. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  157. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  158. package/dist/prosemirror/clipboardIntent.js +9 -0
  159. package/dist/prosemirror/commands/comments.js +60 -35
  160. package/dist/prosemirror/commands/pageBreak.js +2 -2
  161. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  162. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  163. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  164. package/dist/prosemirror/commentOps.js +15 -1
  165. package/dist/prosemirror/conversion/fromProseDoc.js +22 -3
  166. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  167. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  168. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  169. package/dist/prosemirror/conversion/toProseDoc.js +124 -22
  170. package/dist/prosemirror/extensions/core/ParagraphExtension.js +61 -35
  171. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  172. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  173. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +65 -29
  174. package/dist/prosemirror/extensions/marks/markUtils.js +11 -8
  175. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  176. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  177. package/dist/prosemirror/fieldRepresentation.js +18 -0
  178. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  179. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  180. package/dist/prosemirror/insertOperations.js +2 -1
  181. package/dist/prosemirror/listNumbering.js +11 -5
  182. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  183. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  184. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  185. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  186. package/dist/prosemirror/noteReferenceReview.js +14 -1
  187. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  188. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  189. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  190. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  191. package/dist/prosemirror/plugins/suggestionMode.js +48 -48
  192. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  193. package/dist/prosemirror/preservedXmlReview.js +35 -0
  194. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  195. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  196. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  197. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  198. package/dist/prosemirror/runStyleFormatting.d.ts +3 -1
  199. package/dist/prosemirror/runStyleFormatting.js +3 -1
  200. package/dist/prosemirror/schema/marks.d.ts +1 -0
  201. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  202. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  203. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  204. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  205. package/dist/prosemirror/textInput.d.ts +5 -1
  206. package/dist/prosemirror/textInput.js +3 -3
  207. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  208. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  209. package/dist/prosemirror/validation.js +3 -5
  210. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  211. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  212. package/dist/types/canonicalCapabilities.d.ts +201 -0
  213. package/dist/types/canonicalCapabilities.js +198 -0
  214. package/dist/types/canonicalComments.d.ts +51 -0
  215. package/dist/types/canonicalComments.js +0 -0
  216. package/dist/types/canonicalSave.d.ts +11 -0
  217. package/dist/types/canonicalSave.js +0 -0
  218. package/dist/types/canonicalSections.d.ts +12 -0
  219. package/dist/types/canonicalSections.js +0 -0
  220. package/dist/types/docxSerialization.d.ts +14 -0
  221. package/dist/types/docxSerialization.js +7 -0
  222. package/dist/utils/headingCollector.js +45 -4
  223. package/dist/utils/outlineDepth.d.ts +9 -0
  224. package/dist/utils/outlineDepth.js +9 -0
  225. package/package.json +4 -4
@@ -0,0 +1,476 @@
1
+ import { readNoteMarkerAttrs } from "../internal/noteMarkerAttrs.js";
2
+ import { readCommentMarkAttrs, readFootnoteRefMarkAttrs, readPreservedXmlAttrs } from "../prosemirror/attrs/index.js";
3
+ import { bookmarkMarkerFromAttrs, readBookmarkBoundaryAttrs } from "../prosemirror/bookmarkBoundaryAttrs.js";
4
+ import { HYPHEN_TEXT_CARRIERS } from "../prosemirror/conversion/hyphenTextCarriers.js";
5
+ import { readMoveRangeBoundaryAttrs } from "../prosemirror/moveRangeBoundaryAttrs.js";
6
+ import { readRangeAnchorAttrs } from "../prosemirror/rangeAnchorAttrs.js";
7
+ import { PRESERVED_XML_LEVELS } from "../prosemirror/schema/nodes.js";
8
+ import { Result, TaggedError, panic } from "better-result";
9
+ import { hasIllegalXmlCharacters } from "@stll/docx-core";
10
+ import { inlineLeafSpans, paragraphLogicalText } from "@stll/docx-core/ops";
11
+ //#region src/controller/canonicalInlineProjection.ts
12
+ const CANONICAL_SOURCE_INLINE_POLICIES = {
13
+ run: "container",
14
+ hyperlink: "container",
15
+ insertion: "container",
16
+ deletion: "container",
17
+ moveFrom: "container",
18
+ moveTo: "container",
19
+ inlineWrapper: "container",
20
+ inlineSdt: "control",
21
+ simpleField: "leaf",
22
+ complexField: "leaf",
23
+ mathEquation: "leaf",
24
+ preservedInline: "leaf",
25
+ bookmarkStart: "leaf",
26
+ bookmarkEnd: "leaf",
27
+ commentRangeStart: "leaf",
28
+ commentRangeEnd: "leaf",
29
+ commentReference: "leaf",
30
+ moveFromRangeStart: "leaf",
31
+ moveFromRangeEnd: "leaf",
32
+ moveToRangeStart: "leaf",
33
+ moveToRangeEnd: "leaf",
34
+ text: "text",
35
+ tab: "leaf",
36
+ break: "leaf",
37
+ symbol: "leaf",
38
+ footnoteRef: "leaf",
39
+ endnoteRef: "leaf",
40
+ noteMarker: "leaf",
41
+ fieldChar: "rawField",
42
+ instrText: "rawField",
43
+ softHyphen: "leaf",
44
+ noBreakHyphen: "leaf",
45
+ renderedPageBreak: "layout",
46
+ preservedXml: "leaf",
47
+ drawing: "leaf",
48
+ shape: "leaf"
49
+ };
50
+ const CANONICAL_NATIVE_INLINE_POLICIES = {
51
+ text: "text",
52
+ bookmarkBoundary: "markers",
53
+ moveRangeBoundary: "markers",
54
+ rangeAnchor: "markers",
55
+ noteMarker: "noteMarker",
56
+ preservedXml: "capture",
57
+ field: "unit",
58
+ structuredField: "unit",
59
+ hardBreak: "unit",
60
+ image: "unit",
61
+ math: "unit",
62
+ pageBreakRun: "unit",
63
+ shape: "unit",
64
+ symbol: "unit",
65
+ tab: "unit",
66
+ textBoxAnchor: "unit",
67
+ commentReference: "unit",
68
+ renderedPageBreak: "refused",
69
+ sdt: "refused"
70
+ };
71
+ const nativePolicies = new Map(Object.entries(CANONICAL_NATIVE_INLINE_POLICIES));
72
+ var InlineProjectionError = class extends TaggedError("InlineProjectionError") {};
73
+ const refuse = (message) => Result.err(new InlineProjectionError({ message }));
74
+ /** Every source variant makes a decision before conversion can discard its positions. */
75
+ const canonicalInlineSourceIssue = (items, context = "paragraph") => {
76
+ for (const item of items) switch (CANONICAL_SOURCE_INLINE_POLICIES[item.type]) {
77
+ case "control":
78
+ if (context === "fieldPayload" && item.type === "inlineSdt") {
79
+ const issue = canonicalInlineSourceIssue(item.content, context);
80
+ if (issue !== null) return issue;
81
+ break;
82
+ }
83
+ return "Canonical editing cannot preserve inline content-control boundary positions.";
84
+ case "rawField":
85
+ if (context === "fieldPayload") break;
86
+ return "Canonical editing cannot preserve unstructured field instructions; a structured field is required.";
87
+ case "layout": return "Canonical editing cannot preserve cached rendered page-break positions.";
88
+ case "text":
89
+ if (item.type === "text" && (hasIllegalXmlCharacters(item.text) || /[\t\r\n]/u.test(item.text))) return "Canonical text requires valid XML characters and explicit tab or break elements.";
90
+ break;
91
+ case "container": {
92
+ if ((item.type === "insertion" || item.type === "deletion") && item.resolutionJoins !== void 0 && item.content.some((child) => child.type === "hyperlink")) return "Canonical editing cannot preserve hyperlink wrapper review provenance.";
93
+ const children = sourceContainerChildren(item);
94
+ const issue = canonicalInlineSourceIssue(children, context);
95
+ if (issue !== null) return issue;
96
+ break;
97
+ }
98
+ case "leaf": {
99
+ let children = [];
100
+ if (item.type === "simpleField") children = item.content;
101
+ if (item.type === "complexField") children = [...item.fieldCode, ...item.fieldResult];
102
+ const issue = canonicalInlineSourceIssue(children, "fieldPayload");
103
+ if (issue !== null) return issue;
104
+ break;
105
+ }
106
+ default:
107
+ }
108
+ return null;
109
+ };
110
+ const sourceContainerChildren = (item) => {
111
+ switch (item.type) {
112
+ case "hyperlink": return item.children;
113
+ case "run":
114
+ case "insertion":
115
+ case "deletion":
116
+ case "moveFrom":
117
+ case "moveTo":
118
+ case "inlineWrapper": return item.content;
119
+ default: return panic(`Source policy incorrectly classified ${item.type} as a container.`);
120
+ }
121
+ };
122
+ /** Native positions and deletion geometry read the same inline-cell classification. */
123
+ const canonicalNativeCells = (paragraph) => {
124
+ const cells = [];
125
+ let issue;
126
+ paragraph.forEach((node, from) => {
127
+ if (issue !== void 0) return;
128
+ const to = from + node.nodeSize;
129
+ switch (nativePolicies.get(node.type.name)) {
130
+ case "text": {
131
+ const reference = node.marks.find(({ type }) => type.name === "footnoteRef");
132
+ if (!reference) {
133
+ cells.push({
134
+ type: "text",
135
+ text: node.text ?? "",
136
+ from,
137
+ to,
138
+ node
139
+ });
140
+ return;
141
+ }
142
+ const attrs = readFootnoteRefMarkAttrs(reference);
143
+ if (!attrs.ok) {
144
+ issue = "Canonical editing requires valid note-reference mark attributes.";
145
+ return;
146
+ }
147
+ const label = String(attrs.value.id);
148
+ const text = node.text ?? "";
149
+ if (label.length === 0 || text.length % label.length !== 0 || text !== label.repeat(text.length / label.length)) {
150
+ issue = "Canonical editing cannot map the rendered note-reference labels.";
151
+ return;
152
+ }
153
+ for (let offset = 0; offset < text.length; offset += label.length) cells.push({
154
+ type: "markedUnit",
155
+ sourceType: attrs.value.noteType === "endnote" ? "endnoteRef" : "footnoteRef",
156
+ label,
157
+ from: from + offset,
158
+ to: from + offset + label.length,
159
+ node
160
+ });
161
+ return;
162
+ }
163
+ case "unit":
164
+ cells.push({
165
+ type: "unit",
166
+ from,
167
+ to,
168
+ node
169
+ });
170
+ return;
171
+ case "markers":
172
+ if (node.type.name === "bookmarkBoundary") {
173
+ const attrs = readBookmarkBoundaryAttrs(node);
174
+ if (!attrs.ok) {
175
+ issue = "Canonical editing requires valid bookmark boundary attributes.";
176
+ return;
177
+ }
178
+ cells.push({
179
+ type: "zeroWidth",
180
+ sources: [bookmarkMarkerFromAttrs(attrs.value)],
181
+ from,
182
+ to,
183
+ node
184
+ });
185
+ } else if (node.type.name === "moveRangeBoundary") {
186
+ const attrs = readMoveRangeBoundaryAttrs(node);
187
+ if (!attrs.ok) {
188
+ issue = "Canonical editing requires valid move-range boundary attributes.";
189
+ return;
190
+ }
191
+ cells.push({
192
+ type: "zeroWidth",
193
+ sources: [attrs.value],
194
+ from,
195
+ to,
196
+ node
197
+ });
198
+ } else {
199
+ const attrs = readRangeAnchorAttrs(node);
200
+ if (!attrs.ok) {
201
+ issue = "Canonical editing requires a valid collapsed range anchor.";
202
+ return;
203
+ }
204
+ cells.push({
205
+ type: "zeroWidth",
206
+ sources: [attrs.value.start, attrs.value.end],
207
+ from,
208
+ to,
209
+ node
210
+ });
211
+ }
212
+ return;
213
+ case "noteMarker": {
214
+ const attrs = readNoteMarkerAttrs(node);
215
+ if (!attrs.ok) {
216
+ issue = "Canonical editing requires a valid automatic note marker.";
217
+ return;
218
+ }
219
+ cells.push({
220
+ type: "zeroWidth",
221
+ sources: [{
222
+ type: "noteMarker",
223
+ kind: attrs.value.kind
224
+ }],
225
+ from,
226
+ to,
227
+ node
228
+ });
229
+ return;
230
+ }
231
+ case "capture": {
232
+ const attrs = readPreservedXmlAttrs(node);
233
+ if (!attrs.ok) {
234
+ issue = "Canonical editing requires valid preserved inline markup.";
235
+ return;
236
+ }
237
+ if (attrs.value.level === PRESERVED_XML_LEVELS.inline && attrs.value.text === "") cells.push({
238
+ type: "zeroWidth",
239
+ sources: [{
240
+ type: "preservedInline",
241
+ xml: attrs.value.xml,
242
+ text: ""
243
+ }],
244
+ from,
245
+ to,
246
+ node
247
+ });
248
+ else cells.push({
249
+ type: "unit",
250
+ from,
251
+ to,
252
+ node
253
+ });
254
+ return;
255
+ }
256
+ case "refused":
257
+ issue = `Canonical editing cannot preserve ${node.type.name} boundary positions.`;
258
+ return;
259
+ case void 0:
260
+ issue = `Canonical editing has no position mapping for inline ${node.type.name}.`;
261
+ return;
262
+ default:
263
+ }
264
+ });
265
+ return issue === void 0 ? Result.ok(cells) : refuse(issue);
266
+ };
267
+ const sameSourceMarker = (left, right) => {
268
+ switch (left.type) {
269
+ case "bookmarkStart":
270
+ case "bookmarkEnd":
271
+ case "commentRangeStart":
272
+ case "commentRangeEnd":
273
+ case "moveFromRangeStart":
274
+ case "moveFromRangeEnd":
275
+ case "moveToRangeStart":
276
+ case "moveToRangeEnd": return right.type === left.type && left.id === right.id;
277
+ case "noteMarker": return right.type === "noteMarker" && left.kind === right.kind;
278
+ case "preservedInline": return right.type === "preservedInline" && left.xml === right.xml && left.text === right.text;
279
+ default: return false;
280
+ }
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
+ };
328
+ /** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
329
+ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds, commentContext }) => {
330
+ const native = canonicalNativeCells(paragraph);
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;
338
+ const spans = inlineLeafSpans(source.content);
339
+ const erasedBookmarks = /* @__PURE__ */ new Set();
340
+ const collectErasedBookmarks = (items, context) => {
341
+ for (const item of items) {
342
+ if (item.type === "bookmarkStart" || item.type === "bookmarkEnd") {
343
+ if (context === "paragraph" && !pairedBookmarkIds.has(item.id)) erasedBookmarks.add(item);
344
+ continue;
345
+ }
346
+ if (CANONICAL_SOURCE_INLINE_POLICIES[item.type] !== "container") continue;
347
+ collectErasedBookmarks(sourceContainerChildren(item), item.type === "run" || item.type === "inlineWrapper" ? context : "nested");
348
+ }
349
+ };
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
+ }
373
+ const text = paragraphLogicalText(source);
374
+ const boundaries = Array.from({ length: text.length + 1 }, () => []);
375
+ let index = 0;
376
+ let consumed = 0;
377
+ const addGap = (position, gap) => {
378
+ const gaps = boundaries.at(gap.offset);
379
+ if (!gaps) panic(`Source leaf offset ${gap.offset} exceeds paragraph ${source.paraId}.`);
380
+ const existing = gaps.findIndex((entry) => entry.position === position);
381
+ const entry = {
382
+ position,
383
+ zeroWidthBefore: gap.zeroWidthBefore
384
+ };
385
+ if (existing < 0) gaps.push(entry);
386
+ else gaps[existing] = entry;
387
+ };
388
+ const consumeErasedLeaves = (position) => {
389
+ while (consumed === 0) {
390
+ const span = spans.at(index);
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;
400
+ index += 1;
401
+ if (position !== null) addGap(position, span.after);
402
+ }
403
+ };
404
+ addGap(0, {
405
+ offset: 0,
406
+ zeroWidthBefore: 0
407
+ });
408
+ for (const cell of native.value) {
409
+ consumeErasedLeaves(cell.from);
410
+ let span = spans.at(index);
411
+ if (!span) return refuse(`Canonical editing cannot map native ${cell.node.type.name} to a source leaf.`);
412
+ addGap(cell.from, consumed === 0 ? span.before : {
413
+ offset: span.before.offset + consumed,
414
+ zeroWidthBefore: 0
415
+ });
416
+ switch (cell.type) {
417
+ case "zeroWidth":
418
+ for (const marker of cell.sources) {
419
+ consumeErasedLeaves(null);
420
+ span = spans.at(index);
421
+ if (!span || consumed !== 0 || span.before.offset !== span.after.offset || !sameSourceMarker(span.node, marker)) return refuse(`Canonical editing cannot preserve ${marker.type} source boundary positions.`);
422
+ index += 1;
423
+ }
424
+ if (!span) return refuse("Canonical editing lost a source range boundary.");
425
+ addGap(cell.to, span.after);
426
+ break;
427
+ case "unit":
428
+ case "markedUnit":
429
+ if (cell.type === "markedUnit" && (span.node.type !== "footnoteRef" && span.node.type !== "endnoteRef" || span.node.type !== cell.sourceType || String(span.node.id) !== cell.label)) return refuse("Canonical editing cannot preserve note-reference identity.");
430
+ if (consumed !== 0 || span.node.type === "text" || span.after.offset - span.before.offset !== 1) return refuse(`Canonical editing cannot map ${span.node.type} to one native inline unit.`);
431
+ index += 1;
432
+ addGap(cell.to, span.after);
433
+ break;
434
+ case "text":
435
+ for (let unit = 0; unit < cell.text.length; unit += 1) {
436
+ consumeErasedLeaves(cell.from + unit);
437
+ span = spans.at(index);
438
+ if (!span) return refuse("Canonical editing cannot map missing source text to native text positions.");
439
+ let value;
440
+ switch (span.node.type) {
441
+ case "text":
442
+ value = span.node.text;
443
+ break;
444
+ case "softHyphen":
445
+ case "noBreakHyphen":
446
+ value = HYPHEN_TEXT_CARRIERS[span.node.type];
447
+ break;
448
+ default: return refuse(`Canonical editing cannot map ${span.node.type} to native text positions.`);
449
+ }
450
+ if (value.charAt(consumed) !== cell.text.charAt(unit)) return refuse("Canonical editing cannot preserve the source text carrier.");
451
+ consumed += 1;
452
+ addGap(cell.from + unit + 1, {
453
+ offset: span.before.offset + consumed,
454
+ zeroWidthBefore: 0
455
+ });
456
+ if (consumed === value.length) {
457
+ index += 1;
458
+ consumed = 0;
459
+ }
460
+ }
461
+ break;
462
+ default:
463
+ }
464
+ }
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.");
467
+ const missing = spans.at(index);
468
+ if (missing || consumed !== 0) return refuse(`Canonical editing cannot display the source positions of ${missing?.node.type ?? "incomplete text"}.`);
469
+ if (boundaries.some((gaps) => gaps.length === 0)) return refuse("Canonical editing lost a source character boundary.");
470
+ return Result.ok({
471
+ text,
472
+ boundaries
473
+ });
474
+ };
475
+ //#endregion
476
+ export { CANONICAL_NATIVE_INLINE_POLICIES, CANONICAL_SOURCE_INLINE_POLICIES, canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline };
@@ -1,12 +1,18 @@
1
1
  import { EditorState, Transaction } from "prosemirror-state";
2
2
  import { EditorView } from "prosemirror-view";
3
3
  //#region src/controller/canonicalInput.d.ts
4
+ declare const CANONICAL_COMPOSITION_INPUT_TYPES: readonly ["insertCompositionText", "insertFromComposition", "deleteCompositionText", "deleteByComposition"];
4
5
  type ReplaceTextInput = {
5
6
  from: number;
6
7
  to: number;
7
8
  text: string;
8
- semantic?: "typing" | "deleteBackward" | "deleteForward" | "composition";
9
- };
9
+ } & ({
10
+ semantic: "composition";
11
+ compositionPhase?: "correction";
12
+ } | {
13
+ semantic?: "typing" | "deleteBackward" | "deleteForward";
14
+ compositionPhase?: never;
15
+ });
10
16
  type CanonicalInputOptions = {
11
17
  replace: (input: ReplaceTextInput) => void;
12
18
  structure?: (type: "split" | "joinBackward" | "joinForward" | "lineBreak" | "pageBreak" | "tab" | "indent" | "outdent") => void;
@@ -14,6 +20,8 @@ type CanonicalInputOptions = {
14
20
  breakUndoGroup?: () => void;
15
21
  beginComposition?: () => boolean;
16
22
  endComposition?: () => void;
23
+ pastePlainText?: (view: EditorView) => void;
24
+ cut?: (view: EditorView, event: ClipboardEvent) => boolean;
17
25
  undo: () => boolean;
18
26
  redo: () => boolean;
19
27
  refuse: (reason: string) => void;
@@ -26,6 +34,7 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
26
34
  handleTextInput: (view: EditorView, from: number, to: number, text: string) => boolean;
27
35
  handleKeyDown: (view: EditorView, event: KeyboardEvent) => boolean;
28
36
  handleDOMEvents: {
37
+ keydown: (view: EditorView, event: KeyboardEvent) => boolean;
29
38
  compositionstart: (view: EditorView) => boolean;
30
39
  compositionend: (view: EditorView) => boolean;
31
40
  mousedown: (view: EditorView) => boolean;
@@ -33,7 +42,7 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
33
42
  beforeinput: (view: EditorView, event: InputEvent) => boolean;
34
43
  input: (view: EditorView) => boolean;
35
44
  paste: (_view: EditorView, event: Event) => boolean;
36
- cut: (_view: EditorView, event: Event) => boolean;
45
+ cut: (view: EditorView, event: ClipboardEvent) => boolean;
37
46
  drop: (_view: EditorView, event: Event) => boolean;
38
47
  };
39
48
  /** Returns a classified intent only; the proposed PM document is never authoritative. */
@@ -42,4 +51,4 @@ declare const createCanonicalInputBoundary: (options: CanonicalInputOptions) =>
42
51
  commitNativeProposal: (view: EditorView, transaction: Transaction) => boolean;
43
52
  };
44
53
  //#endregion
45
- export { createCanonicalInputBoundary };
54
+ export { CANONICAL_COMPOSITION_INPUT_TYPES, createCanonicalInputBoundary };
@@ -1,9 +1,15 @@
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";
5
5
  import { Selection, TextSelection } from "prosemirror-state";
6
6
  //#region src/controller/canonicalInput.ts
7
+ const CANONICAL_COMPOSITION_INPUT_TYPES = [
8
+ "insertCompositionText",
9
+ "insertFromComposition",
10
+ "deleteCompositionText",
11
+ "deleteByComposition"
12
+ ];
7
13
  /** Native changes require a preceding, classified input event at the same state. */
8
14
  const createCanonicalInputBoundary = (options) => {
9
15
  const composition = createCanonicalComposition({
@@ -15,7 +21,8 @@ const createCanonicalInputBoundary = (options) => {
15
21
  let proposal = { type: "idle" };
16
22
  let authorizedInput = null;
17
23
  let groupClosed = false;
18
- const beginGesture = () => {
24
+ const beginGesture = (lifecycle = "new") => {
25
+ if (lifecycle === "new") composition.forgetCompleted();
19
26
  proposal = { type: "idle" };
20
27
  authorizedInput = null;
21
28
  groupClosed = false;
@@ -37,6 +44,12 @@ const createCanonicalInputBoundary = (options) => {
37
44
  if (!composition.active) proposal = { type: "refused" };
38
45
  return true;
39
46
  };
47
+ const refuseClipboardGesture = (event, reason) => {
48
+ event.preventDefault();
49
+ closeGroup();
50
+ options.refuse(reason);
51
+ return true;
52
+ };
40
53
  const refuseNativeMutation = (view) => {
41
54
  if (!composition.active && proposal.type !== "refused") {
42
55
  options.refuse("Unclassified native text is unavailable in this session.");
@@ -72,7 +85,7 @@ const createCanonicalInputBoundary = (options) => {
72
85
  },
73
86
  acceptComposition: composition.accept,
74
87
  handleTextInput: (view, from, to, text) => {
75
- if (composition.active) return false;
88
+ if (composition.active || composition.pendingFinal) return false;
76
89
  const pending = proposal;
77
90
  if (authorizedInput?.view === view && authorizedInput.state === view.state) {
78
91
  options.replace({
@@ -92,6 +105,7 @@ const createCanonicalInputBoundary = (options) => {
92
105
  return true;
93
106
  },
94
107
  handleKeyDown: (view, event) => {
108
+ if (event.isComposing || event.keyCode === 229) return false;
95
109
  beginGesture();
96
110
  if (composition.active) {
97
111
  if (event.key === "Escape") {
@@ -138,6 +152,12 @@ const createCanonicalInputBoundary = (options) => {
138
152
  }
139
153
  return true;
140
154
  }
155
+ if (modifier && event.altKey && event.key.toLowerCase() === "v" && options.pastePlainText) {
156
+ event.preventDefault();
157
+ closeGroup();
158
+ options.pastePlainText(view);
159
+ return true;
160
+ }
141
161
  if (event.key === "Enter") {
142
162
  if (view.composing) return refuseEvent(event, "Paragraph structure edits are unavailable during composition.");
143
163
  event.preventDefault();
@@ -177,6 +197,13 @@ const createCanonicalInputBoundary = (options) => {
177
197
  return true;
178
198
  },
179
199
  handleDOMEvents: {
200
+ keydown: (view, event) => {
201
+ if (event.isComposing || event.keyCode === 229) return true;
202
+ if (!composition.active || event.key !== "Escape") return false;
203
+ event.preventDefault();
204
+ composition.cancel(view);
205
+ return true;
206
+ },
180
207
  compositionstart: (view) => {
181
208
  beginGesture();
182
209
  composition.start(view);
@@ -200,19 +227,31 @@ const createCanonicalInputBoundary = (options) => {
200
227
  return false;
201
228
  },
202
229
  beforeinput: (view, event) => {
203
- beginGesture();
204
- if (event.isComposing || event.inputType === "insertCompositionText" || event.inputType === "insertFromComposition" || event.inputType === "deleteCompositionText" || event.inputType === "deleteByComposition") {
230
+ beginGesture(event.inputType === "insertFromComposition" ? "compositionFinal" : "new");
231
+ if (event.isComposing || CANONICAL_COMPOSITION_INPUT_TYPES.some((inputType) => inputType === event.inputType)) {
205
232
  if (composition.active) {
206
233
  composition.authorizeNative(view);
207
234
  return false;
208
235
  }
236
+ if (event.inputType === "insertFromComposition" && composition.authorizeLateFinal(view, event.data)) return false;
209
237
  return refuseEvent(event, "Composition has no captured canonical baseline.");
210
238
  }
211
239
  if (composition.active) {
212
- const refusedNativeCommit = composition.status === "refused" && view.composing;
213
- composition.recover(view);
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
+ }
249
+ const nativeFinal = view.composing && (event.inputType === "insertText" || event.inputType === "insertReplacementText");
250
+ const nativeStillComposing = view.composing;
251
+ const recovered = composition.recover(view);
252
+ composition.forgetCompleted();
214
253
  proposal = { type: "idle" };
215
- if (refusedNativeCommit) {
254
+ if (nativeFinal && (recovered.type === "refused" && nativeStillComposing || (recovered.type === "committed" || recovered.type === "refused") && recovered.text !== null && recovered.text === event.data)) {
216
255
  event.preventDefault();
217
256
  return true;
218
257
  }
@@ -298,7 +337,7 @@ const createCanonicalInputBoundary = (options) => {
298
337
  return refuseEvent(event, `Input ${event.inputType} is unavailable in this session.`);
299
338
  },
300
339
  input: (view) => {
301
- if (composition.active) {
340
+ if (composition.active || composition.pendingFinal) {
302
341
  composition.flushed(view);
303
342
  return false;
304
343
  }
@@ -312,15 +351,24 @@ const createCanonicalInputBoundary = (options) => {
312
351
  },
313
352
  paste: (_view, event) => {
314
353
  beginGesture();
315
- return refuseEvent(event, "Paste is unavailable in this session.");
354
+ closeGroup();
355
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before pasting.");
356
+ return false;
316
357
  },
317
- cut: (_view, event) => {
358
+ cut: (view, event) => {
318
359
  beginGesture();
319
- return refuseEvent(event, "Cut is unavailable in this session.");
360
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before cutting.");
361
+ if (options.cut !== void 0) {
362
+ closeGroup();
363
+ return options.cut(view, event);
364
+ }
365
+ return refuseEvent(event, "The canonical clipboard owner is unavailable.");
320
366
  },
321
367
  drop: (_view, event) => {
322
368
  beginGesture();
323
- return refuseEvent(event, "Drop is unavailable in this session.");
369
+ closeGroup();
370
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before dropping.");
371
+ return false;
324
372
  }
325
373
  },
326
374
  /** Returns a classified intent only; the proposed PM document is never authoritative. */
@@ -335,4 +383,4 @@ const createCanonicalInputBoundary = (options) => {
335
383
  };
336
384
  };
337
385
  //#endregion
338
- export { createCanonicalInputBoundary };
386
+ export { CANONICAL_COMPOSITION_INPUT_TYPES, createCanonicalInputBoundary };