@stll/folio-core 0.55.0 → 0.56.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 (119) hide show
  1. package/dist/ai-edits/apply.js +169 -38
  2. package/dist/ai-edits/headless.js +2 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/reproducible-package.d.ts +0 -17
  6. package/dist/compare/reproducible-package.js +12 -18
  7. package/dist/controller/canonicalClipboard.d.ts +23 -0
  8. package/dist/controller/canonicalClipboard.js +476 -0
  9. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  10. package/dist/controller/canonicalClipboardResources.js +382 -0
  11. package/dist/controller/canonicalComposition.d.ts +20 -3
  12. package/dist/controller/canonicalComposition.js +166 -38
  13. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  14. package/dist/controller/canonicalInlineProjection.js +393 -0
  15. package/dist/controller/canonicalInput.d.ts +13 -4
  16. package/dist/controller/canonicalInput.js +52 -13
  17. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  18. package/dist/controller/canonicalInputTimer.js +11 -0
  19. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  20. package/dist/controller/canonicalPublicOperations.js +556 -0
  21. package/dist/controller/canonicalSession.d.ts +24 -6
  22. package/dist/controller/canonicalSession.js +259 -111
  23. package/dist/controller/canonicalStoryEditor.d.ts +5 -3
  24. package/dist/controller/canonicalStoryEditor.js +7 -2
  25. package/dist/controller/canonicalStructure.js +57 -4
  26. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  27. package/dist/controller/canonicalTocSelection.js +43 -0
  28. package/dist/controller/folioEditor.d.ts +2 -9
  29. package/dist/controller/folioEditor.js +5 -4
  30. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  31. package/dist/controller/headerFooterEditorManager.js +8 -7
  32. package/dist/controller/hiddenEditorApi.d.ts +12 -2
  33. package/dist/controller/hiddenEditorApi.js +4 -0
  34. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  35. package/dist/controller/hiddenEditorManager.js +216 -34
  36. package/dist/controller/noteEditorManager.d.ts +2 -1
  37. package/dist/controller/noteEditorManager.js +8 -7
  38. package/dist/document-operations.d.ts +8 -1
  39. package/dist/document-operations.js +6 -4
  40. package/dist/docx/blockPlainText.d.ts +1 -1
  41. package/dist/docx/blockPlainText.js +11 -3
  42. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  43. package/dist/docx/canonicalResourceSave.js +27 -0
  44. package/dist/docx/canonicalSave.d.ts +29 -0
  45. package/dist/docx/canonicalSave.js +101 -0
  46. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  47. package/dist/docx/canonicalStoryRepack.js +38 -5
  48. package/dist/docx/documentParser.d.ts +1 -13
  49. package/dist/docx/documentParser.js +1 -47
  50. package/dist/docx/ensureParaIds.js +10 -4
  51. package/dist/docx/footnoteParser.d.ts +1 -11
  52. package/dist/docx/footnoteParser.js +1 -16
  53. package/dist/docx/headerFooterParser.d.ts +1 -9
  54. package/dist/docx/headerFooterParser.js +1 -12
  55. package/dist/docx/headerFooterVerbatim.d.ts +26 -1
  56. package/dist/docx/headerFooterVerbatim.js +81 -3
  57. package/dist/docx/imageParser.d.ts +13 -1
  58. package/dist/docx/imageParser.js +20 -11
  59. package/dist/docx/metadataPrivacy.js +6 -1
  60. package/dist/docx/packageParts.js +9 -3
  61. package/dist/docx/paragraphPropertySource.js +2 -1
  62. package/dist/docx/parser.js +2 -1
  63. package/dist/docx/removeHeaderFooterParts.js +17 -6
  64. package/dist/docx/rezip.d.ts +49 -5
  65. package/dist/docx/rezip.js +745 -229
  66. package/dist/docx/saveDiagnostics.d.ts +11 -0
  67. package/dist/docx/selectiveSave.d.ts +2 -1
  68. package/dist/docx/selectiveSave.js +32 -3
  69. package/dist/docx/selectiveXmlPatch.js +7 -5
  70. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  71. package/dist/docx/serializer/documentSerializer.js +29 -10
  72. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  73. package/dist/docx/storyBlockReplay.d.ts +21 -1
  74. package/dist/docx/storyBlockReplay.js +113 -9
  75. package/dist/docx/storyPlainText.d.ts +20 -0
  76. package/dist/docx/storyPlainText.js +23 -0
  77. package/dist/docx/structuralXmlPatch.js +10 -22
  78. package/dist/docx/styleParser.d.ts +5 -1
  79. package/dist/docx/styleParser.js +1 -1
  80. package/dist/docx/tableParser.d.ts +1 -8
  81. package/dist/docx/tableParser.js +1 -28
  82. package/dist/docx/textBoxParser.d.ts +1 -5
  83. package/dist/docx/textBoxParser.js +1 -20
  84. package/dist/docx/unzip.js +6 -1
  85. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  86. package/dist/internal/acceptedBlockProjection.js +45 -0
  87. package/dist/managers/DocumentLoaderManager.js +2 -1
  88. package/dist/markdown/escape.d.ts +3 -1
  89. package/dist/markdown/escape.js +9 -2
  90. package/dist/markdown/renderBlock.js +5 -120
  91. package/dist/markdown/renderParagraph.js +4 -2
  92. package/dist/markdown/renderRuns.d.ts +7 -1
  93. package/dist/markdown/renderRuns.js +32 -13
  94. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  95. package/dist/prosemirror/canonicalCommands.js +11 -4
  96. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  97. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  98. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  99. package/dist/prosemirror/clipboardIntent.js +9 -0
  100. package/dist/prosemirror/commands/pageBreak.js +2 -2
  101. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  102. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  103. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  104. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  105. package/dist/prosemirror/conversion/toProseDoc.js +5 -4
  106. package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
  107. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  108. package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
  109. package/dist/prosemirror/insertOperations.js +2 -1
  110. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  111. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  112. package/dist/prosemirror/plugins/suggestionMode.js +7 -5
  113. package/dist/types/canonicalCapabilities.d.ts +215 -0
  114. package/dist/types/canonicalCapabilities.js +212 -0
  115. package/dist/types/canonicalSave.d.ts +11 -0
  116. package/dist/types/canonicalSave.js +0 -0
  117. package/dist/types/docxSerialization.d.ts +14 -0
  118. package/dist/types/docxSerialization.js +7 -0
  119. package/package.json +4 -4
