@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.
- package/dist/ai-edits/apply.js +169 -38
- package/dist/ai-edits/headless.js +2 -2
- package/dist/ai-edits/types.d.ts +7 -1
- package/dist/compare/compare.js +1 -0
- package/dist/compare/reproducible-package.d.ts +0 -17
- package/dist/compare/reproducible-package.js +12 -18
- package/dist/controller/canonicalClipboard.d.ts +23 -0
- package/dist/controller/canonicalClipboard.js +476 -0
- package/dist/controller/canonicalClipboardResources.d.ts +21 -0
- package/dist/controller/canonicalClipboardResources.js +382 -0
- package/dist/controller/canonicalComposition.d.ts +20 -3
- package/dist/controller/canonicalComposition.js +166 -38
- package/dist/controller/canonicalInlineProjection.d.ts +113 -0
- package/dist/controller/canonicalInlineProjection.js +393 -0
- package/dist/controller/canonicalInput.d.ts +13 -4
- package/dist/controller/canonicalInput.js +52 -13
- package/dist/controller/canonicalInputTimer.d.ts +7 -0
- package/dist/controller/canonicalInputTimer.js +11 -0
- package/dist/controller/canonicalPublicOperations.d.ts +48 -0
- package/dist/controller/canonicalPublicOperations.js +556 -0
- package/dist/controller/canonicalSession.d.ts +24 -6
- package/dist/controller/canonicalSession.js +259 -111
- package/dist/controller/canonicalStoryEditor.d.ts +5 -3
- package/dist/controller/canonicalStoryEditor.js +7 -2
- package/dist/controller/canonicalStructure.js +57 -4
- package/dist/controller/canonicalTocSelection.d.ts +11 -0
- package/dist/controller/canonicalTocSelection.js +43 -0
- package/dist/controller/folioEditor.d.ts +2 -9
- package/dist/controller/folioEditor.js +5 -4
- package/dist/controller/headerFooterEditorManager.d.ts +2 -1
- package/dist/controller/headerFooterEditorManager.js +8 -7
- package/dist/controller/hiddenEditorApi.d.ts +12 -2
- package/dist/controller/hiddenEditorApi.js +4 -0
- package/dist/controller/hiddenEditorManager.d.ts +3 -1
- package/dist/controller/hiddenEditorManager.js +216 -34
- package/dist/controller/noteEditorManager.d.ts +2 -1
- package/dist/controller/noteEditorManager.js +8 -7
- package/dist/document-operations.d.ts +8 -1
- package/dist/document-operations.js +6 -4
- package/dist/docx/blockPlainText.d.ts +1 -1
- package/dist/docx/blockPlainText.js +11 -3
- package/dist/docx/canonicalResourceSave.d.ts +16 -0
- package/dist/docx/canonicalResourceSave.js +27 -0
- package/dist/docx/canonicalSave.d.ts +29 -0
- package/dist/docx/canonicalSave.js +101 -0
- package/dist/docx/canonicalStoryRepack.d.ts +1 -1
- package/dist/docx/canonicalStoryRepack.js +38 -5
- package/dist/docx/documentParser.d.ts +1 -13
- package/dist/docx/documentParser.js +1 -47
- package/dist/docx/ensureParaIds.js +10 -4
- package/dist/docx/footnoteParser.d.ts +1 -11
- package/dist/docx/footnoteParser.js +1 -16
- package/dist/docx/headerFooterParser.d.ts +1 -9
- package/dist/docx/headerFooterParser.js +1 -12
- package/dist/docx/headerFooterVerbatim.d.ts +26 -1
- package/dist/docx/headerFooterVerbatim.js +81 -3
- package/dist/docx/imageParser.d.ts +13 -1
- package/dist/docx/imageParser.js +20 -11
- package/dist/docx/metadataPrivacy.js +6 -1
- package/dist/docx/packageParts.js +9 -3
- package/dist/docx/paragraphPropertySource.js +2 -1
- package/dist/docx/parser.js +2 -1
- package/dist/docx/removeHeaderFooterParts.js +17 -6
- package/dist/docx/rezip.d.ts +49 -5
- package/dist/docx/rezip.js +745 -229
- package/dist/docx/saveDiagnostics.d.ts +11 -0
- package/dist/docx/selectiveSave.d.ts +2 -1
- package/dist/docx/selectiveSave.js +32 -3
- package/dist/docx/selectiveXmlPatch.js +7 -5
- package/dist/docx/serializer/documentSerializer.d.ts +9 -2
- package/dist/docx/serializer/documentSerializer.js +29 -10
- package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
- package/dist/docx/storyBlockReplay.d.ts +21 -1
- package/dist/docx/storyBlockReplay.js +113 -9
- package/dist/docx/storyPlainText.d.ts +20 -0
- package/dist/docx/storyPlainText.js +23 -0
- package/dist/docx/structuralXmlPatch.js +10 -22
- package/dist/docx/styleParser.d.ts +5 -1
- package/dist/docx/styleParser.js +1 -1
- package/dist/docx/tableParser.d.ts +1 -8
- package/dist/docx/tableParser.js +1 -28
- package/dist/docx/textBoxParser.d.ts +1 -5
- package/dist/docx/textBoxParser.js +1 -20
- package/dist/docx/unzip.js +6 -1
- package/dist/internal/acceptedBlockProjection.d.ts +12 -0
- package/dist/internal/acceptedBlockProjection.js +45 -0
- package/dist/managers/DocumentLoaderManager.js +2 -1
- package/dist/markdown/escape.d.ts +3 -1
- package/dist/markdown/escape.js +9 -2
- package/dist/markdown/renderBlock.js +5 -120
- package/dist/markdown/renderParagraph.js +4 -2
- package/dist/markdown/renderRuns.d.ts +7 -1
- package/dist/markdown/renderRuns.js +32 -13
- package/dist/prosemirror/canonicalCommands.d.ts +28 -4
- package/dist/prosemirror/canonicalCommands.js +11 -4
- package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
- package/dist/prosemirror/canonicalSelectionRange.js +15 -0
- package/dist/prosemirror/clipboardIntent.d.ts +7 -0
- package/dist/prosemirror/clipboardIntent.js +9 -0
- package/dist/prosemirror/commands/pageBreak.js +2 -2
- package/dist/prosemirror/commands/pastePlainText.js +2 -1
- package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
- package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
- package/dist/prosemirror/conversion/toProseDoc.js +5 -4
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
- package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
- package/dist/prosemirror/insertOperations.js +2 -1
- package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
- package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
- package/dist/prosemirror/plugins/suggestionMode.js +7 -5
- package/dist/types/canonicalCapabilities.d.ts +215 -0
- package/dist/types/canonicalCapabilities.js +212 -0
- package/dist/types/canonicalSave.d.ts +11 -0
- package/dist/types/canonicalSave.js +0 -0
- package/dist/types/docxSerialization.d.ts +14 -0
- package/dist/types/docxSerialization.js +7 -0
- package/package.json +4 -4
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
|
|
2
2
|
import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
|
|
3
|
-
import {
|
|
3
|
+
import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
|
|
4
|
+
import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
|
|
4
5
|
import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
|
|
5
6
|
import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
|
|
7
|
+
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
8
|
+
import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
|
|
9
|
+
import { mapTocSelection } from "./canonicalTocSelection.js";
|
|
6
10
|
import { Result, TaggedError, panic } from "better-result";
|
|
7
11
|
import { hasIllegalXmlCharacters } from "@stll/docx-core";
|
|
8
12
|
import { Fragment } from "prosemirror-model";
|
|
9
13
|
import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
|
|
10
|
-
import { TextSelection } from "prosemirror-state";
|
|
14
|
+
import { AllSelection, TextSelection } from "prosemirror-state";
|
|
11
15
|
//#region src/controller/canonicalSession.ts
|
|
12
16
|
var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
|
|
13
17
|
const refuse = (message) => Result.err(new CanonicalSessionError({
|
|
18
|
+
gap: CANONICAL_GAP.dispatch,
|
|
14
19
|
message,
|
|
15
20
|
reason: "refused"
|
|
16
21
|
}));
|
|
17
22
|
const noChange = (message) => Result.err(new CanonicalSessionError({
|
|
23
|
+
gap: CANONICAL_GAP.dispatch,
|
|
18
24
|
message,
|
|
19
25
|
reason: "noChange"
|
|
20
26
|
}));
|
|
@@ -26,6 +32,7 @@ const operationChangesPackage = (op) => {
|
|
|
26
32
|
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
27
33
|
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
28
34
|
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
35
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
29
36
|
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
30
37
|
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
31
38
|
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
@@ -36,6 +43,51 @@ const operationChangesPackage = (op) => {
|
|
|
36
43
|
return op;
|
|
37
44
|
}
|
|
38
45
|
};
|
|
46
|
+
const operationChangesStructure = (op) => {
|
|
47
|
+
switch (op.type) {
|
|
48
|
+
case DOCUMENT_OP_TYPES.DELETE_BLOCKS:
|
|
49
|
+
case DOCUMENT_OP_TYPES.INSERT_BLOCKS:
|
|
50
|
+
case DOCUMENT_OP_TYPES.SPLIT_BLOCK:
|
|
51
|
+
case DOCUMENT_OP_TYPES.JOIN_BLOCKS:
|
|
52
|
+
case DOCUMENT_OP_TYPES.REPLACE_BLOCKS:
|
|
53
|
+
case DOCUMENT_OP_TYPES.INSERT_TABLE:
|
|
54
|
+
case DOCUMENT_OP_TYPES.DELETE_TABLE:
|
|
55
|
+
case DOCUMENT_OP_TYPES.SET_CONTAINER_BLOCKS:
|
|
56
|
+
case DOCUMENT_OP_TYPES.INSERT_ROW:
|
|
57
|
+
case DOCUMENT_OP_TYPES.DELETE_ROW:
|
|
58
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_ROWS:
|
|
59
|
+
case DOCUMENT_OP_TYPES.INSERT_COLUMN:
|
|
60
|
+
case DOCUMENT_OP_TYPES.DELETE_COLUMN:
|
|
61
|
+
case DOCUMENT_OP_TYPES.MERGE_CELLS:
|
|
62
|
+
case DOCUMENT_OP_TYPES.SPLIT_CELL:
|
|
63
|
+
case DOCUMENT_OP_TYPES.SET_TABLE:
|
|
64
|
+
case DOCUMENT_OP_TYPES.RESOLVE_REVISION: return true;
|
|
65
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
66
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
67
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
68
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
69
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
70
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
71
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
72
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
73
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
74
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT:
|
|
75
|
+
case DOCUMENT_OP_TYPES.INSERT_TEXT:
|
|
76
|
+
case DOCUMENT_OP_TYPES.INSERT_CONTENT:
|
|
77
|
+
case DOCUMENT_OP_TYPES.DELETE_RANGE:
|
|
78
|
+
case DOCUMENT_OP_TYPES.SPLIT_INLINE:
|
|
79
|
+
case DOCUMENT_OP_TYPES.JOIN_INLINE:
|
|
80
|
+
case DOCUMENT_OP_TYPES.SET_RUN_PROPS:
|
|
81
|
+
case DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS:
|
|
82
|
+
case DOCUMENT_OP_TYPES.SET_PARAGRAPH_REVIEW:
|
|
83
|
+
case DOCUMENT_OP_TYPES.REPLACE_INLINE:
|
|
84
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_GRID:
|
|
85
|
+
case DOCUMENT_OP_TYPES.SET_CELL_PROPS:
|
|
86
|
+
case DOCUMENT_OP_TYPES.SET_ROW_PROPS:
|
|
87
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_PROPS: return false;
|
|
88
|
+
default: return op;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
39
91
|
const operationChangesBodyProjection = (op) => {
|
|
40
92
|
switch (op.type) {
|
|
41
93
|
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
@@ -44,6 +96,7 @@ const operationChangesBodyProjection = (op) => {
|
|
|
44
96
|
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
45
97
|
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
46
98
|
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
99
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
47
100
|
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
48
101
|
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
49
102
|
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
@@ -55,7 +108,7 @@ const operationChangesBodyProjection = (op) => {
|
|
|
55
108
|
}
|
|
56
109
|
};
|
|
57
110
|
const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
|
|
58
|
-
/** Paragraph gaps
|
|
111
|
+
/** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
|
|
59
112
|
var CanonicalProjection = class {
|
|
60
113
|
doc;
|
|
61
114
|
story;
|
|
@@ -72,15 +125,16 @@ var CanonicalProjection = class {
|
|
|
72
125
|
const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
|
|
73
126
|
if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
|
|
74
127
|
const relative = position - paragraph.start;
|
|
75
|
-
const offset = paragraph.boundaries.findIndex((gaps) => gaps.
|
|
76
|
-
if (offset < 0) return refuse("The input splits
|
|
128
|
+
const offset = paragraph.boundaries.findIndex((gaps) => gaps.some((gap) => gap.position === relative));
|
|
129
|
+
if (offset < 0) return refuse("The input splits an indivisible inline element.");
|
|
77
130
|
if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
|
|
78
131
|
const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
|
|
132
|
+
const gap = gaps.find((entry) => entry.position === relative) ?? panic("Missing canonical native gap");
|
|
79
133
|
return Result.ok({
|
|
80
134
|
story: this.story,
|
|
81
135
|
blockId: paragraph.blockId,
|
|
82
136
|
offset,
|
|
83
|
-
...gaps.length > 1 ? { zeroWidthBefore:
|
|
137
|
+
...gaps.length > 1 || gap.zeroWidthBefore > 0 ? { zeroWidthBefore: gap.zeroWidthBefore } : {}
|
|
84
138
|
});
|
|
85
139
|
}
|
|
86
140
|
inputAddressAt(position) {
|
|
@@ -88,30 +142,32 @@ var CanonicalProjection = class {
|
|
|
88
142
|
if (address.isErr()) return address;
|
|
89
143
|
const paragraph = this.paragraph(address.value.blockId);
|
|
90
144
|
if (paragraph === void 0) panic("Input lost its paragraph.");
|
|
91
|
-
let
|
|
145
|
+
let destination = position;
|
|
92
146
|
paragraph.node.forEach((node, start) => {
|
|
93
|
-
if (node.marks.some((mark) => mark.type.name === "deletion") &&
|
|
94
|
-
});
|
|
95
|
-
return Result.ok({
|
|
96
|
-
...address.value,
|
|
97
|
-
offset
|
|
147
|
+
if (node.marks.some((mark) => mark.type.name === "deletion") && position > paragraph.start + start && position < paragraph.start + start + node.nodeSize) destination = paragraph.start + start + node.nodeSize;
|
|
98
148
|
});
|
|
149
|
+
return destination === position ? address : this.addressAt(destination);
|
|
99
150
|
}
|
|
100
151
|
positionAt(address) {
|
|
101
152
|
const paragraph = this.paragraph(address.blockId);
|
|
102
153
|
if (!sameStory(address.story, this.story) || paragraph === void 0 || !Number.isInteger(address.offset) || address.offset < 0 || address.offset > paragraph.text.length || splitsSurrogatePair(paragraph.text, address.offset)) return refuse("The canonical selection is outside a plain paragraph.");
|
|
103
154
|
const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
155
|
+
const ordinal = address.zeroWidthBefore ?? (gaps.at(-1) ?? panic("Missing canonical gap")).zeroWidthBefore;
|
|
156
|
+
const gap = gaps.find(({ zeroWidthBefore }) => zeroWidthBefore === ordinal);
|
|
157
|
+
if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal > (gaps.at(-1)?.zeroWidthBefore ?? 0)) return refuse("The canonical selection is outside a paragraph gap.");
|
|
158
|
+
if (gap === void 0) return refuse("This source gap is inside a collapsed or erased inline boundary and has no editor position.");
|
|
159
|
+
return Result.ok(paragraph.start + gap.position);
|
|
107
160
|
}
|
|
108
161
|
selectionAt(state) {
|
|
109
|
-
if (!(state.selection instanceof TextSelection)) return refuse("Canonical input requires a text selection.");
|
|
110
|
-
const
|
|
162
|
+
if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
|
|
163
|
+
const range = canonicalSelectionRange(state);
|
|
164
|
+
const type = state.selection instanceof AllSelection ? "all" : "text";
|
|
165
|
+
const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
|
|
111
166
|
if (anchor.isErr()) return anchor;
|
|
112
|
-
const head = this.addressAt(state.selection.head);
|
|
167
|
+
const head = this.addressAt(type === "all" ? range.to : state.selection.head);
|
|
113
168
|
if (head.isErr()) return head;
|
|
114
169
|
return Result.ok({
|
|
170
|
+
type,
|
|
115
171
|
anchor: anchor.value,
|
|
116
172
|
head: head.value
|
|
117
173
|
});
|
|
@@ -162,31 +218,29 @@ const preservePropertySources = ({ target, source, stories = documentStories(sou
|
|
|
162
218
|
}
|
|
163
219
|
copyDocumentParagraphPropertySourceContract(target, source);
|
|
164
220
|
};
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
case "run": return item.content.every((child) => child.type === "text" && !hasIllegalXmlCharacters(child.text) && !/[\t\r\n]/u.test(child.text) || child.type === "noteMarker" || child.type === "footnoteRef" || child.type === "endnoteRef" || child.type === "tab" || child.type === "break");
|
|
168
|
-
case "insertion":
|
|
169
|
-
case "deletion": return supportsTextContent(item.content);
|
|
170
|
-
default: return false;
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
const supportsSeed = (document, stories = documentStories(document)) => {
|
|
174
|
-
if ((document.package.document.comments?.length ?? 0) > 0) return false;
|
|
175
|
-
return stories.every((story) => {
|
|
221
|
+
const unsupportedSeedReason = (document, stories = documentStories(document)) => {
|
|
222
|
+
for (const story of stories) {
|
|
176
223
|
const body = findStoryBody(document, story);
|
|
177
224
|
if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
|
|
178
225
|
const note = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id);
|
|
179
|
-
if (note?.noteType !== void 0 && note.noteType !== "normal")
|
|
226
|
+
if (note?.noteType !== void 0 && note.noteType !== "normal") continue;
|
|
180
227
|
}
|
|
181
228
|
const content = body?.content;
|
|
182
|
-
|
|
183
|
-
|
|
229
|
+
if (!content || content.length === 0) return "Canonical sessions require a nonempty paragraph story.";
|
|
230
|
+
for (const paragraph of content) {
|
|
231
|
+
if (paragraph.type !== "paragraph" || paragraph.reviewCarrier !== void 0) return "Canonical sessions currently require plain paragraphs and supported inline atoms.";
|
|
232
|
+
const issue = canonicalInlineSourceIssue(paragraph.content);
|
|
233
|
+
if (issue !== null) return issue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
184
237
|
};
|
|
185
238
|
const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
|
|
186
239
|
const body = findStoryBody(document, story);
|
|
187
240
|
if (!body) return refuse("The canonical story no longer exists.");
|
|
241
|
+
const currentStyles = Object.hasOwn(document.package, "styles") ? document.package.styles : styles;
|
|
188
242
|
const options = {
|
|
189
|
-
...
|
|
243
|
+
...currentStyles == null ? {} : { styles: currentStyles },
|
|
190
244
|
...document.package.theme === void 0 ? {} : { theme: document.package.theme }
|
|
191
245
|
};
|
|
192
246
|
const converted = Result.try({
|
|
@@ -196,67 +250,50 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
|
|
|
196
250
|
return footnoteToProseDoc(body.content, options);
|
|
197
251
|
},
|
|
198
252
|
catch: (cause) => new CanonicalSessionError({
|
|
253
|
+
gap: CANONICAL_GAP.dispatch,
|
|
199
254
|
reason: "refused",
|
|
200
255
|
message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
201
256
|
})
|
|
202
257
|
});
|
|
203
258
|
if (converted.isErr()) return converted;
|
|
259
|
+
const pairedBookmarkIds = collectPairedBookmarkIds(body.content);
|
|
204
260
|
const paragraphs = [];
|
|
205
261
|
let failure;
|
|
206
262
|
converted.value.forEach((node, offset, index) => {
|
|
207
263
|
const source = body.content.at(index);
|
|
208
264
|
if (source?.type !== "paragraph" || source.paraId === void 0) {
|
|
209
265
|
failure = new CanonicalSessionError({
|
|
266
|
+
gap: CANONICAL_GAP.dispatch,
|
|
210
267
|
reason: "refused",
|
|
211
268
|
message: "The projection changed paragraph structure."
|
|
212
269
|
});
|
|
213
270
|
return;
|
|
214
271
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
renderedSize += 1;
|
|
230
|
-
boundaries.push([renderedSize]);
|
|
231
|
-
}
|
|
232
|
-
else if (child.type === "footnoteRef" || child.type === "endnoteRef") {
|
|
233
|
-
text += "";
|
|
234
|
-
renderedText += String(child.id);
|
|
235
|
-
renderedSize += String(child.id).length;
|
|
236
|
-
boundaries.push([renderedSize]);
|
|
237
|
-
} else if (child.type === "tab" || child.type === "break") {
|
|
238
|
-
text += "";
|
|
239
|
-
renderedSize += 1;
|
|
240
|
-
boundaries.push([renderedSize]);
|
|
241
|
-
} else if (child.type === "noteMarker") {
|
|
242
|
-
renderedSize += 1;
|
|
243
|
-
(boundaries.at(-1) ?? panic("Missing canonical note gap")).push(renderedSize);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
appendContent(source.content);
|
|
248
|
-
if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId || node.textContent !== renderedText || node.content.size !== renderedSize) {
|
|
272
|
+
if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
|
|
273
|
+
failure = new CanonicalSessionError({
|
|
274
|
+
gap: CANONICAL_GAP.dispatch,
|
|
275
|
+
reason: "refused",
|
|
276
|
+
message: "The projection changed paragraph structure."
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const inline = projectCanonicalInline({
|
|
281
|
+
source,
|
|
282
|
+
paragraph: node,
|
|
283
|
+
pairedBookmarkIds
|
|
284
|
+
});
|
|
285
|
+
if (inline.isErr()) {
|
|
249
286
|
failure = new CanonicalSessionError({
|
|
287
|
+
gap: CANONICAL_GAP.dispatch,
|
|
250
288
|
reason: "refused",
|
|
251
|
-
message:
|
|
289
|
+
message: inline.error.message
|
|
252
290
|
});
|
|
253
291
|
return;
|
|
254
292
|
}
|
|
255
293
|
paragraphs.push({
|
|
256
294
|
blockId: source.paraId,
|
|
257
295
|
start: offset + 1,
|
|
258
|
-
|
|
259
|
-
boundaries,
|
|
296
|
+
...inline.value,
|
|
260
297
|
node,
|
|
261
298
|
source
|
|
262
299
|
});
|
|
@@ -271,9 +308,15 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
|
|
|
271
308
|
};
|
|
272
309
|
const intentStory = (intent) => {
|
|
273
310
|
switch (intent.type) {
|
|
311
|
+
case "setHyperlink":
|
|
312
|
+
case "removeHyperlink":
|
|
313
|
+
case "insertHyperlink":
|
|
314
|
+
case "replaceFragment":
|
|
315
|
+
case "moveFragment":
|
|
274
316
|
case "replaceText":
|
|
275
317
|
case "formatRun":
|
|
276
318
|
case "insertAtom": return intent.from.story;
|
|
319
|
+
case "generateTOC":
|
|
277
320
|
case "splitParagraph":
|
|
278
321
|
case "formatParagraph": return intent.at.story;
|
|
279
322
|
case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
|
|
@@ -284,14 +327,14 @@ const intentStory = (intent) => {
|
|
|
284
327
|
};
|
|
285
328
|
const UNDO_GROUP_WINDOW_MS = 500;
|
|
286
329
|
const samePosition = (left, right) => sameStory(left.story, right.story) && left.blockId === right.blockId && left.offset === right.offset && (left.zeroWidthBefore ?? 0) === (right.zeroWidthBefore ?? 0);
|
|
287
|
-
const sameSelection = (left, right) => samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
|
|
330
|
+
const sameSelection = (left, right) => left.type === right.type && samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
|
|
288
331
|
const continuesGroup = (previous, next) => {
|
|
289
332
|
switch (next.semantic) {
|
|
290
333
|
case "typing":
|
|
291
334
|
case "deleteBackward":
|
|
292
335
|
case "deleteForward": return previous.grouping === "run" && next.grouping === "run" && previous.semantic === next.semantic && previous.boundary === next.boundary && next.time >= previous.time && next.time - previous.time <= UNDO_GROUP_WINDOW_MS && sameSelection(previous.postSelection, next.preSelection) && samePosition(next.preSelection.anchor, next.preSelection.head);
|
|
336
|
+
case "composition": return next.grouping === "compositionCorrection" && previous.semantic === "composition" && previous.boundary === next.boundary && sameSelection(previous.postSelection, next.preSelection);
|
|
293
337
|
case "paste":
|
|
294
|
-
case "composition":
|
|
295
338
|
case "replacement":
|
|
296
339
|
case "structure": return false;
|
|
297
340
|
default: {
|
|
@@ -312,6 +355,7 @@ const publishCanonicalProjection = ({ state, commit, session }) => {
|
|
|
312
355
|
const applied = Result.try({
|
|
313
356
|
try: () => state.applyTransaction(commit.transaction),
|
|
314
357
|
catch: (cause) => new CanonicalSessionError({
|
|
358
|
+
gap: CANONICAL_GAP.dispatch,
|
|
315
359
|
reason: "refused",
|
|
316
360
|
message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
317
361
|
})
|
|
@@ -336,7 +380,10 @@ const storyProjectionKey = (story) => {
|
|
|
336
380
|
var CanonicalSession = class {
|
|
337
381
|
currentDocument;
|
|
338
382
|
currentProjection;
|
|
383
|
+
sourceBlockIds;
|
|
339
384
|
currentVersion = 0;
|
|
385
|
+
saveTouched = /* @__PURE__ */ new Set();
|
|
386
|
+
saveStructure = "stable";
|
|
340
387
|
allocateIntentIds = createEditorIntentIdAllocator();
|
|
341
388
|
mode = { type: "editing" };
|
|
342
389
|
allocatedBlockIds = /* @__PURE__ */ new Set();
|
|
@@ -352,6 +399,7 @@ var CanonicalSession = class {
|
|
|
352
399
|
constructor({ document, projection, styles }) {
|
|
353
400
|
this.currentDocument = document;
|
|
354
401
|
this.currentProjection = projection;
|
|
402
|
+
this.sourceBlockIds = new Set(packageParagraphIds(document.package));
|
|
355
403
|
for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
|
|
356
404
|
this.advanceBlockId();
|
|
357
405
|
this.styles = styles == null ? styles : structuredClone(styles);
|
|
@@ -375,13 +423,18 @@ var CanonicalSession = class {
|
|
|
375
423
|
return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
|
|
376
424
|
}
|
|
377
425
|
intentMode(document, intent) {
|
|
378
|
-
if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
|
|
426
|
+
if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
|
|
427
|
+
type: "editing",
|
|
428
|
+
reservedBlockIds: this.allocatedBlockIds
|
|
429
|
+
};
|
|
379
430
|
const ids = this.allocateIntentIds(document, intent);
|
|
380
431
|
return this.mode.type === "editing" ? {
|
|
381
432
|
type: "editing",
|
|
382
|
-
newIds: ids.newIds
|
|
433
|
+
newIds: ids.newIds,
|
|
434
|
+
reservedBlockIds: this.allocatedBlockIds
|
|
383
435
|
} : {
|
|
384
436
|
type: "suggesting",
|
|
437
|
+
reservedBlockIds: this.allocatedBlockIds,
|
|
385
438
|
revision: {
|
|
386
439
|
id: ids.revisionId,
|
|
387
440
|
author: this.mode.author,
|
|
@@ -394,8 +447,14 @@ var CanonicalSession = class {
|
|
|
394
447
|
let blockIds;
|
|
395
448
|
switch (intent.type) {
|
|
396
449
|
case "table": return true;
|
|
450
|
+
case "generateTOC":
|
|
397
451
|
case "setList":
|
|
398
452
|
case "formatParagraph": return false;
|
|
453
|
+
case "setHyperlink":
|
|
454
|
+
case "removeHyperlink":
|
|
455
|
+
case "insertHyperlink":
|
|
456
|
+
case "replaceFragment":
|
|
457
|
+
case "moveFragment": return true;
|
|
399
458
|
case "replaceText":
|
|
400
459
|
case "insertAtom":
|
|
401
460
|
case "formatRun":
|
|
@@ -428,11 +487,26 @@ var CanonicalSession = class {
|
|
|
428
487
|
}
|
|
429
488
|
get document() {
|
|
430
489
|
if (this.isComposing) throw new CanonicalSessionError({
|
|
490
|
+
gap: CANONICAL_GAP.dispatch,
|
|
431
491
|
reason: "refused",
|
|
432
492
|
message: "Composition must finish before taking a snapshot."
|
|
433
493
|
});
|
|
434
494
|
return this.currentDocument;
|
|
435
495
|
}
|
|
496
|
+
captureSaveSnapshot() {
|
|
497
|
+
if (this.isComposing) throw new CanonicalSessionError({
|
|
498
|
+
gap: CANONICAL_GAP.save,
|
|
499
|
+
reason: "refused",
|
|
500
|
+
message: "Composition must finish before saving the canonical document."
|
|
501
|
+
});
|
|
502
|
+
const retainedBlockIdKeys = new Set([...this.sourceBlockIds, ...packageParagraphIds(this.currentDocument.package)].map((blockId) => blockId.toUpperCase()));
|
|
503
|
+
return {
|
|
504
|
+
document: cloneDocumentWithParagraphPropertySources(this.currentDocument),
|
|
505
|
+
version: this.currentVersion,
|
|
506
|
+
changedBlockIds: [...this.saveTouched].filter((blockId) => retainedBlockIdKeys.has(blockId.toUpperCase())),
|
|
507
|
+
structure: this.saveStructure
|
|
508
|
+
};
|
|
509
|
+
}
|
|
436
510
|
get projection() {
|
|
437
511
|
return this.currentProjection;
|
|
438
512
|
}
|
|
@@ -469,7 +543,7 @@ var CanonicalSession = class {
|
|
|
469
543
|
if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
|
|
470
544
|
return Result.ok(void 0);
|
|
471
545
|
}
|
|
472
|
-
prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", time = Date.now() }) {
|
|
546
|
+
prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", compositionPhase, time = Date.now() }) {
|
|
473
547
|
const projected = this.projectStory(story);
|
|
474
548
|
if (projected.isErr()) return projected;
|
|
475
549
|
const projection = projected.value;
|
|
@@ -498,11 +572,14 @@ var CanonicalSession = class {
|
|
|
498
572
|
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
499
573
|
const { ops, selection: caret } = compiled.value;
|
|
500
574
|
const postSelection = {
|
|
575
|
+
type: "text",
|
|
501
576
|
anchor: caret,
|
|
502
577
|
head: caret
|
|
503
578
|
};
|
|
504
579
|
const preDocument = this.currentDocument;
|
|
505
|
-
const
|
|
580
|
+
const isCaret = state.selection.empty;
|
|
581
|
+
const deletion = isCaret && text.length === 0 && (semantic === "deleteBackward" || semantic === "deleteForward") ? deletionRange(state, semantic === "deleteBackward" ? "backward" : "forward") : null;
|
|
582
|
+
const grouping = isCaret && (semantic === "typing" && from === to && from === state.selection.head || deletion?.isOk() && deletion.value.from === from && deletion.value.to === to) ? "run" : "isolated";
|
|
506
583
|
return this.stage({
|
|
507
584
|
state,
|
|
508
585
|
story,
|
|
@@ -521,7 +598,7 @@ var CanonicalSession = class {
|
|
|
521
598
|
version,
|
|
522
599
|
origin: "input",
|
|
523
600
|
semantic,
|
|
524
|
-
grouping:
|
|
601
|
+
grouping: compositionPhase === "correction" ? "compositionCorrection" : grouping,
|
|
525
602
|
time,
|
|
526
603
|
boundary: this.groupingBoundary
|
|
527
604
|
};
|
|
@@ -541,9 +618,9 @@ var CanonicalSession = class {
|
|
|
541
618
|
});
|
|
542
619
|
}
|
|
543
620
|
prepareIntent(state, intent) {
|
|
544
|
-
return this.prepareIntents(state, [intent]);
|
|
621
|
+
return this.prepareIntents(state, { intents: [intent] });
|
|
545
622
|
}
|
|
546
|
-
prepareIntents(state, intents) {
|
|
623
|
+
prepareIntents(state, { intents, resourceOps = [], semantic = "structure" }) {
|
|
547
624
|
const first = intents.at(0);
|
|
548
625
|
if (first === void 0) return noChange("The intent batch is empty.");
|
|
549
626
|
const story = intentStory(first);
|
|
@@ -556,12 +633,24 @@ var CanonicalSession = class {
|
|
|
556
633
|
if (preSelection.isErr()) return preSelection;
|
|
557
634
|
let document = this.currentDocument;
|
|
558
635
|
let postSelection = preSelection.value;
|
|
559
|
-
const ops = [];
|
|
636
|
+
const ops = [...resourceOps];
|
|
560
637
|
const edits = [];
|
|
638
|
+
if (resourceOps.length > 0) {
|
|
639
|
+
const imported = applyDocumentOps(document, resourceOps);
|
|
640
|
+
if (imported.isErr()) return refuse(imported.error.message);
|
|
641
|
+
document = imported.value.document;
|
|
642
|
+
edits.push(imported.value);
|
|
643
|
+
}
|
|
561
644
|
for (const intent of intents) {
|
|
645
|
+
if (this.mode.type === "suggesting" && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink" || intent.type === "generateTOC")) return Result.err(new CanonicalSessionError({
|
|
646
|
+
gap: CANONICAL_GAP.trackedHyperlinkResolution,
|
|
647
|
+
reason: "refused",
|
|
648
|
+
message: "Hyperlink and TOC suggestions require serializable wrapper review provenance."
|
|
649
|
+
}));
|
|
562
650
|
const compiled = compileEditorIntent(document, {
|
|
563
651
|
intent,
|
|
564
|
-
mode: this.intentMode(document, intent)
|
|
652
|
+
mode: this.intentMode(document, intent),
|
|
653
|
+
firstBlockId: this.nextBlockId
|
|
565
654
|
});
|
|
566
655
|
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
567
656
|
const applied = applyDocumentOps(document, compiled.value.ops);
|
|
@@ -569,7 +658,18 @@ var CanonicalSession = class {
|
|
|
569
658
|
document = applied.value.document;
|
|
570
659
|
edits.push(applied.value);
|
|
571
660
|
ops.push(...compiled.value.ops);
|
|
572
|
-
if (intent.type
|
|
661
|
+
if (intent.type === "generateTOC") {
|
|
662
|
+
const mapping = {
|
|
663
|
+
at: intent.at,
|
|
664
|
+
after: compiled.value.selection,
|
|
665
|
+
ops: compiled.value.ops
|
|
666
|
+
};
|
|
667
|
+
postSelection = Object.assign({}, postSelection, {
|
|
668
|
+
anchor: mapTocSelection(postSelection.anchor, mapping),
|
|
669
|
+
head: mapTocSelection(postSelection.head, mapping)
|
|
670
|
+
});
|
|
671
|
+
} else if (intent.type !== "setHyperlink" && intent.type !== "removeHyperlink" && intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
|
|
672
|
+
type: "text",
|
|
573
673
|
anchor: compiled.value.selection,
|
|
574
674
|
head: compiled.value.selection
|
|
575
675
|
};
|
|
@@ -582,7 +682,8 @@ var CanonicalSession = class {
|
|
|
582
682
|
state,
|
|
583
683
|
ops,
|
|
584
684
|
postSelection,
|
|
585
|
-
stagedApplied
|
|
685
|
+
stagedApplied,
|
|
686
|
+
semantic
|
|
586
687
|
});
|
|
587
688
|
}
|
|
588
689
|
prepareOps(state, ops, postSelection) {
|
|
@@ -592,7 +693,7 @@ var CanonicalSession = class {
|
|
|
592
693
|
postSelection
|
|
593
694
|
});
|
|
594
695
|
}
|
|
595
|
-
prepareJournalledOps({ state, ops, postSelection, stagedApplied }) {
|
|
696
|
+
prepareJournalledOps({ state, ops, postSelection, stagedApplied, semantic = "structure" }) {
|
|
596
697
|
const story = postSelection.anchor.story;
|
|
597
698
|
const projected = this.projectStory(story);
|
|
598
699
|
if (projected.isErr()) return projected;
|
|
@@ -618,7 +719,7 @@ var CanonicalSession = class {
|
|
|
618
719
|
postDocument: this.currentDocument,
|
|
619
720
|
version,
|
|
620
721
|
origin: "input",
|
|
621
|
-
semantic
|
|
722
|
+
semantic,
|
|
622
723
|
grouping: "isolated",
|
|
623
724
|
time: Date.now(),
|
|
624
725
|
boundary: this.groupingBoundary
|
|
@@ -656,7 +757,7 @@ var CanonicalSession = class {
|
|
|
656
757
|
},
|
|
657
758
|
patch: { styleId: nextStyle }
|
|
658
759
|
});
|
|
659
|
-
return this.prepareIntents(state, intents);
|
|
760
|
+
return this.prepareIntents(state, { intents });
|
|
660
761
|
}
|
|
661
762
|
prepareJoin(state, direction) {
|
|
662
763
|
const checked = this.checkState(state);
|
|
@@ -712,6 +813,7 @@ var CanonicalSession = class {
|
|
|
712
813
|
state,
|
|
713
814
|
ops,
|
|
714
815
|
postSelection: {
|
|
816
|
+
type: "text",
|
|
715
817
|
anchor: caret,
|
|
716
818
|
head: caret
|
|
717
819
|
},
|
|
@@ -836,7 +938,8 @@ var CanonicalSession = class {
|
|
|
836
938
|
document: applied.value.document,
|
|
837
939
|
previous: this.currentDocument
|
|
838
940
|
});
|
|
839
|
-
|
|
941
|
+
const unsupported = unsupportedSeedReason(applied.value.document, changed);
|
|
942
|
+
if (unsupported !== null) return refuse(unsupported);
|
|
840
943
|
preservePropertySources({
|
|
841
944
|
target: applied.value.document,
|
|
842
945
|
source: this.currentDocument,
|
|
@@ -894,8 +997,22 @@ var CanonicalSession = class {
|
|
|
894
997
|
if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
|
|
895
998
|
const anchor = projected.value.positionAt(selection.anchor);
|
|
896
999
|
const head = projected.value.positionAt(selection.head);
|
|
897
|
-
|
|
898
|
-
|
|
1000
|
+
switch (selection.type) {
|
|
1001
|
+
case "all":
|
|
1002
|
+
transaction.setSelection(new AllSelection(transaction.doc));
|
|
1003
|
+
break;
|
|
1004
|
+
case "text":
|
|
1005
|
+
if (anchor.isOk() && head.isOk()) {
|
|
1006
|
+
transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
|
|
1010
|
+
break;
|
|
1011
|
+
default: {
|
|
1012
|
+
const exhaustive = selection.type;
|
|
1013
|
+
return panic(`Unknown canonical selection type: ${exhaustive}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
899
1016
|
}
|
|
900
1017
|
transaction.setMeta(CANONICAL_PROJECTION_META, {
|
|
901
1018
|
type: "canonical",
|
|
@@ -939,6 +1056,12 @@ var CanonicalSession = class {
|
|
|
939
1056
|
this.currentSelection = selection;
|
|
940
1057
|
this.currentDocument = applied.value.document;
|
|
941
1058
|
this.currentProjection = bodyProjection.value;
|
|
1059
|
+
for (const ids of [
|
|
1060
|
+
applied.value.touched.modified,
|
|
1061
|
+
applied.value.touched.inserted,
|
|
1062
|
+
applied.value.touched.removed
|
|
1063
|
+
]) for (const id of ids) this.saveTouched.add(id);
|
|
1064
|
+
if (applied.value.touched.inserted.length > 0 || applied.value.touched.removed.length > 0 || ops.some(operationChangesPackage) || ops.some(operationChangesStructure)) this.saveStructure = "changed";
|
|
942
1065
|
this.currentVersion = baseVersion + 1;
|
|
943
1066
|
onPublish(applied.value.inverse, this.currentVersion);
|
|
944
1067
|
return Result.ok(void 0);
|
|
@@ -947,7 +1070,8 @@ var CanonicalSession = class {
|
|
|
947
1070
|
}
|
|
948
1071
|
};
|
|
949
1072
|
const createCanonicalSession = (document, styles) => {
|
|
950
|
-
|
|
1073
|
+
const unsupported = unsupportedSeedReason(document);
|
|
1074
|
+
if (unsupported !== null) return refuse(unsupported);
|
|
951
1075
|
const owned = cloneDocumentWithParagraphPropertySources(document);
|
|
952
1076
|
const normalized = normalizeForOps(owned);
|
|
953
1077
|
preservePropertySources({
|
|
@@ -980,31 +1104,55 @@ const visibleDeletionContext = (state) => {
|
|
|
980
1104
|
let last = index;
|
|
981
1105
|
while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
|
|
982
1106
|
let text = "";
|
|
983
|
-
const
|
|
1107
|
+
const physicalStarts = [];
|
|
1108
|
+
const physicalEnds = [];
|
|
984
1109
|
let visibleOffset = 0;
|
|
1110
|
+
let issue;
|
|
985
1111
|
state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
|
|
986
1112
|
if (paragraphIndex < first || paragraphIndex > last) return;
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1113
|
+
const native = canonicalNativeCells(paragraph);
|
|
1114
|
+
if (native.isErr()) {
|
|
1115
|
+
issue = new CanonicalSessionError({
|
|
1116
|
+
gap: CANONICAL_GAP.dispatch,
|
|
1117
|
+
reason: "refused",
|
|
1118
|
+
message: native.error.message
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
for (const cell of native.value) {
|
|
1123
|
+
if (cell.type === "zeroWidth" || cell.node.marks.some((mark) => mark.type.name === "deletion")) continue;
|
|
1124
|
+
const value = cell.type === "text" ? cell.text : "";
|
|
1125
|
+
const from = paragraphOffset + 1 + cell.from;
|
|
1126
|
+
const to = paragraphOffset + 1 + cell.to;
|
|
1127
|
+
if (cell.type !== "text" && selection.from > from && selection.from < to) {
|
|
1128
|
+
issue = new CanonicalSessionError({
|
|
1129
|
+
gap: CANONICAL_GAP.dispatch,
|
|
1130
|
+
reason: "refused",
|
|
1131
|
+
message: "Character deletion cannot start inside an inline atom."
|
|
1132
|
+
});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
990
1135
|
for (let unit = 0; unit < value.length; unit += 1) {
|
|
991
|
-
const position =
|
|
992
|
-
|
|
1136
|
+
const position = from + (cell.type === "text" ? unit : 0);
|
|
1137
|
+
physicalStarts.push(position);
|
|
993
1138
|
text += value.charAt(unit);
|
|
994
|
-
|
|
1139
|
+
physicalEnds.push(cell.type === "text" ? position + 1 : to);
|
|
995
1140
|
if (position < selection.from) visibleOffset += 1;
|
|
996
1141
|
}
|
|
997
|
-
}
|
|
1142
|
+
}
|
|
998
1143
|
});
|
|
999
|
-
return {
|
|
1144
|
+
return issue === void 0 ? Result.ok({
|
|
1000
1145
|
text,
|
|
1001
|
-
|
|
1146
|
+
physicalStarts,
|
|
1147
|
+
physicalEnds,
|
|
1002
1148
|
visibleOffset
|
|
1003
|
-
};
|
|
1149
|
+
}) : Result.err(issue);
|
|
1004
1150
|
};
|
|
1005
1151
|
const isCanonicalJoinBoundary = (state, direction) => {
|
|
1006
1152
|
if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
|
|
1007
|
-
const
|
|
1153
|
+
const context = visibleDeletionContext(state);
|
|
1154
|
+
if (context.isErr()) return false;
|
|
1155
|
+
const { text, visibleOffset } = context.value;
|
|
1008
1156
|
return visibleOffset === (direction === "backward" ? 0 : text.length);
|
|
1009
1157
|
};
|
|
1010
1158
|
/** Native character deletion consumes one whole visible grapheme. */
|
|
@@ -1018,20 +1166,20 @@ const deletionRange = (state, direction) => {
|
|
|
1018
1166
|
const parent = selection.$from.parent;
|
|
1019
1167
|
const offset = selection.$from.parentOffset;
|
|
1020
1168
|
if ((direction === "backward" ? parent.childBefore(offset).node : parent.childAfter(offset).node)?.type.name === "noteMarker") return refuse("Automatic note marks cannot be deleted as characters.");
|
|
1021
|
-
const
|
|
1169
|
+
const context = visibleDeletionContext(state);
|
|
1170
|
+
if (context.isErr()) return context;
|
|
1171
|
+
const { text, physicalStarts, physicalEnds, visibleOffset } = context.value;
|
|
1022
1172
|
if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
|
|
1023
1173
|
if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
|
|
1024
1174
|
if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
|
|
1025
1175
|
let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
|
|
1026
1176
|
while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
from: selection.from,
|
|
1034
|
-
to: position
|
|
1177
|
+
const from = physicalStarts.at(direction === "backward" ? boundary : visibleOffset);
|
|
1178
|
+
const to = physicalEnds.at((direction === "backward" ? visibleOffset : boundary) - 1);
|
|
1179
|
+
if (from === void 0 || to === void 0) panic("Canonical deletion lost a grapheme boundary.");
|
|
1180
|
+
return Result.ok({
|
|
1181
|
+
from,
|
|
1182
|
+
to
|
|
1035
1183
|
});
|
|
1036
1184
|
};
|
|
1037
1185
|
//#endregion
|