@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
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
|
|
2
|
+
import { buildCleanBlockText, resolveCleanTextRange } from "../ai-edits/clean-text.js";
|
|
3
|
+
import { stripInlineEmphasisMarkers } from "../ai-edits/inline-emphasis.js";
|
|
4
|
+
import { collectNoteReferenceLabels, hashFolioAIBlockText } from "../ai-edits/snapshot.js";
|
|
5
|
+
import { wordDiffSessionFromOptions } from "../ai-edits/word-diff.js";
|
|
6
|
+
import { getFolioDocumentOperationIssues, getFolioDocumentOperationReceiptsForStory, parseFolioDocumentOperationBatch } from "../document-operations.js";
|
|
7
|
+
import { maxAnnotationIdInDoc } from "../prosemirror/plugins/revisionIds.js";
|
|
8
|
+
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
9
|
+
import { Result, TaggedError, panic } from "better-result";
|
|
10
|
+
import { Mark } from "prosemirror-model";
|
|
11
|
+
import { OP_STORIES, applyDocumentOps, compileEditorIntent, createEditorIntentIdAllocator, documentStories } from "@stll/docx-core/ops";
|
|
12
|
+
//#region src/controller/canonicalPublicOperations.ts
|
|
13
|
+
/** Public batches compile against the read projection; only the canonical journal publishes. */
|
|
14
|
+
/** A new public kind cannot bypass a canonical compiler/refusal decision. */
|
|
15
|
+
const CANONICAL_PUBLIC_OPERATION_DISPOSITIONS = {
|
|
16
|
+
replaceInBlock: "compile",
|
|
17
|
+
replaceRange: "compile",
|
|
18
|
+
replaceBlock: "compile",
|
|
19
|
+
splitBlock: "compile",
|
|
20
|
+
formatRange: "compile",
|
|
21
|
+
mergeBlockWithNext: "compile",
|
|
22
|
+
setBlockParagraphProperties: CANONICAL_GAP.publicUnsupportedInline,
|
|
23
|
+
insertAfterBlock: CANONICAL_GAP.publicUnsupportedInline,
|
|
24
|
+
insertBeforeBlock: CANONICAL_GAP.publicUnsupportedInline,
|
|
25
|
+
deleteBlock: CANONICAL_GAP.publicUnsupportedInline,
|
|
26
|
+
commentOnBlock: CANONICAL_GAP.publicComments,
|
|
27
|
+
commentOnRange: CANONICAL_GAP.publicComments,
|
|
28
|
+
insertTable: CANONICAL_GAP.publicTableProjection,
|
|
29
|
+
insertSignatureTable: CANONICAL_GAP.publicTableProjection,
|
|
30
|
+
deleteTable: CANONICAL_GAP.publicTableProjection,
|
|
31
|
+
insertTableRow: CANONICAL_GAP.publicTableProjection,
|
|
32
|
+
deleteTableRow: CANONICAL_GAP.publicTableProjection,
|
|
33
|
+
insertTableColumn: CANONICAL_GAP.publicTableProjection,
|
|
34
|
+
deleteTableColumn: CANONICAL_GAP.publicTableProjection,
|
|
35
|
+
mergeTableCells: CANONICAL_GAP.publicTableProjection,
|
|
36
|
+
splitTableCell: CANONICAL_GAP.publicTableProjection
|
|
37
|
+
};
|
|
38
|
+
const compilePublicIntents = ({ document, intents, mode, author, date, nextRevisionId, allocate }) => {
|
|
39
|
+
const ops = [];
|
|
40
|
+
const revisions = [];
|
|
41
|
+
for (const sourceIntent of intents) {
|
|
42
|
+
const ids = allocate(document, sourceIntent);
|
|
43
|
+
const intent = sourceIntent.type === "splitParagraph" ? {
|
|
44
|
+
type: "splitParagraph",
|
|
45
|
+
at: sourceIntent.at,
|
|
46
|
+
...sourceIntent.to === void 0 ? {} : { to: sourceIntent.to },
|
|
47
|
+
newBlockId: ids.newBlockId
|
|
48
|
+
} : sourceIntent;
|
|
49
|
+
const firstRevisionId = nextRevisionId;
|
|
50
|
+
const compiled = compileEditorIntent(document, {
|
|
51
|
+
intent,
|
|
52
|
+
mode: mode === "direct" ? {
|
|
53
|
+
type: "editing",
|
|
54
|
+
newIds: ids.newIds
|
|
55
|
+
} : {
|
|
56
|
+
type: "suggesting",
|
|
57
|
+
revision: {
|
|
58
|
+
id: firstRevisionId,
|
|
59
|
+
author,
|
|
60
|
+
date
|
|
61
|
+
},
|
|
62
|
+
newIds: {
|
|
63
|
+
control: ids.newIds.control,
|
|
64
|
+
revision: ids.newIds.revision.map((_, index) => firstRevisionId + index + 1)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (compiled.isErr()) return Result.err(compiled.error);
|
|
69
|
+
const applied = applyDocumentOps(document, compiled.value.ops);
|
|
70
|
+
if (applied.isErr()) return Result.err(applied.error);
|
|
71
|
+
document = applied.value.document;
|
|
72
|
+
ops.push(...compiled.value.ops);
|
|
73
|
+
revisions.push(...applied.value.revisions);
|
|
74
|
+
for (const id of applied.value.revisions) nextRevisionId = Math.max(nextRevisionId, id + 1);
|
|
75
|
+
}
|
|
76
|
+
return Result.ok({
|
|
77
|
+
document,
|
|
78
|
+
ops,
|
|
79
|
+
revisions,
|
|
80
|
+
nextRevisionId
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
var CanonicalPublicOperationResolutionError = class extends TaggedError("CanonicalPublicOperationResolutionError") {};
|
|
84
|
+
const canonicalPublicStory = (story) => {
|
|
85
|
+
if (story === "main") return story;
|
|
86
|
+
switch (story.type) {
|
|
87
|
+
case "header":
|
|
88
|
+
case "footer": return {
|
|
89
|
+
kind: story.type,
|
|
90
|
+
rId: story.relationshipId
|
|
91
|
+
};
|
|
92
|
+
case "footnote":
|
|
93
|
+
case "endnote": return {
|
|
94
|
+
kind: story.type,
|
|
95
|
+
id: story.noteId
|
|
96
|
+
};
|
|
97
|
+
default: return panic(`Unknown public operation story ${story}`);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const batchStatus = (batch, skippedCount) => {
|
|
101
|
+
if (batch.dryRun) return "previewed";
|
|
102
|
+
if (batch.atomic && skippedCount > 0) return "rejected";
|
|
103
|
+
return "committed";
|
|
104
|
+
};
|
|
105
|
+
/** Handles address isolated journal groups; no second document or PM history is retained. */
|
|
106
|
+
var CanonicalPublicOperations = class {
|
|
107
|
+
options;
|
|
108
|
+
handles = [];
|
|
109
|
+
nextHandle = 1;
|
|
110
|
+
handleNamespace = crypto.randomUUID();
|
|
111
|
+
constructor(options) {
|
|
112
|
+
this.options = options;
|
|
113
|
+
}
|
|
114
|
+
apply(options) {
|
|
115
|
+
const batch = parseFolioDocumentOperationBatch(options.batch);
|
|
116
|
+
const { session } = this.options;
|
|
117
|
+
const publicStory = options.story ?? "main";
|
|
118
|
+
const story = canonicalPublicStory(publicStory);
|
|
119
|
+
const state = this.options.getState(story);
|
|
120
|
+
const projected = session.projectStory(story);
|
|
121
|
+
const skipped = [];
|
|
122
|
+
const normalizations = [];
|
|
123
|
+
const refusals = /* @__PURE__ */ new Map();
|
|
124
|
+
const resolved = [];
|
|
125
|
+
const { diff } = wordDiffSessionFromOptions(options.wordDiff);
|
|
126
|
+
let nextRevisionId = options.revisionStamp?.idSeed ?? 1;
|
|
127
|
+
if (!session.isComposing && options.revisionStamp === void 0) for (const currentStory of documentStories(session.document)) {
|
|
128
|
+
const currentProjection = session.projectStory(currentStory);
|
|
129
|
+
if (currentProjection.isOk()) nextRevisionId = Math.max(nextRevisionId, maxAnnotationIdInDoc(currentProjection.value.doc) + 1);
|
|
130
|
+
}
|
|
131
|
+
const skip = (id, reason, message) => {
|
|
132
|
+
skipped.push({
|
|
133
|
+
id,
|
|
134
|
+
reason,
|
|
135
|
+
...message === void 0 ? {} : { message }
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const result = (applied, undoHandle) => {
|
|
139
|
+
const failures = new Map(skipped.map((item) => [item.id, item]));
|
|
140
|
+
const ordered = batch.operations.flatMap(({ id }) => {
|
|
141
|
+
const failure = failures.get(id);
|
|
142
|
+
if (!failure) return [];
|
|
143
|
+
const canonicalRefusal = refusals.get(id);
|
|
144
|
+
return [canonicalRefusal ? {
|
|
145
|
+
...failure,
|
|
146
|
+
canonicalRefusal
|
|
147
|
+
} : failure];
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
version: 1,
|
|
151
|
+
status: batchStatus(batch, ordered.length),
|
|
152
|
+
applied,
|
|
153
|
+
skipped: ordered,
|
|
154
|
+
issues: getFolioDocumentOperationIssues(batch.operations, ordered),
|
|
155
|
+
receipts: getFolioDocumentOperationReceiptsForStory({
|
|
156
|
+
operations: batch.operations,
|
|
157
|
+
applied,
|
|
158
|
+
story: publicStory
|
|
159
|
+
}),
|
|
160
|
+
undoHandle,
|
|
161
|
+
nextRevisionId,
|
|
162
|
+
...normalizations.length === 0 ? {} : { normalizations }
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
for (const operation of batch.operations) {
|
|
166
|
+
if (story !== OP_STORIES.MAIN) {
|
|
167
|
+
refusals.set(operation.id, { gap: CANONICAL_GAP.publicSecondaryStories });
|
|
168
|
+
skip(operation.id, "unsupportedBlock", "Canonical public secondary-story routing is unavailable.");
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!state || projected.isErr() || session.isComposing) {
|
|
172
|
+
skip(operation.id, "documentNotEditable");
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (batch.mode === "suggested") {
|
|
176
|
+
refusals.set(operation.id, { gap: CANONICAL_GAP.publicSuggestedMode });
|
|
177
|
+
skip(operation.id, "unsupportedMode", "Canonical pending suggestions are unavailable.");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (options.tableTemplates !== void 0 || options.undefinedStyles === "keep") {
|
|
181
|
+
refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
|
|
182
|
+
skip(operation.id, "unsupportedBlock", "This canonical compiler does not support the supplied execution options.");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const disposition = CANONICAL_PUBLIC_OPERATION_DISPOSITIONS[operation.type];
|
|
186
|
+
if (disposition !== "compile" || "comment" in operation && operation.comment !== void 0) {
|
|
187
|
+
refusals.set(operation.id, { gap: disposition === "compile" ? CANONICAL_GAP.publicComments : disposition });
|
|
188
|
+
skip(operation.id, "unsupportedBlock");
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const compiled = this.resolve({
|
|
192
|
+
operation,
|
|
193
|
+
options,
|
|
194
|
+
state,
|
|
195
|
+
story,
|
|
196
|
+
diff
|
|
197
|
+
});
|
|
198
|
+
if (compiled.isErr()) {
|
|
199
|
+
if (compiled.error.reason === "unsupportedBlock") refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
|
|
200
|
+
skip(operation.id, compiled.error.reason, compiled.error.message);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (resolved.some(({ from, to }) => compiled.value.from < to && compiled.value.to > from || from === to && compiled.value.from <= from && compiled.value.to >= from || compiled.value.from === compiled.value.to && compiled.value.from >= from && compiled.value.from <= to)) {
|
|
204
|
+
skip(operation.id, "overlappingOperation");
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
resolved.push(compiled.value);
|
|
208
|
+
}
|
|
209
|
+
if (batch.atomic && skipped.length > 0) {
|
|
210
|
+
for (const { operation } of resolved) skip(operation.id, "atomicBatchRejected");
|
|
211
|
+
return result([], null);
|
|
212
|
+
}
|
|
213
|
+
if (!state || projected.isErr() || session.isComposing) return result([], null);
|
|
214
|
+
resolved.sort((left, right) => right.from - left.from);
|
|
215
|
+
let document = session.document;
|
|
216
|
+
const ops = [];
|
|
217
|
+
const appliedById = /* @__PURE__ */ new Map();
|
|
218
|
+
const allocate = createEditorIntentIdAllocator();
|
|
219
|
+
const date = options.revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
220
|
+
for (const { operation, intents, normalizations: adjustments } of resolved) {
|
|
221
|
+
const compiled = compilePublicIntents({
|
|
222
|
+
document,
|
|
223
|
+
intents,
|
|
224
|
+
allocate,
|
|
225
|
+
nextRevisionId,
|
|
226
|
+
mode: batch.mode === "direct" ? "direct" : "tracked-changes",
|
|
227
|
+
author: options.author ?? "AI",
|
|
228
|
+
date
|
|
229
|
+
});
|
|
230
|
+
if (compiled.isErr()) {
|
|
231
|
+
refusals.set(operation.id, { gap: CANONICAL_GAP.publicUnsupportedInline });
|
|
232
|
+
skip(operation.id, "unsupportedBlock", compiled.error.message);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (compiled.value.ops.length === 0) {
|
|
236
|
+
skip(operation.id, "noopOperation");
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
normalizations.push(...adjustments);
|
|
240
|
+
document = compiled.value.document;
|
|
241
|
+
ops.push(...compiled.value.ops);
|
|
242
|
+
nextRevisionId = compiled.value.nextRevisionId;
|
|
243
|
+
const revisionIds = compiled.value.revisions;
|
|
244
|
+
const revisionId = revisionIds.at(0);
|
|
245
|
+
appliedById.set(operation.id, {
|
|
246
|
+
id: operation.id,
|
|
247
|
+
...revisionId === void 0 ? {} : {
|
|
248
|
+
revisionId,
|
|
249
|
+
revisionIds
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (batch.atomic && skipped.length > 0) {
|
|
254
|
+
for (const { id } of appliedById.values()) skip(id, "atomicBatchRejected");
|
|
255
|
+
return result([], null);
|
|
256
|
+
}
|
|
257
|
+
if (ops.length === 0) return result([], null);
|
|
258
|
+
const selection = projected.value.selectionAt(state);
|
|
259
|
+
const prepared = selection.isErr() ? selection : session.prepareOps(state, ops, selection.value);
|
|
260
|
+
if (prepared.isErr()) {
|
|
261
|
+
for (const { id } of appliedById.values()) {
|
|
262
|
+
refusals.set(id, { gap: CANONICAL_GAP.publicUnsupportedInline });
|
|
263
|
+
skip(id, prepared.error.reason === "noChange" ? "noopOperation" : "unsupportedBlock", prepared.error.message);
|
|
264
|
+
}
|
|
265
|
+
return result([], null);
|
|
266
|
+
}
|
|
267
|
+
const applied = batch.operations.flatMap(({ id }) => {
|
|
268
|
+
const entry = appliedById.get(id);
|
|
269
|
+
return entry ? [batch.dryRun ? { id } : entry] : [];
|
|
270
|
+
});
|
|
271
|
+
if (batch.dryRun) return result(applied, null);
|
|
272
|
+
if (!this.options.publish(prepared.value)) {
|
|
273
|
+
for (const { id } of applied) skip(id, "documentNotEditable");
|
|
274
|
+
return result([], null);
|
|
275
|
+
}
|
|
276
|
+
const handle = {
|
|
277
|
+
type: "documentOperationUndo",
|
|
278
|
+
id: `canonical-${this.handleNamespace}-${String(this.nextHandle++)}`
|
|
279
|
+
};
|
|
280
|
+
this.handles.push({
|
|
281
|
+
handle,
|
|
282
|
+
version: session.version,
|
|
283
|
+
story
|
|
284
|
+
});
|
|
285
|
+
return result(applied, handle);
|
|
286
|
+
}
|
|
287
|
+
undo(handle) {
|
|
288
|
+
const index = this.handles.findIndex(({ handle: existing }) => existing.id === handle.id && existing.type === handle.type);
|
|
289
|
+
if (index < 0) return {
|
|
290
|
+
status: "rejected",
|
|
291
|
+
undoHandle: handle,
|
|
292
|
+
reason: "unknownHandle"
|
|
293
|
+
};
|
|
294
|
+
if (index !== this.handles.length - 1) return {
|
|
295
|
+
status: "rejected",
|
|
296
|
+
undoHandle: handle,
|
|
297
|
+
reason: "notLatest"
|
|
298
|
+
};
|
|
299
|
+
const entry = this.handles.at(-1);
|
|
300
|
+
const { session } = this.options;
|
|
301
|
+
if (!entry || entry.version !== session.version) return {
|
|
302
|
+
status: "rejected",
|
|
303
|
+
undoHandle: handle,
|
|
304
|
+
reason: "documentChanged"
|
|
305
|
+
};
|
|
306
|
+
const state = this.options.getState(entry.story);
|
|
307
|
+
if (!state) return {
|
|
308
|
+
status: "rejected",
|
|
309
|
+
undoHandle: handle,
|
|
310
|
+
reason: "documentChanged"
|
|
311
|
+
};
|
|
312
|
+
const prepared = session.prepareUndo(state, entry.story);
|
|
313
|
+
if (prepared.isErr() || !this.options.publish(prepared.value)) return {
|
|
314
|
+
status: "rejected",
|
|
315
|
+
undoHandle: handle,
|
|
316
|
+
reason: "documentChanged"
|
|
317
|
+
};
|
|
318
|
+
this.handles.pop();
|
|
319
|
+
const previous = this.handles.at(-1);
|
|
320
|
+
if (previous && previous.version === entry.version - 1) previous.version = session.version;
|
|
321
|
+
return {
|
|
322
|
+
status: "undone",
|
|
323
|
+
undoHandle: handle
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
resolve({ operation, options, state, story, diff }) {
|
|
327
|
+
const refusal = (reason, message) => Result.err(new CanonicalPublicOperationResolutionError({
|
|
328
|
+
reason,
|
|
329
|
+
message: message ?? `The operation was refused: ${reason}.`
|
|
330
|
+
}));
|
|
331
|
+
if ("comment" in operation) return refusal("unsupportedBlock", "Canonical comment operations are unavailable.");
|
|
332
|
+
switch (operation.type) {
|
|
333
|
+
case "replaceInBlock":
|
|
334
|
+
case "replaceRange":
|
|
335
|
+
case "replaceBlock":
|
|
336
|
+
case "splitBlock":
|
|
337
|
+
case "formatRange":
|
|
338
|
+
case "mergeBlockWithNext": break;
|
|
339
|
+
default: return refusal("unsupportedBlock", `Canonical public operation ${operation.type} is unavailable.`);
|
|
340
|
+
}
|
|
341
|
+
const blockId = operation.type === "replaceRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId;
|
|
342
|
+
const anchor = Object.hasOwn(options.snapshot.anchors, blockId) ? options.snapshot.anchors[blockId] : void 0;
|
|
343
|
+
const projection = this.options.session.projectStory(story);
|
|
344
|
+
if (!anchor || projection.isErr()) return refusal("missingBlock");
|
|
345
|
+
const paragraph = projection.value.paragraph(blockId);
|
|
346
|
+
if (!paragraph) return refusal("missingBlock");
|
|
347
|
+
if (paragraph.source.pPrMark?.kind === "del" || paragraph.source.pPrMark?.kind === "moveFrom") return refusal("pendingDeletion");
|
|
348
|
+
const clean = buildCleanBlockText(paragraph.node, paragraph.start - 1, {
|
|
349
|
+
fieldResults: "text",
|
|
350
|
+
noteReferences: collectNoteReferenceLabels(state.doc)
|
|
351
|
+
});
|
|
352
|
+
const hash = hashFolioAIBlockText(clean.text);
|
|
353
|
+
if (operation.precondition && hash !== operation.precondition.blockTextHash) return refusal("preconditionFailed");
|
|
354
|
+
if (hash !== anchor.textHash) return refusal("changedBlock");
|
|
355
|
+
let start = 0;
|
|
356
|
+
let end = clean.text.length;
|
|
357
|
+
let text = "";
|
|
358
|
+
switch (operation.type) {
|
|
359
|
+
case "replaceInBlock":
|
|
360
|
+
if (operation.find === "") return refusal("emptyOperation");
|
|
361
|
+
start = clean.text.indexOf(operation.find);
|
|
362
|
+
if (start < 0) return refusal("missingFind");
|
|
363
|
+
if (clean.text.indexOf(operation.find, start + 1) >= 0) return refusal("ambiguousFind");
|
|
364
|
+
end = start + operation.find.length;
|
|
365
|
+
text = operation.replace;
|
|
366
|
+
break;
|
|
367
|
+
case "replaceRange":
|
|
368
|
+
case "formatRange":
|
|
369
|
+
start = operation.range.startOffset;
|
|
370
|
+
end = operation.range.endOffset;
|
|
371
|
+
if (hashFolioAIBlockText(clean.text.slice(start, end)) !== operation.range.selectedTextHash) return refusal("staleRange");
|
|
372
|
+
text = operation.type === "replaceRange" ? operation.replace : "";
|
|
373
|
+
break;
|
|
374
|
+
case "replaceBlock":
|
|
375
|
+
if (operation.styleId !== void 0 || operation.preserveFormatting === false) return refusal("unsupportedBlock", "Canonical replacement paragraph properties are unavailable.");
|
|
376
|
+
text = operation.text;
|
|
377
|
+
break;
|
|
378
|
+
case "splitBlock":
|
|
379
|
+
if (operation.firstParagraphProperties || operation.secondParagraphProperties) return refusal("unsupportedBlock", "Canonical split paragraph properties are unavailable.");
|
|
380
|
+
start = operation.offset;
|
|
381
|
+
end = start + (operation.separator?.length ?? 0);
|
|
382
|
+
if (start <= 0 || start >= clean.text.length || clean.text.slice(start, end) !== (operation.separator ?? "")) return refusal("staleRange");
|
|
383
|
+
break;
|
|
384
|
+
case "mergeBlockWithNext":
|
|
385
|
+
if (operation.mergedParagraphProperties || operation.separator) return refusal("unsupportedBlock", "Canonical public join properties and separators are unavailable.");
|
|
386
|
+
start = clean.text.length;
|
|
387
|
+
end = start;
|
|
388
|
+
break;
|
|
389
|
+
default: return operation;
|
|
390
|
+
}
|
|
391
|
+
if ([start, end].some((offset) => splitsSurrogatePair(clean.text, offset) || splitsGraphemeCluster(clean.text, offset))) return refusal("splitsCharacter");
|
|
392
|
+
if (clean.structuralBoundaries.some((boundary) => boundary.type === "noteReference" && start < boundary.offset + boundary.length && end > boundary.offset)) return refusal("protectedReference");
|
|
393
|
+
if (stripInlineEmphasisMarkers(text) !== text) return refusal("unsupportedBlock", "Canonical public emphasis replacements are unavailable.");
|
|
394
|
+
if (/\[\^(?:e)?\d+\]/u.test(text) || /[\t\r\n]/u.test(text)) return refusal("unsupportedBlock", "Canonical public replacements require plain text.");
|
|
395
|
+
const range = resolveCleanTextRange({
|
|
396
|
+
cleanBlock: clean,
|
|
397
|
+
startOffset: start,
|
|
398
|
+
endOffset: end
|
|
399
|
+
});
|
|
400
|
+
if (!range) return refusal("staleRange");
|
|
401
|
+
const from = projection.value.addressAt(range.from);
|
|
402
|
+
const to = projection.value.addressAt(range.to);
|
|
403
|
+
if (from.isErr() || to.isErr()) return refusal("unsupportedBlock");
|
|
404
|
+
if (operation.type !== "splitBlock" && operation.type !== "formatRange" && operation.type !== "mergeBlockWithNext" && clean.text.slice(start, end) === text) return refusal("noopOperation");
|
|
405
|
+
let intent;
|
|
406
|
+
switch (operation.type) {
|
|
407
|
+
case "splitBlock":
|
|
408
|
+
intent = {
|
|
409
|
+
type: "splitParagraph",
|
|
410
|
+
at: from.value,
|
|
411
|
+
to: to.value,
|
|
412
|
+
newBlockId: createEditorIntentIdAllocator()(this.options.session.document, {
|
|
413
|
+
type: "splitParagraph",
|
|
414
|
+
at: from.value,
|
|
415
|
+
to: to.value
|
|
416
|
+
}).newBlockId
|
|
417
|
+
};
|
|
418
|
+
break;
|
|
419
|
+
case "mergeBlockWithNext": {
|
|
420
|
+
const body = this.options.session.document.package.document.content;
|
|
421
|
+
if (story !== "main") return refusal("unsupportedBlock", "Canonical public joins currently require the main story.");
|
|
422
|
+
const next = body.at(body.findIndex((block) => block.type === "paragraph" && block.paraId === blockId) + 1);
|
|
423
|
+
if (next?.type !== "paragraph" || !next.paraId) return refusal("missingBlock");
|
|
424
|
+
intent = {
|
|
425
|
+
type: "joinParagraphs",
|
|
426
|
+
story,
|
|
427
|
+
blockId,
|
|
428
|
+
nextBlockId: next.paraId
|
|
429
|
+
};
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case "formatRange": {
|
|
433
|
+
const formatting = operation.formatting;
|
|
434
|
+
const patch = {
|
|
435
|
+
...formatting.bold === void 0 ? {} : { bold: formatting.bold },
|
|
436
|
+
...formatting.italic === void 0 ? {} : { italic: formatting.italic },
|
|
437
|
+
...formatting.underline === void 0 ? {} : { underline: formatting.underline === null ? null : { style: formatting.underline ? "single" : "none" } },
|
|
438
|
+
...formatting.strike === void 0 ? {} : {
|
|
439
|
+
strike: formatting.strike,
|
|
440
|
+
doubleStrike: null
|
|
441
|
+
},
|
|
442
|
+
...formatting.fontSizePt === void 0 ? {} : { fontSize: formatting.fontSizePt === null ? null : formatting.fontSizePt * 2 },
|
|
443
|
+
...formatting.fontFamily === void 0 ? {} : { fontFamily: formatting.fontFamily === null ? null : {
|
|
444
|
+
ascii: formatting.fontFamily,
|
|
445
|
+
hAnsi: formatting.fontFamily
|
|
446
|
+
} },
|
|
447
|
+
...formatting.color === void 0 ? {} : { color: formatting.color === null ? null : { rgb: formatting.color.replace(/^#/u, "").toUpperCase() } }
|
|
448
|
+
};
|
|
449
|
+
intent = {
|
|
450
|
+
type: "formatRun",
|
|
451
|
+
from: from.value,
|
|
452
|
+
to: to.value,
|
|
453
|
+
patch
|
|
454
|
+
};
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "replaceInBlock":
|
|
458
|
+
case "replaceRange":
|
|
459
|
+
case "replaceBlock":
|
|
460
|
+
intent = {
|
|
461
|
+
type: "replaceText",
|
|
462
|
+
from: from.value,
|
|
463
|
+
to: to.value,
|
|
464
|
+
text,
|
|
465
|
+
...options.replacementBackground === "keep" ? {} : { runPropsPatch: {
|
|
466
|
+
highlight: null,
|
|
467
|
+
shading: null
|
|
468
|
+
} }
|
|
469
|
+
};
|
|
470
|
+
break;
|
|
471
|
+
default: return panic(`Unknown canonical public operation ${operation}`);
|
|
472
|
+
}
|
|
473
|
+
const intents = [];
|
|
474
|
+
const normalizations = [];
|
|
475
|
+
if (intent.type === "replaceText") {
|
|
476
|
+
let offset = start;
|
|
477
|
+
let pending = null;
|
|
478
|
+
const changes = [];
|
|
479
|
+
for (const segment of diff(clean.text.slice(start, end), text)) switch (segment.type) {
|
|
480
|
+
case "equal":
|
|
481
|
+
if (pending) changes.push(pending);
|
|
482
|
+
pending = null;
|
|
483
|
+
offset += segment.text.length;
|
|
484
|
+
break;
|
|
485
|
+
case "del":
|
|
486
|
+
pending ??= {
|
|
487
|
+
start: offset,
|
|
488
|
+
end: offset,
|
|
489
|
+
text: ""
|
|
490
|
+
};
|
|
491
|
+
offset += segment.text.length;
|
|
492
|
+
pending.end = offset;
|
|
493
|
+
break;
|
|
494
|
+
case "ins":
|
|
495
|
+
pending ??= {
|
|
496
|
+
start: offset,
|
|
497
|
+
end: offset,
|
|
498
|
+
text: ""
|
|
499
|
+
};
|
|
500
|
+
pending.text += segment.text;
|
|
501
|
+
break;
|
|
502
|
+
default: {
|
|
503
|
+
const unreachable = segment.type;
|
|
504
|
+
return panic(`Unknown replacement diff ${unreachable}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (pending) changes.push(pending);
|
|
508
|
+
for (const change of changes.toReversed()) {
|
|
509
|
+
const gap = resolveCleanTextRange({
|
|
510
|
+
cleanBlock: clean,
|
|
511
|
+
startOffset: change.start,
|
|
512
|
+
endOffset: change.end
|
|
513
|
+
});
|
|
514
|
+
if (!gap) return refusal("unsupportedBlock");
|
|
515
|
+
if (change.text.length > 0 && gap.from < gap.to) {
|
|
516
|
+
let first = null;
|
|
517
|
+
let uniform = true;
|
|
518
|
+
paragraph.node.nodesBetween(gap.from - paragraph.start, gap.to - paragraph.start, (node) => {
|
|
519
|
+
if (!node.isText || node.marks.some((mark) => mark.type.name === "deletion")) return;
|
|
520
|
+
const marks = node.marks.filter((mark) => mark.type.name !== "insertion");
|
|
521
|
+
if (first === null) first = marks;
|
|
522
|
+
else if (!Mark.sameSet(first, marks)) uniform = false;
|
|
523
|
+
});
|
|
524
|
+
if (!uniform && normalizations.length === 0) normalizations.push({
|
|
525
|
+
id: operation.id,
|
|
526
|
+
code: "uniformReplacementFormatting"
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const changeFrom = projection.value.addressAt(gap.from);
|
|
530
|
+
const changeTo = projection.value.addressAt(gap.to);
|
|
531
|
+
if (changeFrom.isErr() || changeTo.isErr()) return refusal("unsupportedBlock");
|
|
532
|
+
intents.push({
|
|
533
|
+
type: "replaceText",
|
|
534
|
+
from: changeFrom.value,
|
|
535
|
+
to: changeTo.value,
|
|
536
|
+
text: change.text,
|
|
537
|
+
...intent.runPropsPatch === void 0 ? {} : { runPropsPatch: intent.runPropsPatch }
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} else intents.push(intent);
|
|
541
|
+
const structural = operation.type === "splitBlock" || operation.type === "mergeBlockWithNext";
|
|
542
|
+
const nextParagraph = operation.type === "mergeBlockWithNext" && intent.type === "joinParagraphs" ? projection.value.paragraph(intent.nextBlockId) : void 0;
|
|
543
|
+
let claimedTo = range.to;
|
|
544
|
+
if (structural) claimedTo = paragraph.start + paragraph.node.content.size;
|
|
545
|
+
if (nextParagraph) claimedTo = nextParagraph.start + nextParagraph.node.content.size;
|
|
546
|
+
return Result.ok({
|
|
547
|
+
operation,
|
|
548
|
+
intents,
|
|
549
|
+
normalizations,
|
|
550
|
+
from: structural ? paragraph.start : range.from,
|
|
551
|
+
to: claimedTo
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
//#endregion
|
|
556
|
+
export { CANONICAL_PUBLIC_OPERATION_DISPOSITIONS, CanonicalPublicOperations };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { CanonicalGap } from "../types/canonicalCapabilities.js";
|
|
3
|
+
import { CanonicalSaveSnapshot } from "../types/canonicalSave.js";
|
|
4
|
+
import { CanonicalInlineGap } from "./canonicalInlineProjection.js";
|
|
2
5
|
import { Node } from "prosemirror-model";
|
|
3
6
|
import { EditorState, Transaction } from "prosemirror-state";
|
|
4
7
|
import { Result } from "better-result";
|
|
@@ -7,9 +10,11 @@ import { DocumentOp, EditorIntent, OpStory, TextPosition, TouchedBlocks } from "
|
|
|
7
10
|
declare const CanonicalSessionError_base: import("better-result").TaggedErrorClass<"CanonicalSessionError">;
|
|
8
11
|
declare class CanonicalSessionError extends CanonicalSessionError_base<{
|
|
9
12
|
message: string;
|
|
13
|
+
gap: CanonicalGap;
|
|
10
14
|
reason: "refused" | "noChange";
|
|
11
15
|
}> {}
|
|
12
16
|
type CanonicalSelection = {
|
|
17
|
+
type: "text" | "all";
|
|
13
18
|
anchor: TextPosition;
|
|
14
19
|
head: TextPosition;
|
|
15
20
|
};
|
|
@@ -25,7 +30,7 @@ type ParagraphAddress = {
|
|
|
25
30
|
blockId: string;
|
|
26
31
|
start: number;
|
|
27
32
|
text: string;
|
|
28
|
-
boundaries: readonly (readonly
|
|
33
|
+
boundaries: readonly (readonly CanonicalInlineGap[])[];
|
|
29
34
|
node: Node;
|
|
30
35
|
source: document_d_exports.Paragraph;
|
|
31
36
|
};
|
|
@@ -34,7 +39,7 @@ type CanonicalProjectionOptions = {
|
|
|
34
39
|
paragraphs: readonly ParagraphAddress[];
|
|
35
40
|
story: OpStory;
|
|
36
41
|
};
|
|
37
|
-
/** Paragraph gaps
|
|
42
|
+
/** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
|
|
38
43
|
declare class CanonicalProjection {
|
|
39
44
|
readonly doc: Node;
|
|
40
45
|
readonly story: OpStory;
|
|
@@ -82,14 +87,22 @@ type CanonicalReplaceTextInput = {
|
|
|
82
87
|
to: number;
|
|
83
88
|
text: string;
|
|
84
89
|
story?: OpStory;
|
|
85
|
-
semantic?: CanonicalInputSemantic;
|
|
86
90
|
time?: number;
|
|
87
|
-
}
|
|
91
|
+
} & ({
|
|
92
|
+
semantic: "composition";
|
|
93
|
+
compositionPhase?: "correction";
|
|
94
|
+
} | {
|
|
95
|
+
semantic?: Exclude<CanonicalInputSemantic, "composition">;
|
|
96
|
+
compositionPhase?: never;
|
|
97
|
+
});
|
|
88
98
|
/** Immutable model authority with a journal staged independently of the PM view. */
|
|
89
99
|
declare class CanonicalSession {
|
|
90
100
|
private currentDocument;
|
|
91
101
|
private currentProjection;
|
|
102
|
+
private readonly sourceBlockIds;
|
|
92
103
|
private currentVersion;
|
|
104
|
+
private readonly saveTouched;
|
|
105
|
+
private saveStructure;
|
|
93
106
|
private readonly allocateIntentIds;
|
|
94
107
|
private mode;
|
|
95
108
|
private readonly allocatedBlockIds;
|
|
@@ -111,6 +124,7 @@ declare class CanonicalSession {
|
|
|
111
124
|
private intentNeedsIdentityIds;
|
|
112
125
|
private rememberSources;
|
|
113
126
|
get document(): document_d_exports.Document;
|
|
127
|
+
captureSaveSnapshot(): CanonicalSaveSnapshot;
|
|
114
128
|
get projection(): CanonicalProjection;
|
|
115
129
|
get selection(): CanonicalSelection | null;
|
|
116
130
|
get version(): number;
|
|
@@ -121,9 +135,13 @@ declare class CanonicalSession {
|
|
|
121
135
|
beginComposition(): Result<void, CanonicalSessionError>;
|
|
122
136
|
endComposition(): void;
|
|
123
137
|
private checkState;
|
|
124
|
-
prepareReplace(state: EditorState, { from, to, text, story, semantic, time }: CanonicalReplaceTextInput): Result<CanonicalCommit, CanonicalSessionError>;
|
|
138
|
+
prepareReplace(state: EditorState, { from, to, text, story, semantic, compositionPhase, time }: CanonicalReplaceTextInput): Result<CanonicalCommit, CanonicalSessionError>;
|
|
125
139
|
prepareIntent(state: EditorState, intent: EditorIntent): Result<CanonicalCommit, CanonicalSessionError>;
|
|
126
|
-
prepareIntents(state: EditorState, intents
|
|
140
|
+
prepareIntents(state: EditorState, { intents, resourceOps, semantic }: {
|
|
141
|
+
intents: readonly EditorIntent[];
|
|
142
|
+
resourceOps?: readonly DocumentOp[];
|
|
143
|
+
semantic?: CanonicalInputSemantic;
|
|
144
|
+
}): Result<CanonicalCommit, CanonicalSessionError>;
|
|
127
145
|
prepareOps(state: EditorState, ops: readonly DocumentOp[], postSelection: CanonicalSelection): Result<CanonicalCommit, CanonicalSessionError>;
|
|
128
146
|
private prepareJournalledOps;
|
|
129
147
|
prepareSplit(state: EditorState): Result<CanonicalCommit, CanonicalSessionError>;
|