@@ -0,0 +1,393 @@
1
+ import { readNoteMarkerAttrs } from "../internal/noteMarkerAttrs.js";
2
+ import { 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
+ /** Bind rendered gaps to the operation owner's source leaf ordinals, including collapsed pairs. */
283
+ const projectCanonicalInline = ({ source, paragraph, pairedBookmarkIds }) => {
284
+ const native = canonicalNativeCells(paragraph);
285
+ if (native.isErr()) return native;
286
+ const spans = inlineLeafSpans(source.content);
287
+ const erasedBookmarks = /* @__PURE__ */ new Set();
288
+ const collectErasedBookmarks = (items, context) => {
289
+ for (const item of items) {
290
+ if (item.type === "bookmarkStart" || item.type === "bookmarkEnd") {
291
+ if (context === "paragraph" && !pairedBookmarkIds.has(item.id)) erasedBookmarks.add(item);
292
+ continue;
293
+ }
294
+ if (CANONICAL_SOURCE_INLINE_POLICIES[item.type] !== "container") continue;
295
+ collectErasedBookmarks(sourceContainerChildren(item), item.type === "run" || item.type === "inlineWrapper" ? context : "nested");
296
+ }
297
+ };
298
+ collectErasedBookmarks(source.content, "paragraph");
299
+ const text = paragraphLogicalText(source);
300
+ const boundaries = Array.from({ length: text.length + 1 }, () => []);
301
+ let index = 0;
302
+ let consumed = 0;
303
+ const addGap = (position, gap) => {
304
+ const gaps = boundaries.at(gap.offset);
305
+ if (!gaps) panic(`Source leaf offset ${gap.offset} exceeds paragraph ${source.paraId}.`);
306
+ const existing = gaps.findIndex((entry) => entry.position === position);
307
+ const entry = {
308
+ position,
309
+ zeroWidthBefore: gap.zeroWidthBefore
310
+ };
311
+ if (existing < 0) gaps.push(entry);
312
+ else gaps[existing] = entry;
313
+ };
314
+ const consumeErasedLeaves = (position) => {
315
+ while (consumed === 0) {
316
+ const span = spans.at(index);
317
+ if (!span || span.before.offset !== span.after.offset || !(erasedBookmarks.has(span.node) || CANONICAL_SOURCE_INLINE_POLICIES[span.node.type] === "container" && sourceContainerChildren(span.node).length === 0)) break;
318
+ index += 1;
319
+ if (position !== null) addGap(position, span.after);
320
+ }
321
+ };
322
+ addGap(0, {
323
+ offset: 0,
324
+ zeroWidthBefore: 0
325
+ });
326
+ for (const cell of native.value) {
327
+ consumeErasedLeaves(cell.from);
328
+ let span = spans.at(index);
329
+ if (!span) return refuse(`Canonical editing cannot map native ${cell.node.type.name} to a source leaf.`);
330
+ addGap(cell.from, consumed === 0 ? span.before : {
331
+ offset: span.before.offset + consumed,
332
+ zeroWidthBefore: 0
333
+ });
334
+ switch (cell.type) {
335
+ case "zeroWidth":
336
+ for (const marker of cell.sources) {
337
+ consumeErasedLeaves(null);
338
+ span = spans.at(index);
339
+ 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.`);
340
+ index += 1;
341
+ }
342
+ if (!span) return refuse("Canonical editing lost a source range boundary.");
343
+ addGap(cell.to, span.after);
344
+ break;
345
+ case "unit":
346
+ case "markedUnit":
347
+ 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.");
348
+ 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.`);
349
+ index += 1;
350
+ addGap(cell.to, span.after);
351
+ break;
352
+ case "text":
353
+ for (let unit = 0; unit < cell.text.length; unit += 1) {
354
+ consumeErasedLeaves(cell.from + unit);
355
+ span = spans.at(index);
356
+ if (!span) return refuse("Canonical editing cannot map missing source text to native text positions.");
357
+ let value;
358
+ switch (span.node.type) {
359
+ case "text":
360
+ value = span.node.text;
361
+ break;
362
+ case "softHyphen":
363
+ case "noBreakHyphen":
364
+ value = HYPHEN_TEXT_CARRIERS[span.node.type];
365
+ break;
366
+ default: return refuse(`Canonical editing cannot map ${span.node.type} to native text positions.`);
367
+ }
368
+ if (value.charAt(consumed) !== cell.text.charAt(unit)) return refuse("Canonical editing cannot preserve the source text carrier.");
369
+ consumed += 1;
370
+ addGap(cell.from + unit + 1, {
371
+ offset: span.before.offset + consumed,
372
+ zeroWidthBefore: 0
373
+ });
374
+ if (consumed === value.length) {
375
+ index += 1;
376
+ consumed = 0;
377
+ }
378
+ }
379
+ break;
380
+ default:
381
+ }
382
+ }
383
+ consumeErasedLeaves(paragraph.content.size);
384
+ const missing = spans.at(index);
385
+ if (missing || consumed !== 0) return refuse(`Canonical editing cannot display the source positions of ${missing?.node.type ?? "incomplete text"}.`);
386
+ if (boundaries.some((gaps) => gaps.length === 0)) return refuse("Canonical editing lost a source character boundary.");
387
+ return Result.ok({
388
+ text,
389
+ boundaries
390
+ });
391
+ };
392
+ //#endregion
393
+ 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 };
@@ -4,6 +4,12 @@ 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,22 @@ 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
+ const nativeFinal = view.composing && (event.inputType === "insertText" || event.inputType === "insertReplacementText");
241
+ const nativeStillComposing = view.composing;
242
+ const recovered = composition.recover(view);
243
+ composition.forgetCompleted();
214
244
  proposal = { type: "idle" };
