@stll/folio-core 0.54.2 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/apply.js +22 -1
- package/dist/ai-edits/headless.js +1 -0
- package/dist/compare/content-types.d.ts +3 -15
- package/dist/controller/canonicalComposition.d.ts +36 -0
- package/dist/controller/canonicalComposition.js +168 -0
- package/dist/controller/canonicalInput.d.ts +45 -0
- package/dist/controller/canonicalInput.js +338 -0
- package/dist/controller/canonicalOperations.d.ts +22 -0
- package/dist/controller/canonicalOperations.js +85 -0
- package/dist/controller/canonicalReview.d.ts +12 -0
- package/dist/controller/canonicalReview.js +19 -0
- package/dist/controller/canonicalSession.d.ts +149 -0
- package/dist/controller/canonicalSession.js +1038 -0
- package/dist/controller/canonicalStoryEditor.d.ts +34 -0
- package/dist/controller/canonicalStoryEditor.js +82 -0
- package/dist/controller/canonicalStructure.d.ts +16 -0
- package/dist/controller/canonicalStructure.js +269 -0
- package/dist/controller/folioEditor.d.ts +1 -1
- package/dist/controller/folioEditor.js +9 -0
- package/dist/controller/headerFooterEditorManager.d.ts +4 -0
- package/dist/controller/headerFooterEditorManager.js +71 -3
- package/dist/controller/hiddenEditorApi.d.ts +35 -0
- package/dist/controller/hiddenEditorApi.js +25 -8
- package/dist/controller/hiddenEditorManager.d.ts +11 -2
- package/dist/controller/hiddenEditorManager.js +456 -30
- package/dist/controller/layoutPipeline.d.ts +1 -1
- package/dist/controller/layoutPipeline.js +2 -0
- package/dist/controller/noteEditorManager.d.ts +4 -0
- package/dist/controller/noteEditorManager.js +113 -15
- package/dist/docx/attributeRemainder.js +1 -0
- package/dist/docx/canonicalSessionInput.d.ts +11 -0
- package/dist/docx/canonicalSessionInput.js +27 -0
- package/dist/docx/canonicalStoryRepack.d.ts +10 -0
- package/dist/docx/canonicalStoryRepack.js +41 -0
- package/dist/docx/contentTypeRegistry.d.ts +8 -0
- package/dist/docx/contentTypeRegistry.js +64 -0
- package/dist/docx/ensureParaIds.d.ts +7 -1
- package/dist/docx/ensureParaIds.js +163 -211
- package/dist/docx/foldedListNumberFields.d.ts +85 -0
- package/dist/docx/foldedListNumberFields.js +267 -0
- package/dist/docx/headerFooterVerbatim.d.ts +14 -1
- package/dist/docx/headerFooterVerbatim.js +95 -2
- package/dist/docx/noteIds.d.ts +5 -0
- package/dist/docx/noteIds.js +6 -2
- package/dist/docx/noteReferenceMark.d.ts +3 -1
- package/dist/docx/noteReferenceMark.js +15 -14
- package/dist/docx/numberingParser.d.ts +3 -1
- package/dist/docx/numberingParser.js +17 -3
- package/dist/docx/packageParts.d.ts +3 -1
- package/dist/docx/packageParts.js +65 -16
- package/dist/docx/paragraphMarkPropertyPresence.d.ts +9 -0
- package/dist/docx/paragraphMarkPropertyPresence.js +21 -0
- package/dist/docx/paragraphParser.js +101 -53
- package/dist/docx/paragraphPropertySource.d.ts +15 -1
- package/dist/docx/paragraphPropertySource.js +76 -22
- package/dist/docx/parser.js +2 -1
- package/dist/docx/reviewHistoryNamespace.d.ts +4 -0
- package/dist/docx/reviewHistoryNamespace.js +4 -0
- package/dist/docx/reviewResolutionProvenance.d.ts +25 -0
- package/dist/docx/reviewResolutionProvenance.js +153 -0
- package/dist/docx/rezip.d.ts +7 -3
- package/dist/docx/rezip.js +107 -45
- package/dist/docx/runConsolidator.d.ts +2 -58
- package/dist/docx/runConsolidator.js +3 -175
- package/dist/docx/runParser.d.ts +2 -1
- package/dist/docx/runParser.js +156 -116
- package/dist/docx/saveDiagnostics.d.ts +11 -0
- package/dist/docx/saveDiagnostics.js +0 -0
- package/dist/docx/selectiveSave.d.ts +2 -1
- package/dist/docx/selectiveSave.js +4 -1
- package/dist/docx/selectiveXmlPatch.d.ts +16 -8
- package/dist/docx/selectiveXmlPatch.js +74 -2
- package/dist/docx/serializer/headerFooterSerializer.d.ts +5 -1
- package/dist/docx/serializer/headerFooterSerializer.js +19 -11
- package/dist/docx/serializer/noteSerializer.d.ts +2 -2
- package/dist/docx/serializer/noteSerializer.js +24 -8
- package/dist/docx/serializer/paragraphSerializer.js +41 -4
- package/dist/docx/serializer/partNamespaces.js +2 -1
- package/dist/docx/serializer/runSerializer.js +3 -1
- package/dist/docx/settingsHeaderFooterUpdate.d.ts +5 -0
- package/dist/docx/settingsHeaderFooterUpdate.js +90 -0
- package/dist/docx/settingsParser.d.ts +1 -2
- package/dist/docx/settingsParser.js +5 -6
- package/dist/docx/storyBlockReplay.d.ts +11 -0
- package/dist/docx/storyBlockReplay.js +367 -0
- package/dist/docx/streamingXmlParser.d.ts +22 -1
- package/dist/docx/streamingXmlParser.js +49 -3
- package/dist/docx/structuralXmlPatch.js +8 -1
- package/dist/docx/verbatimCapture.d.ts +3 -1
- package/dist/docx/verbatimCapture.js +49 -6
- package/dist/i18n/messages/ar.gen.d.ts +5 -0
- package/dist/i18n/messages/ar.gen.js +656 -0
- package/dist/i18n/messages/cs.gen.d.ts +5 -0
- package/dist/i18n/messages/cs.gen.js +656 -0
- package/dist/i18n/messages/de.gen.d.ts +5 -0
- package/dist/i18n/messages/de.gen.js +656 -0
- package/dist/i18n/messages/en.gen.d.ts +5 -0
- package/dist/i18n/messages/en.gen.js +656 -0
- package/dist/i18n/messages/es.gen.d.ts +5 -0
- package/dist/i18n/messages/es.gen.js +656 -0
- package/dist/i18n/messages/et.gen.d.ts +5 -0
- package/dist/i18n/messages/et.gen.js +656 -0
- package/dist/i18n/messages/fr.gen.d.ts +5 -0
- package/dist/i18n/messages/fr.gen.js +656 -0
- package/dist/i18n/messages/he.gen.d.ts +5 -0
- package/dist/i18n/messages/he.gen.js +656 -0
- package/dist/i18n/messages/hi.gen.d.ts +5 -0
- package/dist/i18n/messages/hi.gen.js +656 -0
- package/dist/i18n/messages/hu.gen.d.ts +5 -0
- package/dist/i18n/messages/hu.gen.js +656 -0
- package/dist/i18n/messages/locales.d.ts +5 -0
- package/dist/i18n/messages/locales.js +22 -0
- package/dist/i18n/messages/lt.gen.d.ts +5 -0
- package/dist/i18n/messages/lt.gen.js +656 -0
- package/dist/i18n/messages/lv.gen.d.ts +5 -0
- package/dist/i18n/messages/lv.gen.js +656 -0
- package/dist/i18n/messages/pl.gen.d.ts +5 -0
- package/dist/i18n/messages/pl.gen.js +656 -0
- package/dist/i18n/messages/pt-BR.gen.d.ts +5 -0
- package/dist/i18n/messages/pt-BR.gen.js +656 -0
- package/dist/i18n/messages/sk.gen.d.ts +5 -0
- package/dist/i18n/messages/sk.gen.js +656 -0
- package/dist/i18n/messages/tr.gen.d.ts +5 -0
- package/dist/i18n/messages/tr.gen.js +656 -0
- package/dist/i18n/messages/zh-CN.gen.d.ts +5 -0
- package/dist/i18n/messages/zh-CN.gen.js +656 -0
- package/dist/i18n/messages.d.ts +2 -3
- package/dist/i18n/messages.js +1 -19
- package/dist/internal/noteMarkerAttrs.d.ts +8 -0
- package/dist/internal/noteMarkerAttrs.js +16 -0
- package/dist/internal/pageBreakRunSourceDescendantIndex.js +1 -0
- package/dist/internal/prosemirrorAttrBoundary.d.ts +21 -0
- package/dist/internal/prosemirrorAttrBoundary.js +50 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +3 -3
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +1 -0
- package/dist/layout-bridge/convert/noteReferences.d.ts +1 -3
- package/dist/layout-bridge/convert/noteReferences.js +1 -6
- package/dist/layout-bridge/convert/paragraphRuns.js +2 -4
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -2
- package/dist/managers/DocumentLoaderManager.d.ts +1 -0
- package/dist/managers/DocumentLoaderManager.js +8 -1
- package/dist/markdown/renderBlock.js +15 -1
- package/dist/markdown/renderParagraph.js +9 -13
- package/dist/markdown/renderRuns.js +3 -2
- package/dist/markdown/renderTable.js +2 -0
- package/dist/markdown/types.d.ts +3 -9
- package/dist/prosemirror/attrs/index.d.ts +2 -12
- package/dist/prosemirror/attrs/index.js +22 -48
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +2 -1
- package/dist/prosemirror/canonicalCommands.d.ts +97 -0
- package/dist/prosemirror/canonicalCommands.js +28 -0
- package/dist/prosemirror/commands/pageBreak.js +15 -1
- package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
- package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
- package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -3
- package/dist/prosemirror/conversion/toProseDoc.js +51 -11
- package/dist/prosemirror/executeEditorCommand.d.ts +11 -0
- package/dist/prosemirror/executeEditorCommand.js +26 -0
- package/dist/prosemirror/extensions/StarterKit.js +2 -0
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +50 -19
- package/dist/prosemirror/extensions/features/ListExtension.js +18 -5
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +5 -2
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +13 -1
- package/dist/prosemirror/extensions/marks/TextColorExtension.js +3 -2
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
- package/dist/prosemirror/extensions/marks/markUtils.js +70 -8
- package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.d.ts +6 -0
- package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.js +36 -0
- package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +4 -2
- package/dist/prosemirror/listMarker.d.ts +1 -0
- package/dist/prosemirror/listMarker.js +16 -7
- package/dist/prosemirror/listNumbering.js +26 -5
- package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
- package/dist/prosemirror/listRenderingAttrs.js +3 -0
- package/dist/prosemirror/markupViewNotes.js +3 -2
- package/dist/prosemirror/moveRangeBoundaryAttrs.d.ts +2 -1
- package/dist/prosemirror/noteReferenceReview.js +5 -3
- package/dist/prosemirror/plugins/suggestionMode.js +50 -2
- package/dist/prosemirror/rangeAnchorAttrs.d.ts +2 -1
- package/dist/prosemirror/runFormattingFromMarks.d.ts +4 -1
- package/dist/prosemirror/runFormattingFromMarks.js +15 -1
- package/dist/prosemirror/runFormattingInlineCarriers.d.ts +2 -1
- package/dist/prosemirror/runFormattingInlineCarriers.js +4 -1
- package/dist/prosemirror/schema/marks.d.ts +2 -0
- package/dist/prosemirror/schema/nodes.d.ts +10 -2
- package/dist/prosemirror/textBoxAnchorAttrs.d.ts +2 -1
- package/dist/prosemirror/textInput.js +5 -1
- package/dist/prosemirror/trackedRevisionPath.js +2 -1
- package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -0
- package/dist/prosemirror/trackedRunInlineAtoms.js +1 -0
- package/dist/prosemirror/validation.js +4 -0
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/createDocument.js +2 -2
- package/package.json +14 -2
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
|
|
2
|
+
import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
|
|
3
|
+
import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
|
|
4
|
+
import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
|
|
5
|
+
import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
|
|
6
|
+
import { Result, TaggedError, panic } from "better-result";
|
|
7
|
+
import { hasIllegalXmlCharacters } from "@stll/docx-core";
|
|
8
|
+
import { Fragment } from "prosemirror-model";
|
|
9
|
+
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";
|
|
11
|
+
//#region src/controller/canonicalSession.ts
|
|
12
|
+
var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
|
|
13
|
+
const refuse = (message) => Result.err(new CanonicalSessionError({
|
|
14
|
+
message,
|
|
15
|
+
reason: "refused"
|
|
16
|
+
}));
|
|
17
|
+
const noChange = (message) => Result.err(new CanonicalSessionError({
|
|
18
|
+
message,
|
|
19
|
+
reason: "noChange"
|
|
20
|
+
}));
|
|
21
|
+
const operationChangesPackage = (op) => {
|
|
22
|
+
switch (op.type) {
|
|
23
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
24
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
25
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
26
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
27
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
28
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
29
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
30
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
31
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
32
|
+
default:
|
|
33
|
+
if ("story" in op) return !sameStory(op.story, OP_STORIES.MAIN);
|
|
34
|
+
if ("at" in op) return !sameStory(op.at.story, OP_STORIES.MAIN);
|
|
35
|
+
if ("from" in op) return !sameStory(op.from.story, OP_STORIES.MAIN);
|
|
36
|
+
return op;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const operationChangesBodyProjection = (op) => {
|
|
40
|
+
switch (op.type) {
|
|
41
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
42
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
43
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
44
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
45
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
46
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
47
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
48
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
49
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
50
|
+
default:
|
|
51
|
+
if ("story" in op) return sameStory(op.story, OP_STORIES.MAIN);
|
|
52
|
+
if ("at" in op) return sameStory(op.at.story, OP_STORIES.MAIN);
|
|
53
|
+
if ("from" in op) return sameStory(op.from.story, OP_STORIES.MAIN);
|
|
54
|
+
return op;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
|
|
58
|
+
/** Paragraph gaps map logical UTF-16 units and zero-width note marks to PM positions. */
|
|
59
|
+
var CanonicalProjection = class {
|
|
60
|
+
doc;
|
|
61
|
+
story;
|
|
62
|
+
paragraphs;
|
|
63
|
+
paragraphsById;
|
|
64
|
+
constructor({ doc, paragraphs, story }) {
|
|
65
|
+
this.story = story;
|
|
66
|
+
this.doc = doc;
|
|
67
|
+
this.paragraphs = paragraphs;
|
|
68
|
+
this.paragraphsById = new Map(paragraphs.map((paragraph) => [paragraph.blockId, paragraph]));
|
|
69
|
+
}
|
|
70
|
+
addressAt(position) {
|
|
71
|
+
if (!Number.isInteger(position)) return refuse("The input position is not an integer.");
|
|
72
|
+
const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
|
|
73
|
+
if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
|
|
74
|
+
const relative = position - paragraph.start;
|
|
75
|
+
const offset = paragraph.boundaries.findIndex((gaps) => gaps.includes(relative));
|
|
76
|
+
if (offset < 0) return refuse("The input splits a note reference.");
|
|
77
|
+
if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
|
|
78
|
+
const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
|
|
79
|
+
return Result.ok({
|
|
80
|
+
story: this.story,
|
|
81
|
+
blockId: paragraph.blockId,
|
|
82
|
+
offset,
|
|
83
|
+
...gaps.length > 1 ? { zeroWidthBefore: gaps.indexOf(relative) } : {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
inputAddressAt(position) {
|
|
87
|
+
const address = this.addressAt(position);
|
|
88
|
+
if (address.isErr()) return address;
|
|
89
|
+
const paragraph = this.paragraph(address.value.blockId);
|
|
90
|
+
if (paragraph === void 0) panic("Input lost its paragraph.");
|
|
91
|
+
let offset = address.value.offset;
|
|
92
|
+
paragraph.node.forEach((node, start) => {
|
|
93
|
+
if (node.marks.some((mark) => mark.type.name === "deletion") && offset > start && offset < start + node.nodeSize) offset = start + node.nodeSize;
|
|
94
|
+
});
|
|
95
|
+
return Result.ok({
|
|
96
|
+
...address.value,
|
|
97
|
+
offset
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
positionAt(address) {
|
|
101
|
+
const paragraph = this.paragraph(address.blockId);
|
|
102
|
+
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
|
+
const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
|
|
104
|
+
const gapIndex = address.zeroWidthBefore ?? gaps.length - 1;
|
|
105
|
+
if (!Number.isInteger(gapIndex) || gapIndex < 0 || gapIndex >= gaps.length) return refuse("The canonical selection is outside a paragraph gap.");
|
|
106
|
+
return Result.ok(paragraph.start + (gaps.at(gapIndex) ?? panic("Missing canonical gap")));
|
|
107
|
+
}
|
|
108
|
+
selectionAt(state) {
|
|
109
|
+
if (!(state.selection instanceof TextSelection)) return refuse("Canonical input requires a text selection.");
|
|
110
|
+
const anchor = this.addressAt(state.selection.anchor);
|
|
111
|
+
if (anchor.isErr()) return anchor;
|
|
112
|
+
const head = this.addressAt(state.selection.head);
|
|
113
|
+
if (head.isErr()) return head;
|
|
114
|
+
return Result.ok({
|
|
115
|
+
anchor: anchor.value,
|
|
116
|
+
head: head.value
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
paragraph(blockId) {
|
|
120
|
+
return this.paragraphsById.get(blockId);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** Immutable operations retain content identities for every untouched story. */
|
|
124
|
+
const changedStories = ({ document, previous }) => {
|
|
125
|
+
const stories = [];
|
|
126
|
+
if (document.package.document.content !== previous.package.document.content) stories.push(OP_STORIES.MAIN);
|
|
127
|
+
for (const kind of ["header", "footer"]) {
|
|
128
|
+
const parts = kind === "header" ? document.package.headers : document.package.footers;
|
|
129
|
+
const prior = kind === "header" ? previous.package.headers : previous.package.footers;
|
|
130
|
+
if (parts === prior) continue;
|
|
131
|
+
for (const [rId, body] of parts ?? []) if (body.content !== prior?.get(rId)?.content) stories.push({
|
|
132
|
+
kind,
|
|
133
|
+
rId
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const kind of ["footnote", "endnote"]) {
|
|
137
|
+
const notes = kind === "footnote" ? document.package.footnotes : document.package.endnotes;
|
|
138
|
+
const prior = kind === "footnote" ? previous.package.footnotes : previous.package.endnotes;
|
|
139
|
+
if (notes === prior) continue;
|
|
140
|
+
const priorById = new Map(prior?.map((note) => [note.id, note]));
|
|
141
|
+
for (const note of notes ?? []) {
|
|
142
|
+
const original = priorById.get(note.id);
|
|
143
|
+
if (note.content !== original?.content || note.noteType !== original?.noteType) stories.push({
|
|
144
|
+
kind,
|
|
145
|
+
id: note.id
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return stories;
|
|
150
|
+
};
|
|
151
|
+
const preservePropertySources = ({ target, source, stories = documentStories(source) }) => {
|
|
152
|
+
for (const story of stories) {
|
|
153
|
+
const originals = findStoryBody(source, story)?.content ?? [];
|
|
154
|
+
const derived = findStoryBody(target, story)?.content ?? [];
|
|
155
|
+
if (originals === derived) continue;
|
|
156
|
+
const paragraphs = new Map(derived.flatMap((block) => block.type === "paragraph" ? [[block.paraId, block]] : []));
|
|
157
|
+
for (const [index, original] of originals.entries()) {
|
|
158
|
+
if (original.type !== "paragraph") continue;
|
|
159
|
+
const next = original.paraId === void 0 ? derived.at(index) : paragraphs.get(original.paraId);
|
|
160
|
+
if (next?.type === "paragraph" && next !== original) copyParagraphPropertySource(next, original);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
copyDocumentParagraphPropertySourceContract(target, source);
|
|
164
|
+
};
|
|
165
|
+
const supportsTextContent = (content) => content.every((item) => {
|
|
166
|
+
switch (item.type) {
|
|
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) => {
|
|
176
|
+
const body = findStoryBody(document, story);
|
|
177
|
+
if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
|
|
178
|
+
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") return true;
|
|
180
|
+
}
|
|
181
|
+
const content = body?.content;
|
|
182
|
+
return content !== void 0 && content.length > 0 && content.every((paragraph) => paragraph.type === "paragraph" && paragraph.reviewCarrier === void 0 && supportsTextContent(paragraph.content));
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
|
|
186
|
+
const body = findStoryBody(document, story);
|
|
187
|
+
if (!body) return refuse("The canonical story no longer exists.");
|
|
188
|
+
const options = {
|
|
189
|
+
...styles == null ? {} : { styles },
|
|
190
|
+
...document.package.theme === void 0 ? {} : { theme: document.package.theme }
|
|
191
|
+
};
|
|
192
|
+
const converted = Result.try({
|
|
193
|
+
try: () => {
|
|
194
|
+
if (story === OP_STORIES.MAIN) return toProseDoc(document, options);
|
|
195
|
+
if (story.kind === "header" || story.kind === "footer") return headerFooterToProseDoc(body.content, options);
|
|
196
|
+
return footnoteToProseDoc(body.content, options);
|
|
197
|
+
},
|
|
198
|
+
catch: (cause) => new CanonicalSessionError({
|
|
199
|
+
reason: "refused",
|
|
200
|
+
message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
if (converted.isErr()) return converted;
|
|
204
|
+
const paragraphs = [];
|
|
205
|
+
let failure;
|
|
206
|
+
converted.value.forEach((node, offset, index) => {
|
|
207
|
+
const source = body.content.at(index);
|
|
208
|
+
if (source?.type !== "paragraph" || source.paraId === void 0) {
|
|
209
|
+
failure = new CanonicalSessionError({
|
|
210
|
+
reason: "refused",
|
|
211
|
+
message: "The projection changed paragraph structure."
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let text = "";
|
|
216
|
+
let renderedText = "";
|
|
217
|
+
let renderedSize = 0;
|
|
218
|
+
const boundaries = [[0]];
|
|
219
|
+
const appendContent = (content) => {
|
|
220
|
+
for (const run of content) {
|
|
221
|
+
if (run.type === "insertion" || run.type === "deletion") {
|
|
222
|
+
appendContent(run.content);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (run.type !== "run") panic("Canonical projection encountered unsupported content");
|
|
226
|
+
for (const child of run.content) if (child.type === "text") for (const unit of child.text.split("")) {
|
|
227
|
+
text += unit;
|
|
228
|
+
renderedText += unit;
|
|
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) {
|
|
249
|
+
failure = new CanonicalSessionError({
|
|
250
|
+
reason: "refused",
|
|
251
|
+
message: "The paragraph cannot be projected as plain text."
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
paragraphs.push({
|
|
256
|
+
blockId: source.paraId,
|
|
257
|
+
start: offset + 1,
|
|
258
|
+
text,
|
|
259
|
+
boundaries,
|
|
260
|
+
node,
|
|
261
|
+
source
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
if (failure !== void 0) return Result.err(failure);
|
|
265
|
+
if (paragraphs.length !== body.content.length) return refuse("The projection changed paragraph structure.");
|
|
266
|
+
return Result.ok(new CanonicalProjection({
|
|
267
|
+
doc: converted.value,
|
|
268
|
+
paragraphs,
|
|
269
|
+
story
|
|
270
|
+
}));
|
|
271
|
+
};
|
|
272
|
+
const intentStory = (intent) => {
|
|
273
|
+
switch (intent.type) {
|
|
274
|
+
case "replaceText":
|
|
275
|
+
case "formatRun":
|
|
276
|
+
case "insertAtom": return intent.from.story;
|
|
277
|
+
case "splitParagraph":
|
|
278
|
+
case "formatParagraph": return intent.at.story;
|
|
279
|
+
case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
|
|
280
|
+
case "joinParagraphs": return intent.story;
|
|
281
|
+
case "table": return intent.operation.story;
|
|
282
|
+
default: return intent;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const UNDO_GROUP_WINDOW_MS = 500;
|
|
286
|
+
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);
|
|
288
|
+
const continuesGroup = (previous, next) => {
|
|
289
|
+
switch (next.semantic) {
|
|
290
|
+
case "typing":
|
|
291
|
+
case "deleteBackward":
|
|
292
|
+
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);
|
|
293
|
+
case "paste":
|
|
294
|
+
case "composition":
|
|
295
|
+
case "replacement":
|
|
296
|
+
case "structure": return false;
|
|
297
|
+
default: {
|
|
298
|
+
const unreachable = next.semantic;
|
|
299
|
+
return panic(`Unknown canonical input semantic ${unreachable}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const authorizedProjections = /* @__PURE__ */ new WeakMap();
|
|
304
|
+
/** Metadata is descriptive; transaction identity and session version authorize mutations. */
|
|
305
|
+
const isCanonicalProjectionTransaction = (transaction, session) => {
|
|
306
|
+
const authorization = authorizedProjections.get(transaction);
|
|
307
|
+
return authorization?.session === session && authorization.version === session.version && transaction.doc.eq(authorization.doc);
|
|
308
|
+
};
|
|
309
|
+
/** The model publishes only after PM and every plugin accept its exact projection. */
|
|
310
|
+
const publishCanonicalProjection = ({ state, commit, session }) => {
|
|
311
|
+
if (!isCanonicalProjectionTransaction(commit.transaction, session)) return refuse("The staged canonical projection is unauthorized.");
|
|
312
|
+
const applied = Result.try({
|
|
313
|
+
try: () => state.applyTransaction(commit.transaction),
|
|
314
|
+
catch: (cause) => new CanonicalSessionError({
|
|
315
|
+
reason: "refused",
|
|
316
|
+
message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
if (applied.isErr()) return applied;
|
|
320
|
+
const staged = applied.value;
|
|
321
|
+
if (!staged.transactions.includes(commit.transaction) || !staged.state.doc.eq(commit.projection.doc) || staged.transactions.some((transaction) => transaction.docChanged && !isCanonicalProjectionTransaction(transaction, session))) return refuse("A plugin refused or changed the canonical projection.");
|
|
322
|
+
const published = commit.publish();
|
|
323
|
+
if (published.isErr()) return published;
|
|
324
|
+
return Result.ok(staged);
|
|
325
|
+
};
|
|
326
|
+
const storyProjectionKey = (story) => {
|
|
327
|
+
switch (story.kind) {
|
|
328
|
+
case "header":
|
|
329
|
+
case "footer": return `${story.kind}:${story.rId}`;
|
|
330
|
+
case "footnote":
|
|
331
|
+
case "endnote": return `${story.kind}:${story.id}`;
|
|
332
|
+
default: return panic(`Unknown canonical story ${story}`);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
/** Immutable model authority with a journal staged independently of the PM view. */
|
|
336
|
+
var CanonicalSession = class {
|
|
337
|
+
currentDocument;
|
|
338
|
+
currentProjection;
|
|
339
|
+
currentVersion = 0;
|
|
340
|
+
allocateIntentIds = createEditorIntentIdAllocator();
|
|
341
|
+
mode = { type: "editing" };
|
|
342
|
+
allocatedBlockIds = /* @__PURE__ */ new Set();
|
|
343
|
+
nextBlockId = 1;
|
|
344
|
+
sourceOwners = /* @__PURE__ */ new Map();
|
|
345
|
+
storyProjections = /* @__PURE__ */ new Map();
|
|
346
|
+
currentSelection = null;
|
|
347
|
+
applied = [];
|
|
348
|
+
groupingBoundary = 0;
|
|
349
|
+
lifecycle = { type: "committed" };
|
|
350
|
+
undone = [];
|
|
351
|
+
styles;
|
|
352
|
+
constructor({ document, projection, styles }) {
|
|
353
|
+
this.currentDocument = document;
|
|
354
|
+
this.currentProjection = projection;
|
|
355
|
+
for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
|
|
356
|
+
this.advanceBlockId();
|
|
357
|
+
this.styles = styles == null ? styles : structuredClone(styles);
|
|
358
|
+
this.rememberSources(document);
|
|
359
|
+
}
|
|
360
|
+
setMode(mode) {
|
|
361
|
+
if (this.mode.type !== mode.type || this.mode.type === "suggesting" && mode.type === "suggesting" && this.mode.author !== mode.author) this.breakUndoGroup();
|
|
362
|
+
this.mode = mode.type === "editing" ? { type: "editing" } : {
|
|
363
|
+
type: "suggesting",
|
|
364
|
+
author: mode.author
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
hasStyle(styleId) {
|
|
368
|
+
return (this.styles ?? this.currentDocument.package.styles)?.styles.some((style) => style.styleId === styleId) ?? false;
|
|
369
|
+
}
|
|
370
|
+
advanceBlockId() {
|
|
371
|
+
while (this.allocatedBlockIds.has(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase())) this.nextBlockId += 1;
|
|
372
|
+
}
|
|
373
|
+
freshBlockId() {
|
|
374
|
+
if (this.nextBlockId > 2147483647) return refuse("The paragraph identity space is exhausted.");
|
|
375
|
+
return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
|
|
376
|
+
}
|
|
377
|
+
intentMode(document, intent) {
|
|
378
|
+
if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return { type: "editing" };
|
|
379
|
+
const ids = this.allocateIntentIds(document, intent);
|
|
380
|
+
return this.mode.type === "editing" ? {
|
|
381
|
+
type: "editing",
|
|
382
|
+
newIds: ids.newIds
|
|
383
|
+
} : {
|
|
384
|
+
type: "suggesting",
|
|
385
|
+
revision: {
|
|
386
|
+
id: ids.revisionId,
|
|
387
|
+
author: this.mode.author,
|
|
388
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
389
|
+
},
|
|
390
|
+
newIds: ids.newIds
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
intentNeedsIdentityIds(document, intent) {
|
|
394
|
+
let blockIds;
|
|
395
|
+
switch (intent.type) {
|
|
396
|
+
case "table": return true;
|
|
397
|
+
case "setList":
|
|
398
|
+
case "formatParagraph": return false;
|
|
399
|
+
case "replaceText":
|
|
400
|
+
case "insertAtom":
|
|
401
|
+
case "formatRun":
|
|
402
|
+
if (intent.from.blockId !== intent.to.blockId) return true;
|
|
403
|
+
blockIds = [intent.from.blockId];
|
|
404
|
+
break;
|
|
405
|
+
case "splitParagraph":
|
|
406
|
+
if (intent.to !== void 0 && intent.at.blockId !== intent.to.blockId) return true;
|
|
407
|
+
blockIds = [intent.at.blockId];
|
|
408
|
+
break;
|
|
409
|
+
case "joinParagraphs":
|
|
410
|
+
blockIds = [intent.blockId, intent.nextBlockId];
|
|
411
|
+
break;
|
|
412
|
+
default: return intent;
|
|
413
|
+
}
|
|
414
|
+
const hasIdentity = (value) => {
|
|
415
|
+
if (typeof value !== "object" || value === null) return false;
|
|
416
|
+
if (Array.isArray(value)) return value.some(hasIdentity);
|
|
417
|
+
if ("id" in value && typeof value.id === "number" && ("author" in value || "sdtType" in value)) return true;
|
|
418
|
+
return Object.values(value).some(hasIdentity);
|
|
419
|
+
};
|
|
420
|
+
const story = intentStory(intent);
|
|
421
|
+
return blockIds.some((blockId) => {
|
|
422
|
+
const paragraph = document === this.currentDocument && story === OP_STORIES.MAIN ? this.currentProjection.paragraph(blockId)?.source : findStoryBody(document, story)?.content.find((block) => block.type === "paragraph" && block.paraId === blockId);
|
|
423
|
+
return paragraph !== void 0 && hasIdentity(paragraph);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
rememberSources(document) {
|
|
427
|
+
for (const block of document.package.document.content) if (block.type === "paragraph" && block.paraId !== void 0) this.sourceOwners.set(block.paraId, block);
|
|
428
|
+
}
|
|
429
|
+
get document() {
|
|
430
|
+
if (this.isComposing) throw new CanonicalSessionError({
|
|
431
|
+
reason: "refused",
|
|
432
|
+
message: "Composition must finish before taking a snapshot."
|
|
433
|
+
});
|
|
434
|
+
return this.currentDocument;
|
|
435
|
+
}
|
|
436
|
+
get projection() {
|
|
437
|
+
return this.currentProjection;
|
|
438
|
+
}
|
|
439
|
+
get selection() {
|
|
440
|
+
return this.currentSelection;
|
|
441
|
+
}
|
|
442
|
+
get version() {
|
|
443
|
+
return this.currentVersion;
|
|
444
|
+
}
|
|
445
|
+
get canUndo() {
|
|
446
|
+
return this.applied.length > 0;
|
|
447
|
+
}
|
|
448
|
+
get canRedo() {
|
|
449
|
+
return this.undone.length > 0;
|
|
450
|
+
}
|
|
451
|
+
get isComposing() {
|
|
452
|
+
return this.lifecycle.type === "composing";
|
|
453
|
+
}
|
|
454
|
+
breakUndoGroup() {
|
|
455
|
+
this.groupingBoundary += 1;
|
|
456
|
+
}
|
|
457
|
+
beginComposition() {
|
|
458
|
+
if (this.isComposing) return refuse("A canonical composition is already pending.");
|
|
459
|
+
this.lifecycle = { type: "composing" };
|
|
460
|
+
this.breakUndoGroup();
|
|
461
|
+
return Result.ok(void 0);
|
|
462
|
+
}
|
|
463
|
+
endComposition() {
|
|
464
|
+
this.lifecycle = { type: "committed" };
|
|
465
|
+
this.breakUndoGroup();
|
|
466
|
+
}
|
|
467
|
+
checkState(state, projection = this.projection) {
|
|
468
|
+
if (this.isComposing) return refuse("Composition must finish before committing another edit.");
|
|
469
|
+
if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
|
|
470
|
+
return Result.ok(void 0);
|
|
471
|
+
}
|
|
472
|
+
prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", time = Date.now() }) {
|
|
473
|
+
const projected = this.projectStory(story);
|
|
474
|
+
if (projected.isErr()) return projected;
|
|
475
|
+
const projection = projected.value;
|
|
476
|
+
const checked = this.checkState(state, projection);
|
|
477
|
+
if (checked.isErr()) return checked;
|
|
478
|
+
if (!Number.isFinite(time)) return refuse("The input time is invalid.");
|
|
479
|
+
if (from > to || hasIllegalXmlCharacters(text) || /[\t\r\n]/u.test(text)) return refuse("Canonical input accepts a well-formed text replacement.");
|
|
480
|
+
if (from === to && text.length === 0) return noChange("The input makes no text change.");
|
|
481
|
+
const start = projection.inputAddressAt(from);
|
|
482
|
+
if (start.isErr()) return start;
|
|
483
|
+
const end = projection.inputAddressAt(to);
|
|
484
|
+
if (end.isErr()) return end;
|
|
485
|
+
const preSelection = projection.selectionAt(state);
|
|
486
|
+
if (preSelection.isErr()) return preSelection;
|
|
487
|
+
const intent = {
|
|
488
|
+
type: "replaceText",
|
|
489
|
+
from: start.value,
|
|
490
|
+
to: end.value,
|
|
491
|
+
text,
|
|
492
|
+
...state.storedMarks === null ? {} : { runPropsPatch: runFormattingPatchFromMarks(state.selection.$from.marks(), state.storedMarks) }
|
|
493
|
+
};
|
|
494
|
+
const compiled = compileEditorIntent(this.currentDocument, {
|
|
495
|
+
intent,
|
|
496
|
+
mode: this.intentMode(this.currentDocument, intent)
|
|
497
|
+
});
|
|
498
|
+
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
499
|
+
const { ops, selection: caret } = compiled.value;
|
|
500
|
+
const postSelection = {
|
|
501
|
+
anchor: caret,
|
|
502
|
+
head: caret
|
|
503
|
+
};
|
|
504
|
+
const preDocument = this.currentDocument;
|
|
505
|
+
const run = state.selection.empty && (semantic === "typing" && from === to && from === state.selection.head || semantic === "deleteBackward" && text.length === 0 && to === state.selection.head || semantic === "deleteForward" && text.length === 0 && from === state.selection.head);
|
|
506
|
+
return this.stage({
|
|
507
|
+
state,
|
|
508
|
+
story,
|
|
509
|
+
ops,
|
|
510
|
+
selection: postSelection,
|
|
511
|
+
origin: "input",
|
|
512
|
+
onPublish: (inverse, version) => {
|
|
513
|
+
const entry = {
|
|
514
|
+
type: "applied",
|
|
515
|
+
ops,
|
|
516
|
+
inverse,
|
|
517
|
+
preSelection: preSelection.value,
|
|
518
|
+
postSelection,
|
|
519
|
+
preDocument,
|
|
520
|
+
postDocument: this.currentDocument,
|
|
521
|
+
version,
|
|
522
|
+
origin: "input",
|
|
523
|
+
semantic,
|
|
524
|
+
grouping: run ? "run" : "isolated",
|
|
525
|
+
time,
|
|
526
|
+
boundary: this.groupingBoundary
|
|
527
|
+
};
|
|
528
|
+
const group = this.applied.at(-1);
|
|
529
|
+
const previous = group?.entries.at(-1);
|
|
530
|
+
if (group !== void 0 && previous !== void 0 && continuesGroup(previous, entry)) {
|
|
531
|
+
group.entries.push(entry);
|
|
532
|
+
group.postSelection = postSelection;
|
|
533
|
+
} else this.applied.push({
|
|
534
|
+
entries: [entry],
|
|
535
|
+
undo: { type: "entries" },
|
|
536
|
+
preSelection: preSelection.value,
|
|
537
|
+
postSelection
|
|
538
|
+
});
|
|
539
|
+
this.undone.length = 0;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
prepareIntent(state, intent) {
|
|
544
|
+
return this.prepareIntents(state, [intent]);
|
|
545
|
+
}
|
|
546
|
+
prepareIntents(state, intents) {
|
|
547
|
+
const first = intents.at(0);
|
|
548
|
+
if (first === void 0) return noChange("The intent batch is empty.");
|
|
549
|
+
const story = intentStory(first);
|
|
550
|
+
if (intents.some((intent) => !sameStory(story, intentStory(intent)))) return refuse("A compound input must address one story.");
|
|
551
|
+
const projected = this.projectStory(story);
|
|
552
|
+
if (projected.isErr()) return projected;
|
|
553
|
+
const checked = this.checkState(state, projected.value);
|
|
554
|
+
if (checked.isErr()) return checked;
|
|
555
|
+
const preSelection = projected.value.selectionAt(state);
|
|
556
|
+
if (preSelection.isErr()) return preSelection;
|
|
557
|
+
let document = this.currentDocument;
|
|
558
|
+
let postSelection = preSelection.value;
|
|
559
|
+
const ops = [];
|
|
560
|
+
const edits = [];
|
|
561
|
+
for (const intent of intents) {
|
|
562
|
+
const compiled = compileEditorIntent(document, {
|
|
563
|
+
intent,
|
|
564
|
+
mode: this.intentMode(document, intent)
|
|
565
|
+
});
|
|
566
|
+
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
567
|
+
const applied = applyDocumentOps(document, compiled.value.ops);
|
|
568
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
569
|
+
document = applied.value.document;
|
|
570
|
+
edits.push(applied.value);
|
|
571
|
+
ops.push(...compiled.value.ops);
|
|
572
|
+
if (intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
|
|
573
|
+
anchor: compiled.value.selection,
|
|
574
|
+
head: compiled.value.selection
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const stagedApplied = {
|
|
578
|
+
...combineEdits(this.currentDocument, edits),
|
|
579
|
+
revisions: edits.flatMap(({ revisions }) => revisions)
|
|
580
|
+
};
|
|
581
|
+
return this.prepareJournalledOps({
|
|
582
|
+
state,
|
|
583
|
+
ops,
|
|
584
|
+
postSelection,
|
|
585
|
+
stagedApplied
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
prepareOps(state, ops, postSelection) {
|
|
589
|
+
return this.prepareJournalledOps({
|
|
590
|
+
state,
|
|
591
|
+
ops,
|
|
592
|
+
postSelection
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
prepareJournalledOps({ state, ops, postSelection, stagedApplied }) {
|
|
596
|
+
const story = postSelection.anchor.story;
|
|
597
|
+
const projected = this.projectStory(story);
|
|
598
|
+
if (projected.isErr()) return projected;
|
|
599
|
+
const preSelection = projected.value.selectionAt(state);
|
|
600
|
+
if (preSelection.isErr()) return preSelection;
|
|
601
|
+
if (ops.length === 0 || stagedApplied?.inverse.length === 0) return noChange("The intent makes no document change.");
|
|
602
|
+
const preDocument = this.currentDocument;
|
|
603
|
+
return this.stage({
|
|
604
|
+
state,
|
|
605
|
+
story,
|
|
606
|
+
ops,
|
|
607
|
+
selection: postSelection,
|
|
608
|
+
origin: "input",
|
|
609
|
+
...stagedApplied === void 0 ? {} : { stagedApplied },
|
|
610
|
+
onPublish: (inverse, version) => {
|
|
611
|
+
const entry = {
|
|
612
|
+
type: "applied",
|
|
613
|
+
ops,
|
|
614
|
+
inverse,
|
|
615
|
+
preSelection: preSelection.value,
|
|
616
|
+
postSelection,
|
|
617
|
+
preDocument,
|
|
618
|
+
postDocument: this.currentDocument,
|
|
619
|
+
version,
|
|
620
|
+
origin: "input",
|
|
621
|
+
semantic: "structure",
|
|
622
|
+
grouping: "isolated",
|
|
623
|
+
time: Date.now(),
|
|
624
|
+
boundary: this.groupingBoundary
|
|
625
|
+
};
|
|
626
|
+
this.applied.push({
|
|
627
|
+
entries: [entry],
|
|
628
|
+
undo: { type: "entries" },
|
|
629
|
+
preSelection: preSelection.value,
|
|
630
|
+
postSelection
|
|
631
|
+
});
|
|
632
|
+
this.undone.length = 0;
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
prepareSplit(state) {
|
|
637
|
+
const from = this.projection.inputAddressAt(state.selection.from);
|
|
638
|
+
if (from.isErr()) return from;
|
|
639
|
+
const to = this.projection.inputAddressAt(state.selection.to);
|
|
640
|
+
if (to.isErr()) return to;
|
|
641
|
+
const source = this.projection.paragraph(from.value.blockId);
|
|
642
|
+
const nextStyle = (this.styles ?? this.currentDocument.package.styles)?.styles.find(({ styleId }) => styleId === source?.source.formatting?.styleId)?.next;
|
|
643
|
+
const fresh = this.freshBlockId();
|
|
644
|
+
if (fresh.isErr()) return fresh;
|
|
645
|
+
const intents = [{
|
|
646
|
+
type: "splitParagraph",
|
|
647
|
+
at: from.value,
|
|
648
|
+
to: to.value,
|
|
649
|
+
newBlockId: fresh.value
|
|
650
|
+
}];
|
|
651
|
+
if (state.selection.empty && source !== void 0 && from.value.offset === source.text.length && nextStyle !== void 0 && nextStyle !== source.source.formatting?.styleId) intents.push({
|
|
652
|
+
type: "formatParagraph",
|
|
653
|
+
at: {
|
|
654
|
+
...from.value,
|
|
655
|
+
offset: 0
|
|
656
|
+
},
|
|
657
|
+
patch: { styleId: nextStyle }
|
|
658
|
+
});
|
|
659
|
+
return this.prepareIntents(state, intents);
|
|
660
|
+
}
|
|
661
|
+
prepareJoin(state, direction) {
|
|
662
|
+
const checked = this.checkState(state);
|
|
663
|
+
if (checked.isErr()) return checked;
|
|
664
|
+
if (!state.selection.empty) return refuse("Join requires a collapsed text selection.");
|
|
665
|
+
const at = this.projection.inputAddressAt(state.selection.from);
|
|
666
|
+
if (at.isErr()) return at;
|
|
667
|
+
const groups = editorParagraphGroups(this.currentDocument, at.value.story);
|
|
668
|
+
const index = groups.findIndex(({ paragraphs }) => paragraphs.some((paragraph) => paragraph.paraId === at.value.blockId));
|
|
669
|
+
if (index < 0 || direction === "backward" && index === 0) return noChange("There is no adjacent paragraph to join.");
|
|
670
|
+
const firstGroup = groups.at(direction === "backward" ? index - 1 : index);
|
|
671
|
+
const secondGroup = groups.at(direction === "backward" ? index : index + 1);
|
|
672
|
+
const first = firstGroup?.paragraphs.at(-1);
|
|
673
|
+
const second = secondGroup?.paragraphs.at(0);
|
|
674
|
+
if (!first?.paraId || !second?.paraId) return noChange("There is no adjacent paragraph to join.");
|
|
675
|
+
if (!isCanonicalJoinBoundary(state, direction)) return refuse("Join requires a caret at a paragraph boundary.");
|
|
676
|
+
return this.prepareIntent(state, {
|
|
677
|
+
type: "joinParagraphs",
|
|
678
|
+
story: at.value.story,
|
|
679
|
+
blockId: first.paraId,
|
|
680
|
+
nextBlockId: second.paraId
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
prepareResolve(state, { revisionIds, resolution }) {
|
|
684
|
+
const checked = this.checkState(state);
|
|
685
|
+
if (checked.isErr()) return checked;
|
|
686
|
+
const preSelection = this.projection.selectionAt(state);
|
|
687
|
+
if (preSelection.isErr()) return preSelection;
|
|
688
|
+
const ops = [{
|
|
689
|
+
type: DOCUMENT_OP_TYPES.RESOLVE_REVISION,
|
|
690
|
+
story: OP_STORIES.MAIN,
|
|
691
|
+
revisionIds,
|
|
692
|
+
decision: resolution
|
|
693
|
+
}];
|
|
694
|
+
const applied = applyDocumentOps(this.currentDocument, ops);
|
|
695
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
696
|
+
const projected = project({
|
|
697
|
+
document: applied.value.document,
|
|
698
|
+
styles: this.styles
|
|
699
|
+
});
|
|
700
|
+
if (projected.isErr()) return projected;
|
|
701
|
+
const surviving = projected.value.paragraph(preSelection.value.head.blockId);
|
|
702
|
+
const first = applied.value.document.package.document.content.at(0);
|
|
703
|
+
if (first?.type !== "paragraph" || first.paraId === void 0) return refuse("Resolution left no paragraph.");
|
|
704
|
+
let offset = surviving ? Math.min(preSelection.value.head.offset, surviving.text.length) : 0;
|
|
705
|
+
if (surviving && splitsSurrogatePair(surviving.text, offset)) offset -= 1;
|
|
706
|
+
const caret = {
|
|
707
|
+
story: OP_STORIES.MAIN,
|
|
708
|
+
blockId: surviving?.blockId ?? first.paraId,
|
|
709
|
+
offset
|
|
710
|
+
};
|
|
711
|
+
return this.prepareJournalledOps({
|
|
712
|
+
state,
|
|
713
|
+
ops,
|
|
714
|
+
postSelection: {
|
|
715
|
+
anchor: caret,
|
|
716
|
+
head: caret
|
|
717
|
+
},
|
|
718
|
+
stagedApplied: applied.value
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
projectStory(story) {
|
|
722
|
+
if (story === OP_STORIES.MAIN) return Result.ok(this.projection);
|
|
723
|
+
const key = storyProjectionKey(story);
|
|
724
|
+
const body = findStoryBody(this.currentDocument, story);
|
|
725
|
+
if (body === void 0) {
|
|
726
|
+
this.storyProjections.delete(key);
|
|
727
|
+
return refuse("The canonical story no longer exists.");
|
|
728
|
+
}
|
|
729
|
+
const cached = this.storyProjections.get(key);
|
|
730
|
+
const theme = this.currentDocument.package.theme;
|
|
731
|
+
if (cached?.content === body.content && cached.theme === theme) return Result.ok(cached.projection);
|
|
732
|
+
const projected = project({
|
|
733
|
+
document: this.currentDocument,
|
|
734
|
+
styles: this.styles,
|
|
735
|
+
story
|
|
736
|
+
});
|
|
737
|
+
if (projected.isOk()) this.storyProjections.set(key, {
|
|
738
|
+
content: body.content,
|
|
739
|
+
theme,
|
|
740
|
+
projection: projected.value
|
|
741
|
+
});
|
|
742
|
+
return projected;
|
|
743
|
+
}
|
|
744
|
+
prepareOperations(state, ops) {
|
|
745
|
+
if (ops.length === 0) return refuse("The operation batch is empty.");
|
|
746
|
+
const selected = this.projection.selectionAt(state);
|
|
747
|
+
if (selected.isErr()) return selected;
|
|
748
|
+
const preDocument = this.currentDocument;
|
|
749
|
+
return this.stage({
|
|
750
|
+
state,
|
|
751
|
+
ops,
|
|
752
|
+
selection: selected.value,
|
|
753
|
+
origin: "input",
|
|
754
|
+
onPublish: (inverse, version) => {
|
|
755
|
+
this.applied.push({
|
|
756
|
+
entries: [{
|
|
757
|
+
type: "applied",
|
|
758
|
+
ops,
|
|
759
|
+
inverse,
|
|
760
|
+
preSelection: selected.value,
|
|
761
|
+
postSelection: selected.value,
|
|
762
|
+
preDocument,
|
|
763
|
+
postDocument: this.currentDocument,
|
|
764
|
+
version,
|
|
765
|
+
origin: "input",
|
|
766
|
+
semantic: "structure",
|
|
767
|
+
grouping: "isolated",
|
|
768
|
+
time: Date.now(),
|
|
769
|
+
boundary: this.groupingBoundary
|
|
770
|
+
}],
|
|
771
|
+
undo: { type: "entries" },
|
|
772
|
+
preSelection: selected.value,
|
|
773
|
+
postSelection: selected.value
|
|
774
|
+
});
|
|
775
|
+
this.undone.length = 0;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
prepareUndo(state, story = OP_STORIES.MAIN) {
|
|
780
|
+
const group = this.applied.at(-1);
|
|
781
|
+
if (group === void 0) return refuse("There is no canonical edit to undo.");
|
|
782
|
+
const propertySourceDocument = group.entries.at(0)?.preDocument;
|
|
783
|
+
return this.stage({
|
|
784
|
+
state,
|
|
785
|
+
story,
|
|
786
|
+
ops: group.undo.type === "replayed" ? group.undo.inverse : group.entries.toReversed().flatMap((entry) => entry.inverse),
|
|
787
|
+
selection: group.preSelection,
|
|
788
|
+
origin: "undo",
|
|
789
|
+
...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
|
|
790
|
+
onPublish: (redoOps) => {
|
|
791
|
+
this.applied.pop();
|
|
792
|
+
this.undone.push({
|
|
793
|
+
type: "undone",
|
|
794
|
+
group,
|
|
795
|
+
redoOps
|
|
796
|
+
});
|
|
797
|
+
this.breakUndoGroup();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
prepareRedo(state, story = OP_STORIES.MAIN) {
|
|
802
|
+
const undone = this.undone.at(-1);
|
|
803
|
+
if (undone === void 0) return refuse("There is no canonical edit to redo.");
|
|
804
|
+
const propertySourceDocument = undone.group.entries.at(-1)?.postDocument;
|
|
805
|
+
return this.stage({
|
|
806
|
+
state,
|
|
807
|
+
story,
|
|
808
|
+
ops: undone.redoOps,
|
|
809
|
+
selection: undone.group.postSelection,
|
|
810
|
+
origin: "redo",
|
|
811
|
+
...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
|
|
812
|
+
onPublish: (inverse) => {
|
|
813
|
+
this.undone.pop();
|
|
814
|
+
this.applied.push({
|
|
815
|
+
entries: undone.group.entries,
|
|
816
|
+
undo: {
|
|
817
|
+
type: "replayed",
|
|
818
|
+
inverse
|
|
819
|
+
},
|
|
820
|
+
preSelection: undone.group.preSelection,
|
|
821
|
+
postSelection: undone.group.postSelection
|
|
822
|
+
});
|
|
823
|
+
this.breakUndoGroup();
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
stage({ state, ops, selection, origin, onPublish, propertySourceDocument, stagedApplied, story = OP_STORIES.MAIN, previousProjection }) {
|
|
828
|
+
const previous = previousProjection === void 0 ? this.projectStory(story) : Result.ok(previousProjection);
|
|
829
|
+
if (previous.isErr()) return previous;
|
|
830
|
+
const checked = this.checkState(state, previous.value);
|
|
831
|
+
if (checked.isErr()) return checked;
|
|
832
|
+
const applied = stagedApplied === void 0 ? applyDocumentOps(this.currentDocument, ops) : Result.ok(stagedApplied);
|
|
833
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
834
|
+
if (applied.value.inverse.length === 0) return noChange("The intent makes no document change.");
|
|
835
|
+
const changed = changedStories({
|
|
836
|
+
document: applied.value.document,
|
|
837
|
+
previous: this.currentDocument
|
|
838
|
+
});
|
|
839
|
+
if (!supportsSeed(applied.value.document, changed)) return refuse("The operations produce unsupported canonical story content.");
|
|
840
|
+
preservePropertySources({
|
|
841
|
+
target: applied.value.document,
|
|
842
|
+
source: this.currentDocument,
|
|
843
|
+
stories: changed
|
|
844
|
+
});
|
|
845
|
+
if (propertySourceDocument !== void 0) preservePropertySources({
|
|
846
|
+
target: applied.value.document,
|
|
847
|
+
source: propertySourceDocument
|
|
848
|
+
});
|
|
849
|
+
const stagedSources = new Map(this.sourceOwners);
|
|
850
|
+
for (const block of applied.value.document.package.document.content) {
|
|
851
|
+
if (block.type !== "paragraph" || block.paraId === void 0) continue;
|
|
852
|
+
const source = stagedSources.get(block.paraId);
|
|
853
|
+
if (source && block !== source) copyParagraphPropertySource(block, source);
|
|
854
|
+
}
|
|
855
|
+
const projectedStory = findStoryBody(applied.value.document, story) ? story : OP_STORIES.MAIN;
|
|
856
|
+
const projected = project({
|
|
857
|
+
document: applied.value.document,
|
|
858
|
+
styles: this.styles,
|
|
859
|
+
story: projectedStory
|
|
860
|
+
});
|
|
861
|
+
if (projected.isErr()) return projected;
|
|
862
|
+
const bodyProjection = (() => {
|
|
863
|
+
if (sameStory(projectedStory, OP_STORIES.MAIN)) return projected;
|
|
864
|
+
if (ops.some(operationChangesBodyProjection)) return project({
|
|
865
|
+
document: applied.value.document,
|
|
866
|
+
styles: this.styles
|
|
867
|
+
});
|
|
868
|
+
return Result.ok(this.currentProjection);
|
|
869
|
+
})();
|
|
870
|
+
if (bodyProjection.isErr()) return bodyProjection;
|
|
871
|
+
const transaction = state.tr;
|
|
872
|
+
const before = state.doc;
|
|
873
|
+
const after = projected.value.doc;
|
|
874
|
+
let prefix = 0;
|
|
875
|
+
let from = 0;
|
|
876
|
+
while (prefix < before.childCount && prefix < after.childCount && before.child(prefix).eq(after.child(prefix))) {
|
|
877
|
+
from += before.child(prefix).nodeSize;
|
|
878
|
+
prefix += 1;
|
|
879
|
+
}
|
|
880
|
+
let oldEnd = before.childCount;
|
|
881
|
+
let newEnd = after.childCount;
|
|
882
|
+
let to = before.content.size;
|
|
883
|
+
while (oldEnd > prefix && newEnd > prefix && before.child(oldEnd - 1).eq(after.child(newEnd - 1))) {
|
|
884
|
+
oldEnd -= 1;
|
|
885
|
+
newEnd -= 1;
|
|
886
|
+
to -= before.child(oldEnd).nodeSize;
|
|
887
|
+
}
|
|
888
|
+
if (oldEnd !== prefix || newEnd !== prefix) {
|
|
889
|
+
const replacement = [];
|
|
890
|
+
for (let index = prefix; index < newEnd; index += 1) replacement.push(after.child(index));
|
|
891
|
+
transaction.replaceWith(from, to, Fragment.fromArray(replacement));
|
|
892
|
+
}
|
|
893
|
+
if (!transaction.doc.eq(projected.value.doc)) transaction.replaceWith(0, transaction.doc.content.size, projected.value.doc.content);
|
|
894
|
+
if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
|
|
895
|
+
const anchor = projected.value.positionAt(selection.anchor);
|
|
896
|
+
const head = projected.value.positionAt(selection.head);
|
|
897
|
+
if (anchor.isOk() && head.isOk()) transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
|
|
898
|
+
else transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
|
|
899
|
+
}
|
|
900
|
+
transaction.setMeta(CANONICAL_PROJECTION_META, {
|
|
901
|
+
type: "canonical",
|
|
902
|
+
origin,
|
|
903
|
+
version: this.version + 1
|
|
904
|
+
});
|
|
905
|
+
if (ops.some(operationChangesPackage)) markPackageChange(transaction);
|
|
906
|
+
transaction.setMeta("addToHistory", false);
|
|
907
|
+
const baseVersion = this.version;
|
|
908
|
+
const baseBoundary = this.groupingBoundary;
|
|
909
|
+
authorizedProjections.set(transaction, {
|
|
910
|
+
session: this,
|
|
911
|
+
version: baseVersion,
|
|
912
|
+
doc: projected.value.doc
|
|
913
|
+
});
|
|
914
|
+
return Result.ok({
|
|
915
|
+
transaction,
|
|
916
|
+
document: applied.value.document,
|
|
917
|
+
projection: projected.value,
|
|
918
|
+
bodyProjection: bodyProjection.value,
|
|
919
|
+
touched: applied.value.touched,
|
|
920
|
+
version: baseVersion + 1,
|
|
921
|
+
origin,
|
|
922
|
+
publish: () => {
|
|
923
|
+
if (this.isComposing || this.groupingBoundary !== baseBoundary) return refuse("The staged canonical commit crossed a composition or selection boundary.");
|
|
924
|
+
if (this.version !== baseVersion) return refuse("The staged canonical commit is stale.");
|
|
925
|
+
if (!isCanonicalProjectionTransaction(transaction, this)) return refuse("The staged canonical projection was changed after preparation.");
|
|
926
|
+
for (const blockId of applied.value.touched.inserted) this.allocatedBlockIds.add(blockId.toUpperCase());
|
|
927
|
+
for (const op of ops) if (op.type === DOCUMENT_OP_TYPES.SPLIT_BLOCK) this.allocatedBlockIds.add(op.newBlockId.toUpperCase());
|
|
928
|
+
this.advanceBlockId();
|
|
929
|
+
this.rememberSources(applied.value.document);
|
|
930
|
+
if (projectedStory !== OP_STORIES.MAIN) {
|
|
931
|
+
const body = findStoryBody(applied.value.document, projectedStory);
|
|
932
|
+
if (body === void 0) panic("A staged story projection lost its source.");
|
|
933
|
+
this.storyProjections.set(storyProjectionKey(projectedStory), {
|
|
934
|
+
content: body.content,
|
|
935
|
+
theme: applied.value.document.package.theme,
|
|
936
|
+
projection: projected.value
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
this.currentSelection = selection;
|
|
940
|
+
this.currentDocument = applied.value.document;
|
|
941
|
+
this.currentProjection = bodyProjection.value;
|
|
942
|
+
this.currentVersion = baseVersion + 1;
|
|
943
|
+
onPublish(applied.value.inverse, this.currentVersion);
|
|
944
|
+
return Result.ok(void 0);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const createCanonicalSession = (document, styles) => {
|
|
950
|
+
if (!supportsSeed(document)) return refuse("Canonical sessions currently require plain paragraphs and supported inline atoms.");
|
|
951
|
+
const owned = cloneDocumentWithParagraphPropertySources(document);
|
|
952
|
+
const normalized = normalizeForOps(owned);
|
|
953
|
+
preservePropertySources({
|
|
954
|
+
target: normalized,
|
|
955
|
+
source: owned
|
|
956
|
+
});
|
|
957
|
+
const validated = validateOpsDocument(normalized);
|
|
958
|
+
if (validated.isErr()) return refuse(validated.error.message);
|
|
959
|
+
const projected = project({
|
|
960
|
+
document: normalized,
|
|
961
|
+
styles
|
|
962
|
+
});
|
|
963
|
+
if (projected.isErr()) return projected;
|
|
964
|
+
return Result.ok(new CanonicalSession({
|
|
965
|
+
document: normalized,
|
|
966
|
+
projection: projected.value,
|
|
967
|
+
styles
|
|
968
|
+
}));
|
|
969
|
+
};
|
|
970
|
+
const hasDeletedParagraphMark = (node) => {
|
|
971
|
+
const mark = node.attrs["pPrMark"];
|
|
972
|
+
return typeof mark === "object" && mark !== null && "kind" in mark && (mark.kind === "del" || mark.kind === "moveFrom");
|
|
973
|
+
};
|
|
974
|
+
/** Visible graphemes concatenate across retained deletions and deleted paragraph marks. */
|
|
975
|
+
const visibleDeletionContext = (state) => {
|
|
976
|
+
const { selection } = state;
|
|
977
|
+
const index = selection.$from.index(0);
|
|
978
|
+
let first = index;
|
|
979
|
+
while (first > 0 && hasDeletedParagraphMark(state.doc.child(first - 1))) first -= 1;
|
|
980
|
+
let last = index;
|
|
981
|
+
while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
|
|
982
|
+
let text = "";
|
|
983
|
+
const physicalGaps = [];
|
|
984
|
+
let visibleOffset = 0;
|
|
985
|
+
state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
|
|
986
|
+
if (paragraphIndex < first || paragraphIndex > last) return;
|
|
987
|
+
paragraph.forEach((node, offset) => {
|
|
988
|
+
if (node.marks.some((mark) => mark.type.name === "deletion")) return;
|
|
989
|
+
const value = node.isText ? node.text ?? "" : "";
|
|
990
|
+
for (let unit = 0; unit < value.length; unit += 1) {
|
|
991
|
+
const position = paragraphOffset + 1 + offset + unit;
|
|
992
|
+
physicalGaps[text.length] = position;
|
|
993
|
+
text += value.charAt(unit);
|
|
994
|
+
physicalGaps.push(position + (node.isText ? 1 : node.nodeSize));
|
|
995
|
+
if (position < selection.from) visibleOffset += 1;
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
return {
|
|
1000
|
+
text,
|
|
1001
|
+
physicalGaps,
|
|
1002
|
+
visibleOffset
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
const isCanonicalJoinBoundary = (state, direction) => {
|
|
1006
|
+
if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
|
|
1007
|
+
const { text, visibleOffset } = visibleDeletionContext(state);
|
|
1008
|
+
return visibleOffset === (direction === "backward" ? 0 : text.length);
|
|
1009
|
+
};
|
|
1010
|
+
/** Native character deletion consumes one whole visible grapheme. */
|
|
1011
|
+
const deletionRange = (state, direction) => {
|
|
1012
|
+
const { selection } = state;
|
|
1013
|
+
if (!(selection instanceof TextSelection)) return refuse("Canonical deletion requires a text selection.");
|
|
1014
|
+
if (!selection.empty) return Result.ok({
|
|
1015
|
+
from: selection.from,
|
|
1016
|
+
to: selection.to
|
|
1017
|
+
});
|
|
1018
|
+
const parent = selection.$from.parent;
|
|
1019
|
+
const offset = selection.$from.parentOffset;
|
|
1020
|
+
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 { text, physicalGaps, visibleOffset } = visibleDeletionContext(state);
|
|
1022
|
+
if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
|
|
1023
|
+
if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
|
|
1024
|
+
if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
|
|
1025
|
+
let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
|
|
1026
|
+
while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
|
|
1027
|
+
const position = physicalGaps.at(boundary);
|
|
1028
|
+
if (position === void 0) panic("Canonical deletion lost a grapheme boundary.");
|
|
1029
|
+
return Result.ok(direction === "backward" ? {
|
|
1030
|
+
from: position,
|
|
1031
|
+
to: selection.to
|
|
1032
|
+
} : {
|
|
1033
|
+
from: selection.from,
|
|
1034
|
+
to: position
|
|
1035
|
+
});
|
|
1036
|
+
};
|
|
1037
|
+
//#endregion
|
|
1038
|
+
export { CANONICAL_PROJECTION_META, CanonicalSessionError, createCanonicalSession, deletionRange, isCanonicalJoinBoundary, isCanonicalProjectionTransaction, publishCanonicalProjection };
|