215
- if (refusedNativeCommit) {
245
+ if (nativeFinal && (recovered.type === "refused" && nativeStillComposing || (recovered.type === "committed" || recovered.type === "refused") && recovered.text !== null && recovered.text === event.data)) {
216
246
  event.preventDefault();
217
247
  return true;
218
248
  }
@@ -298,7 +328,7 @@ const createCanonicalInputBoundary = (options) => {
298
328
  return refuseEvent(event, `Input ${event.inputType} is unavailable in this session.`);
299
329
  },
300
330
  input: (view) => {
301
- if (composition.active) {
331
+ if (composition.active || composition.pendingFinal) {
302
332
  composition.flushed(view);
303
333
  return false;
304
334
  }
@@ -312,15 +342,24 @@ const createCanonicalInputBoundary = (options) => {
312
342
  },
313
343
  paste: (_view, event) => {
314
344
  beginGesture();
315
- return refuseEvent(event, "Paste is unavailable in this session.");
345
+ closeGroup();
346
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before pasting.");
347
+ return false;
316
348
  },
317
- cut: (_view, event) => {
349
+ cut: (view, event) => {
318
350
  beginGesture();
319
- return refuseEvent(event, "Cut is unavailable in this session.");
351
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before cutting.");
352
+ if (options.cut !== void 0) {
353
+ closeGroup();
354
+ return options.cut(view, event);
355
+ }
356
+ return refuseEvent(event, "The canonical clipboard owner is unavailable.");
320
357
  },
321
358
  drop: (_view, event) => {
322
359
  beginGesture();
323
- return refuseEvent(event, "Drop is unavailable in this session.");
360
+ closeGroup();
361
+ if (composition.active) return refuseClipboardGesture(event, "Composition must finish before dropping.");
362
+ return false;
324
363
  }
325
364
  },
326
365
  /** Returns a classified intent only; the proposed PM document is never authoritative. */
@@ -335,4 +374,4 @@ const createCanonicalInputBoundary = (options) => {
335
374
  };
336
375
  };
337
376
  //#endregion
338
- export { createCanonicalInputBoundary };
377
+ export { CANONICAL_COMPOSITION_INPUT_TYPES, createCanonicalInputBoundary };
@@ -0,0 +1,7 @@
1
+ //#region src/controller/canonicalInputTimer.d.ts
2
+ /** Shared by timer producers and browser diagnostics, regardless of bundling. */
3
+ declare const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
4
+ /** Mark the scheduled wrapper, never the caller's potentially shared callback. */
5
+ declare const setCanonicalInputTimer: (callback: () => void, delay: number) => NodeJS.Timeout;
6
+ //#endregion
7
+ export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
@@ -0,0 +1,11 @@
1
+ //#region src/controller/canonicalInputTimer.ts
2
+ /** Shared by timer producers and browser diagnostics, regardless of bundling. */
3
+ const CANONICAL_INPUT_TIMER_OWNER = "folio.canonical-input-timer";
4
+ /** Mark the scheduled wrapper, never the caller's potentially shared callback. */
5
+ const setCanonicalInputTimer = (callback, delay) => {
6
+ const ownedCallback = () => callback();
7
+ Object.defineProperty(ownedCallback, Symbol.for(CANONICAL_INPUT_TIMER_OWNER), { value: true });
8
+ return setTimeout(ownedCallback, delay);
9
+ };
10
+ //#endregion
11
+ export { CANONICAL_INPUT_TIMER_OWNER, setCanonicalInputTimer };
@@ -0,0 +1,48 @@
1
+ import { ApplyFolioDocumentOperationsOptions, FolioDocumentOperationResult, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult } from "../document-operations.js";
2
+ import { CanonicalCommit, CanonicalSession } from "./canonicalSession.js";
3
+ import { EditorState } from "prosemirror-state";
4
+ import { OpStory } from "@stll/docx-core/ops";
5
+ //#region src/controller/canonicalPublicOperations.d.ts
6
+ /** A new public kind cannot bypass a canonical compiler/refusal decision. */
7
+ declare const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS: {
8
+ readonly replaceInBlock: "compile";
9
+ readonly replaceRange: "compile";
10
+ readonly replaceBlock: "compile";
11
+ readonly splitBlock: "compile";
12
+ readonly formatRange: "compile";
13
+ readonly mergeBlockWithNext: "compile";
14
+ readonly setBlockParagraphProperties: "publicOps.unsupportedInline";
15
+ readonly insertAfterBlock: "publicOps.unsupportedInline";
16
+ readonly insertBeforeBlock: "publicOps.unsupportedInline";
17
+ readonly deleteBlock: "publicOps.unsupportedInline";
18
+ readonly commentOnBlock: "publicOps.comments";
19
+ readonly commentOnRange: "publicOps.comments";
20
+ readonly insertTable: "publicOps.tableProjection";
21
+ readonly insertSignatureTable: "publicOps.tableProjection";
22
+ readonly deleteTable: "publicOps.tableProjection";
23
+ readonly insertTableRow: "publicOps.tableProjection";
24
+ readonly deleteTableRow: "publicOps.tableProjection";
25
+ readonly insertTableColumn: "publicOps.tableProjection";
26
+ readonly deleteTableColumn: "publicOps.tableProjection";
27
+ readonly mergeTableCells: "publicOps.tableProjection";
28
+ readonly splitTableCell: "publicOps.tableProjection";
29
+ };
30
+ type CanonicalPublicOperationOptions = Omit<ApplyFolioDocumentOperationsOptions, "view" | "createUndoHandle" | "createCommentId">;
31
+ type CanonicalPublicOperationsOptions = {
32
+ session: CanonicalSession;
33
+ getState: (story: OpStory) => EditorState | null;
34
+ publish: (commit: CanonicalCommit) => boolean;
35
+ };
36
+ /** Handles address isolated journal groups; no second document or PM history is retained. */
37
+ declare class CanonicalPublicOperations {
38
+ private readonly options;
39
+ private readonly handles;
40
+ private nextHandle;
41
+ private readonly handleNamespace;
42
+ constructor(options: CanonicalPublicOperationsOptions);
43
+ apply(options: CanonicalPublicOperationOptions): FolioDocumentOperationResult;
44
+ undo(handle: FolioDocumentOperationUndoHandle): FolioDocumentOperationUndoResult;
45
+ private resolve;
46
+ }
47
+ //#endregion
48
+ export { CANONICAL_PUBLIC_OPERATION_DISPOSITIONS, CanonicalPublicOperationOptions, CanonicalPublicOperations };