@stll/folio-core 0.54.2 → 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 +185 -33
- package/dist/ai-edits/headless.js +3 -2
- package/dist/ai-edits/types.d.ts +7 -1
- package/dist/compare/compare.js +1 -0
- package/dist/compare/content-types.d.ts +3 -15
- 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 +53 -0
- package/dist/controller/canonicalComposition.js +296 -0
- package/dist/controller/canonicalInlineProjection.d.ts +113 -0
- package/dist/controller/canonicalInlineProjection.js +393 -0
- package/dist/controller/canonicalInput.d.ts +54 -0
- package/dist/controller/canonicalInput.js +377 -0
- package/dist/controller/canonicalInputTimer.d.ts +7 -0
- package/dist/controller/canonicalInputTimer.js +11 -0
- package/dist/controller/canonicalOperations.d.ts +22 -0
- package/dist/controller/canonicalOperations.js +85 -0
- package/dist/controller/canonicalPublicOperations.d.ts +48 -0
- package/dist/controller/canonicalPublicOperations.js +556 -0
- package/dist/controller/canonicalReview.d.ts +12 -0
- package/dist/controller/canonicalReview.js +19 -0
- package/dist/controller/canonicalSession.d.ts +167 -0
- package/dist/controller/canonicalSession.js +1186 -0
- package/dist/controller/canonicalStoryEditor.d.ts +36 -0
- package/dist/controller/canonicalStoryEditor.js +87 -0
- package/dist/controller/canonicalStructure.d.ts +16 -0
- package/dist/controller/canonicalStructure.js +322 -0
- package/dist/controller/canonicalTocSelection.d.ts +11 -0
- package/dist/controller/canonicalTocSelection.js +43 -0
- package/dist/controller/folioEditor.d.ts +3 -10
- package/dist/controller/folioEditor.js +14 -4
- package/dist/controller/headerFooterEditorManager.d.ts +5 -0
- package/dist/controller/headerFooterEditorManager.js +72 -3
- package/dist/controller/hiddenEditorApi.d.ts +45 -0
- package/dist/controller/hiddenEditorApi.js +29 -8
- package/dist/controller/hiddenEditorManager.d.ts +13 -2
- package/dist/controller/hiddenEditorManager.js +639 -31
- package/dist/controller/layoutPipeline.d.ts +1 -1
- package/dist/controller/layoutPipeline.js +2 -0
- package/dist/controller/noteEditorManager.d.ts +5 -0
- package/dist/controller/noteEditorManager.js +114 -15
- package/dist/document-operations.d.ts +8 -1
- package/dist/document-operations.js +6 -4
- package/dist/docx/attributeRemainder.js +1 -0
- 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/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 +74 -0
- package/dist/docx/contentTypeRegistry.d.ts +8 -0
- package/dist/docx/contentTypeRegistry.js +64 -0
- package/dist/docx/documentParser.d.ts +1 -13
- package/dist/docx/documentParser.js +1 -47
- package/dist/docx/ensureParaIds.d.ts +7 -1
- package/dist/docx/ensureParaIds.js +173 -215
- package/dist/docx/foldedListNumberFields.d.ts +85 -0
- package/dist/docx/foldedListNumberFields.js +267 -0
- 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 +39 -1
- package/dist/docx/headerFooterVerbatim.js +173 -2
- 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/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 +74 -19
- 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 +77 -22
- package/dist/docx/parser.js +3 -1
- package/dist/docx/removeHeaderFooterParts.js +17 -6
- 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 +54 -6
- package/dist/docx/rezip.js +835 -257
- 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 +22 -0
- package/dist/docx/saveDiagnostics.js +0 -0
- package/dist/docx/selectiveSave.d.ts +3 -1
- package/dist/docx/selectiveSave.js +36 -4
- package/dist/docx/selectiveXmlPatch.d.ts +16 -8
- package/dist/docx/selectiveXmlPatch.js +81 -7
- package/dist/docx/serializer/documentSerializer.d.ts +9 -2
- package/dist/docx/serializer/documentSerializer.js +29 -10
- 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/server/applyDocxXmlPatchProposal.js +14 -9
- 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 +31 -0
- package/dist/docx/storyBlockReplay.js +471 -0
- package/dist/docx/storyPlainText.d.ts +20 -0
- package/dist/docx/storyPlainText.js +23 -0
- package/dist/docx/streamingXmlParser.d.ts +22 -1
- package/dist/docx/streamingXmlParser.js +49 -3
- package/dist/docx/structuralXmlPatch.js +18 -23
- 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/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/acceptedBlockProjection.d.ts +12 -0
- package/dist/internal/acceptedBlockProjection.js +45 -0
- 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 +9 -1
- package/dist/markdown/escape.d.ts +3 -1
- package/dist/markdown/escape.js +9 -2
- package/dist/markdown/renderBlock.js +20 -121
- package/dist/markdown/renderParagraph.js +13 -15
- package/dist/markdown/renderRuns.d.ts +7 -1
- package/dist/markdown/renderRuns.js +35 -15
- 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 +121 -0
- package/dist/prosemirror/canonicalCommands.js +35 -0
- 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 +15 -1
- package/dist/prosemirror/commands/pastePlainText.js +2 -1
- package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
- package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
- package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
- package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
- package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +11 -4
- package/dist/prosemirror/conversion/toProseDoc.js +55 -14
- 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 +88 -48
- 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/HyperlinkExtension.js +56 -28
- 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 +72 -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/insertOperations.js +2 -1
- 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/paragraphPropertyCarry.d.ts +7 -1
- package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
- package/dist/prosemirror/plugins/suggestionMode.js +57 -7
- 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/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/content.d.ts +2 -2
- package/dist/types/docxSerialization.d.ts +14 -0
- package/dist/types/docxSerialization.js +7 -0
- package/dist/utils/createDocument.js +2 -2
- package/package.json +16 -4
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
import { splitsGraphemeCluster, splitsSurrogatePair } from "../ai-edits/character-boundaries.js";
|
|
2
|
+
import { cloneDocumentWithParagraphPropertySources, copyDocumentParagraphPropertySourceContract, copyParagraphPropertySource } from "../docx/paragraphPropertySource.js";
|
|
3
|
+
import { canonicalSelectionRange } from "../prosemirror/canonicalSelectionRange.js";
|
|
4
|
+
import { collectPairedBookmarkIds, footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
|
|
5
|
+
import { markPackageChange } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
|
|
6
|
+
import { runFormattingPatchFromMarks } from "../prosemirror/runFormattingFromMarks.js";
|
|
7
|
+
import { CANONICAL_GAP } from "../types/canonicalCapabilities.js";
|
|
8
|
+
import { canonicalInlineSourceIssue, canonicalNativeCells, projectCanonicalInline } from "./canonicalInlineProjection.js";
|
|
9
|
+
import { mapTocSelection } from "./canonicalTocSelection.js";
|
|
10
|
+
import { Result, TaggedError, panic } from "better-result";
|
|
11
|
+
import { hasIllegalXmlCharacters } from "@stll/docx-core";
|
|
12
|
+
import { Fragment } from "prosemirror-model";
|
|
13
|
+
import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
|
|
14
|
+
import { AllSelection, TextSelection } from "prosemirror-state";
|
|
15
|
+
//#region src/controller/canonicalSession.ts
|
|
16
|
+
var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
|
|
17
|
+
const refuse = (message) => Result.err(new CanonicalSessionError({
|
|
18
|
+
gap: CANONICAL_GAP.dispatch,
|
|
19
|
+
message,
|
|
20
|
+
reason: "refused"
|
|
21
|
+
}));
|
|
22
|
+
const noChange = (message) => Result.err(new CanonicalSessionError({
|
|
23
|
+
gap: CANONICAL_GAP.dispatch,
|
|
24
|
+
message,
|
|
25
|
+
reason: "noChange"
|
|
26
|
+
}));
|
|
27
|
+
const operationChangesPackage = (op) => {
|
|
28
|
+
switch (op.type) {
|
|
29
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
30
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
31
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
32
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
33
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
34
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
35
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
36
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
37
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
38
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
39
|
+
default:
|
|
40
|
+
if ("story" in op) return !sameStory(op.story, OP_STORIES.MAIN);
|
|
41
|
+
if ("at" in op) return !sameStory(op.at.story, OP_STORIES.MAIN);
|
|
42
|
+
if ("from" in op) return !sameStory(op.from.story, OP_STORIES.MAIN);
|
|
43
|
+
return op;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const operationChangesStructure = (op) => {
|
|
47
|
+
switch (op.type) {
|
|
48
|
+
case DOCUMENT_OP_TYPES.DELETE_BLOCKS:
|
|
49
|
+
case DOCUMENT_OP_TYPES.INSERT_BLOCKS:
|
|
50
|
+
case DOCUMENT_OP_TYPES.SPLIT_BLOCK:
|
|
51
|
+
case DOCUMENT_OP_TYPES.JOIN_BLOCKS:
|
|
52
|
+
case DOCUMENT_OP_TYPES.REPLACE_BLOCKS:
|
|
53
|
+
case DOCUMENT_OP_TYPES.INSERT_TABLE:
|
|
54
|
+
case DOCUMENT_OP_TYPES.DELETE_TABLE:
|
|
55
|
+
case DOCUMENT_OP_TYPES.SET_CONTAINER_BLOCKS:
|
|
56
|
+
case DOCUMENT_OP_TYPES.INSERT_ROW:
|
|
57
|
+
case DOCUMENT_OP_TYPES.DELETE_ROW:
|
|
58
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_ROWS:
|
|
59
|
+
case DOCUMENT_OP_TYPES.INSERT_COLUMN:
|
|
60
|
+
case DOCUMENT_OP_TYPES.DELETE_COLUMN:
|
|
61
|
+
case DOCUMENT_OP_TYPES.MERGE_CELLS:
|
|
62
|
+
case DOCUMENT_OP_TYPES.SPLIT_CELL:
|
|
63
|
+
case DOCUMENT_OP_TYPES.SET_TABLE:
|
|
64
|
+
case DOCUMENT_OP_TYPES.RESOLVE_REVISION: return true;
|
|
65
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
66
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
67
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
68
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
69
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
70
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
71
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
72
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
73
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
74
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT:
|
|
75
|
+
case DOCUMENT_OP_TYPES.INSERT_TEXT:
|
|
76
|
+
case DOCUMENT_OP_TYPES.INSERT_CONTENT:
|
|
77
|
+
case DOCUMENT_OP_TYPES.DELETE_RANGE:
|
|
78
|
+
case DOCUMENT_OP_TYPES.SPLIT_INLINE:
|
|
79
|
+
case DOCUMENT_OP_TYPES.JOIN_INLINE:
|
|
80
|
+
case DOCUMENT_OP_TYPES.SET_RUN_PROPS:
|
|
81
|
+
case DOCUMENT_OP_TYPES.SET_PARAGRAPH_PROPS:
|
|
82
|
+
case DOCUMENT_OP_TYPES.SET_PARAGRAPH_REVIEW:
|
|
83
|
+
case DOCUMENT_OP_TYPES.REPLACE_INLINE:
|
|
84
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_GRID:
|
|
85
|
+
case DOCUMENT_OP_TYPES.SET_CELL_PROPS:
|
|
86
|
+
case DOCUMENT_OP_TYPES.SET_ROW_PROPS:
|
|
87
|
+
case DOCUMENT_OP_TYPES.SET_TABLE_PROPS: return false;
|
|
88
|
+
default: return op;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const operationChangesBodyProjection = (op) => {
|
|
92
|
+
switch (op.type) {
|
|
93
|
+
case DOCUMENT_OP_TYPES.CREATE_HEADER_FOOTER:
|
|
94
|
+
case DOCUMENT_OP_TYPES.REMOVE_HEADER_FOOTER:
|
|
95
|
+
case DOCUMENT_OP_TYPES.ADD_NOTE:
|
|
96
|
+
case DOCUMENT_OP_TYPES.REMOVE_NOTE:
|
|
97
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_PROPS:
|
|
98
|
+
case DOCUMENT_OP_TYPES.RESTORE_STORY_PARTS:
|
|
99
|
+
case DOCUMENT_OP_TYPES.SET_PACKAGE_RESOURCES:
|
|
100
|
+
case DOCUMENT_OP_TYPES.CREATE_NUMBERING_INSTANCE:
|
|
101
|
+
case DOCUMENT_OP_TYPES.DELETE_NUMBERING_INSTANCE:
|
|
102
|
+
case DOCUMENT_OP_TYPES.SET_SECTION_ENDPOINT: return true;
|
|
103
|
+
default:
|
|
104
|
+
if ("story" in op) return sameStory(op.story, OP_STORIES.MAIN);
|
|
105
|
+
if ("at" in op) return sameStory(op.at.story, OP_STORIES.MAIN);
|
|
106
|
+
if ("from" in op) return sameStory(op.from.story, OP_STORIES.MAIN);
|
|
107
|
+
return op;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const CANONICAL_PROJECTION_META = "folioCanonicalProjection";
|
|
111
|
+
/** Paragraph gaps bind logical units and source zero-width ordinals to PM positions. */
|
|
112
|
+
var CanonicalProjection = class {
|
|
113
|
+
doc;
|
|
114
|
+
story;
|
|
115
|
+
paragraphs;
|
|
116
|
+
paragraphsById;
|
|
117
|
+
constructor({ doc, paragraphs, story }) {
|
|
118
|
+
this.story = story;
|
|
119
|
+
this.doc = doc;
|
|
120
|
+
this.paragraphs = paragraphs;
|
|
121
|
+
this.paragraphsById = new Map(paragraphs.map((paragraph) => [paragraph.blockId, paragraph]));
|
|
122
|
+
}
|
|
123
|
+
addressAt(position) {
|
|
124
|
+
if (!Number.isInteger(position)) return refuse("The input position is not an integer.");
|
|
125
|
+
const paragraph = this.paragraphs.find(({ start, node }) => position >= start && position <= start + node.content.size);
|
|
126
|
+
if (paragraph === void 0) return refuse("The input is outside a plain paragraph.");
|
|
127
|
+
const relative = position - paragraph.start;
|
|
128
|
+
const offset = paragraph.boundaries.findIndex((gaps) => gaps.some((gap) => gap.position === relative));
|
|
129
|
+
if (offset < 0) return refuse("The input splits an indivisible inline element.");
|
|
130
|
+
if (splitsSurrogatePair(paragraph.text, offset)) return refuse("The input would split a surrogate pair.");
|
|
131
|
+
const gaps = paragraph.boundaries.at(offset) ?? panic("Missing canonical boundary");
|
|
132
|
+
const gap = gaps.find((entry) => entry.position === relative) ?? panic("Missing canonical native gap");
|
|
133
|
+
return Result.ok({
|
|
134
|
+
story: this.story,
|
|
135
|
+
blockId: paragraph.blockId,
|
|
136
|
+
offset,
|
|
137
|
+
...gaps.length > 1 || gap.zeroWidthBefore > 0 ? { zeroWidthBefore: gap.zeroWidthBefore } : {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
inputAddressAt(position) {
|
|
141
|
+
const address = this.addressAt(position);
|
|
142
|
+
if (address.isErr()) return address;
|
|
143
|
+
const paragraph = this.paragraph(address.value.blockId);
|
|
144
|
+
if (paragraph === void 0) panic("Input lost its paragraph.");
|
|
145
|
+
let destination = position;
|
|
146
|
+
paragraph.node.forEach((node, start) => {
|
|
147
|
+
if (node.marks.some((mark) => mark.type.name === "deletion") && position > paragraph.start + start && position < paragraph.start + start + node.nodeSize) destination = paragraph.start + start + node.nodeSize;
|
|
148
|
+
});
|
|
149
|
+
return destination === position ? address : this.addressAt(destination);
|
|
150
|
+
}
|
|
151
|
+
positionAt(address) {
|
|
152
|
+
const paragraph = this.paragraph(address.blockId);
|
|
153
|
+
if (!sameStory(address.story, this.story) || paragraph === void 0 || !Number.isInteger(address.offset) || address.offset < 0 || address.offset > paragraph.text.length || splitsSurrogatePair(paragraph.text, address.offset)) return refuse("The canonical selection is outside a plain paragraph.");
|
|
154
|
+
const gaps = paragraph.boundaries.at(address.offset) ?? panic("Missing canonical boundary");
|
|
155
|
+
const ordinal = address.zeroWidthBefore ?? (gaps.at(-1) ?? panic("Missing canonical gap")).zeroWidthBefore;
|
|
156
|
+
const gap = gaps.find(({ zeroWidthBefore }) => zeroWidthBefore === ordinal);
|
|
157
|
+
if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal > (gaps.at(-1)?.zeroWidthBefore ?? 0)) return refuse("The canonical selection is outside a paragraph gap.");
|
|
158
|
+
if (gap === void 0) return refuse("This source gap is inside a collapsed or erased inline boundary and has no editor position.");
|
|
159
|
+
return Result.ok(paragraph.start + gap.position);
|
|
160
|
+
}
|
|
161
|
+
selectionAt(state) {
|
|
162
|
+
if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
|
|
163
|
+
const range = canonicalSelectionRange(state);
|
|
164
|
+
const type = state.selection instanceof AllSelection ? "all" : "text";
|
|
165
|
+
const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
|
|
166
|
+
if (anchor.isErr()) return anchor;
|
|
167
|
+
const head = this.addressAt(type === "all" ? range.to : state.selection.head);
|
|
168
|
+
if (head.isErr()) return head;
|
|
169
|
+
return Result.ok({
|
|
170
|
+
type,
|
|
171
|
+
anchor: anchor.value,
|
|
172
|
+
head: head.value
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
paragraph(blockId) {
|
|
176
|
+
return this.paragraphsById.get(blockId);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
/** Immutable operations retain content identities for every untouched story. */
|
|
180
|
+
const changedStories = ({ document, previous }) => {
|
|
181
|
+
const stories = [];
|
|
182
|
+
if (document.package.document.content !== previous.package.document.content) stories.push(OP_STORIES.MAIN);
|
|
183
|
+
for (const kind of ["header", "footer"]) {
|
|
184
|
+
const parts = kind === "header" ? document.package.headers : document.package.footers;
|
|
185
|
+
const prior = kind === "header" ? previous.package.headers : previous.package.footers;
|
|
186
|
+
if (parts === prior) continue;
|
|
187
|
+
for (const [rId, body] of parts ?? []) if (body.content !== prior?.get(rId)?.content) stories.push({
|
|
188
|
+
kind,
|
|
189
|
+
rId
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const kind of ["footnote", "endnote"]) {
|
|
193
|
+
const notes = kind === "footnote" ? document.package.footnotes : document.package.endnotes;
|
|
194
|
+
const prior = kind === "footnote" ? previous.package.footnotes : previous.package.endnotes;
|
|
195
|
+
if (notes === prior) continue;
|
|
196
|
+
const priorById = new Map(prior?.map((note) => [note.id, note]));
|
|
197
|
+
for (const note of notes ?? []) {
|
|
198
|
+
const original = priorById.get(note.id);
|
|
199
|
+
if (note.content !== original?.content || note.noteType !== original?.noteType) stories.push({
|
|
200
|
+
kind,
|
|
201
|
+
id: note.id
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return stories;
|
|
206
|
+
};
|
|
207
|
+
const preservePropertySources = ({ target, source, stories = documentStories(source) }) => {
|
|
208
|
+
for (const story of stories) {
|
|
209
|
+
const originals = findStoryBody(source, story)?.content ?? [];
|
|
210
|
+
const derived = findStoryBody(target, story)?.content ?? [];
|
|
211
|
+
if (originals === derived) continue;
|
|
212
|
+
const paragraphs = new Map(derived.flatMap((block) => block.type === "paragraph" ? [[block.paraId, block]] : []));
|
|
213
|
+
for (const [index, original] of originals.entries()) {
|
|
214
|
+
if (original.type !== "paragraph") continue;
|
|
215
|
+
const next = original.paraId === void 0 ? derived.at(index) : paragraphs.get(original.paraId);
|
|
216
|
+
if (next?.type === "paragraph" && next !== original) copyParagraphPropertySource(next, original);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
copyDocumentParagraphPropertySourceContract(target, source);
|
|
220
|
+
};
|
|
221
|
+
const unsupportedSeedReason = (document, stories = documentStories(document)) => {
|
|
222
|
+
for (const story of stories) {
|
|
223
|
+
const body = findStoryBody(document, story);
|
|
224
|
+
if (story !== OP_STORIES.MAIN && (story.kind === "footnote" || story.kind === "endnote")) {
|
|
225
|
+
const note = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id);
|
|
226
|
+
if (note?.noteType !== void 0 && note.noteType !== "normal") continue;
|
|
227
|
+
}
|
|
228
|
+
const content = body?.content;
|
|
229
|
+
if (!content || content.length === 0) return "Canonical sessions require a nonempty paragraph story.";
|
|
230
|
+
for (const paragraph of content) {
|
|
231
|
+
if (paragraph.type !== "paragraph" || paragraph.reviewCarrier !== void 0) return "Canonical sessions currently require plain paragraphs and supported inline atoms.";
|
|
232
|
+
const issue = canonicalInlineSourceIssue(paragraph.content);
|
|
233
|
+
if (issue !== null) return issue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
|
|
239
|
+
const body = findStoryBody(document, story);
|
|
240
|
+
if (!body) return refuse("The canonical story no longer exists.");
|
|
241
|
+
const currentStyles = Object.hasOwn(document.package, "styles") ? document.package.styles : styles;
|
|
242
|
+
const options = {
|
|
243
|
+
...currentStyles == null ? {} : { styles: currentStyles },
|
|
244
|
+
...document.package.theme === void 0 ? {} : { theme: document.package.theme }
|
|
245
|
+
};
|
|
246
|
+
const converted = Result.try({
|
|
247
|
+
try: () => {
|
|
248
|
+
if (story === OP_STORIES.MAIN) return toProseDoc(document, options);
|
|
249
|
+
if (story.kind === "header" || story.kind === "footer") return headerFooterToProseDoc(body.content, options);
|
|
250
|
+
return footnoteToProseDoc(body.content, options);
|
|
251
|
+
},
|
|
252
|
+
catch: (cause) => new CanonicalSessionError({
|
|
253
|
+
gap: CANONICAL_GAP.dispatch,
|
|
254
|
+
reason: "refused",
|
|
255
|
+
message: `Canonical projection failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
256
|
+
})
|
|
257
|
+
});
|
|
258
|
+
if (converted.isErr()) return converted;
|
|
259
|
+
const pairedBookmarkIds = collectPairedBookmarkIds(body.content);
|
|
260
|
+
const paragraphs = [];
|
|
261
|
+
let failure;
|
|
262
|
+
converted.value.forEach((node, offset, index) => {
|
|
263
|
+
const source = body.content.at(index);
|
|
264
|
+
if (source?.type !== "paragraph" || source.paraId === void 0) {
|
|
265
|
+
failure = new CanonicalSessionError({
|
|
266
|
+
gap: CANONICAL_GAP.dispatch,
|
|
267
|
+
reason: "refused",
|
|
268
|
+
message: "The projection changed paragraph structure."
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
|
|
273
|
+
failure = new CanonicalSessionError({
|
|
274
|
+
gap: CANONICAL_GAP.dispatch,
|
|
275
|
+
reason: "refused",
|
|
276
|
+
message: "The projection changed paragraph structure."
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const inline = projectCanonicalInline({
|
|
281
|
+
source,
|
|
282
|
+
paragraph: node,
|
|
283
|
+
pairedBookmarkIds
|
|
284
|
+
});
|
|
285
|
+
if (inline.isErr()) {
|
|
286
|
+
failure = new CanonicalSessionError({
|
|
287
|
+
gap: CANONICAL_GAP.dispatch,
|
|
288
|
+
reason: "refused",
|
|
289
|
+
message: inline.error.message
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
paragraphs.push({
|
|
294
|
+
blockId: source.paraId,
|
|
295
|
+
start: offset + 1,
|
|
296
|
+
...inline.value,
|
|
297
|
+
node,
|
|
298
|
+
source
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
if (failure !== void 0) return Result.err(failure);
|
|
302
|
+
if (paragraphs.length !== body.content.length) return refuse("The projection changed paragraph structure.");
|
|
303
|
+
return Result.ok(new CanonicalProjection({
|
|
304
|
+
doc: converted.value,
|
|
305
|
+
paragraphs,
|
|
306
|
+
story
|
|
307
|
+
}));
|
|
308
|
+
};
|
|
309
|
+
const intentStory = (intent) => {
|
|
310
|
+
switch (intent.type) {
|
|
311
|
+
case "setHyperlink":
|
|
312
|
+
case "removeHyperlink":
|
|
313
|
+
case "insertHyperlink":
|
|
314
|
+
case "replaceFragment":
|
|
315
|
+
case "moveFragment":
|
|
316
|
+
case "replaceText":
|
|
317
|
+
case "formatRun":
|
|
318
|
+
case "insertAtom": return intent.from.story;
|
|
319
|
+
case "generateTOC":
|
|
320
|
+
case "splitParagraph":
|
|
321
|
+
case "formatParagraph": return intent.at.story;
|
|
322
|
+
case "setList": return intent.items.at(0)?.at.story ?? panic("A list intent requires a paragraph.");
|
|
323
|
+
case "joinParagraphs": return intent.story;
|
|
324
|
+
case "table": return intent.operation.story;
|
|
325
|
+
default: return intent;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const UNDO_GROUP_WINDOW_MS = 500;
|
|
329
|
+
const samePosition = (left, right) => sameStory(left.story, right.story) && left.blockId === right.blockId && left.offset === right.offset && (left.zeroWidthBefore ?? 0) === (right.zeroWidthBefore ?? 0);
|
|
330
|
+
const sameSelection = (left, right) => left.type === right.type && samePosition(left.anchor, right.anchor) && samePosition(left.head, right.head);
|
|
331
|
+
const continuesGroup = (previous, next) => {
|
|
332
|
+
switch (next.semantic) {
|
|
333
|
+
case "typing":
|
|
334
|
+
case "deleteBackward":
|
|
335
|
+
case "deleteForward": return previous.grouping === "run" && next.grouping === "run" && previous.semantic === next.semantic && previous.boundary === next.boundary && next.time >= previous.time && next.time - previous.time <= UNDO_GROUP_WINDOW_MS && sameSelection(previous.postSelection, next.preSelection) && samePosition(next.preSelection.anchor, next.preSelection.head);
|
|
336
|
+
case "composition": return next.grouping === "compositionCorrection" && previous.semantic === "composition" && previous.boundary === next.boundary && sameSelection(previous.postSelection, next.preSelection);
|
|
337
|
+
case "paste":
|
|
338
|
+
case "replacement":
|
|
339
|
+
case "structure": return false;
|
|
340
|
+
default: {
|
|
341
|
+
const unreachable = next.semantic;
|
|
342
|
+
return panic(`Unknown canonical input semantic ${unreachable}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const authorizedProjections = /* @__PURE__ */ new WeakMap();
|
|
347
|
+
/** Metadata is descriptive; transaction identity and session version authorize mutations. */
|
|
348
|
+
const isCanonicalProjectionTransaction = (transaction, session) => {
|
|
349
|
+
const authorization = authorizedProjections.get(transaction);
|
|
350
|
+
return authorization?.session === session && authorization.version === session.version && transaction.doc.eq(authorization.doc);
|
|
351
|
+
};
|
|
352
|
+
/** The model publishes only after PM and every plugin accept its exact projection. */
|
|
353
|
+
const publishCanonicalProjection = ({ state, commit, session }) => {
|
|
354
|
+
if (!isCanonicalProjectionTransaction(commit.transaction, session)) return refuse("The staged canonical projection is unauthorized.");
|
|
355
|
+
const applied = Result.try({
|
|
356
|
+
try: () => state.applyTransaction(commit.transaction),
|
|
357
|
+
catch: (cause) => new CanonicalSessionError({
|
|
358
|
+
gap: CANONICAL_GAP.dispatch,
|
|
359
|
+
reason: "refused",
|
|
360
|
+
message: `Canonical plugin staging failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
if (applied.isErr()) return applied;
|
|
364
|
+
const staged = applied.value;
|
|
365
|
+
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.");
|
|
366
|
+
const published = commit.publish();
|
|
367
|
+
if (published.isErr()) return published;
|
|
368
|
+
return Result.ok(staged);
|
|
369
|
+
};
|
|
370
|
+
const storyProjectionKey = (story) => {
|
|
371
|
+
switch (story.kind) {
|
|
372
|
+
case "header":
|
|
373
|
+
case "footer": return `${story.kind}:${story.rId}`;
|
|
374
|
+
case "footnote":
|
|
375
|
+
case "endnote": return `${story.kind}:${story.id}`;
|
|
376
|
+
default: return panic(`Unknown canonical story ${story}`);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
/** Immutable model authority with a journal staged independently of the PM view. */
|
|
380
|
+
var CanonicalSession = class {
|
|
381
|
+
currentDocument;
|
|
382
|
+
currentProjection;
|
|
383
|
+
sourceBlockIds;
|
|
384
|
+
currentVersion = 0;
|
|
385
|
+
saveTouched = /* @__PURE__ */ new Set();
|
|
386
|
+
saveStructure = "stable";
|
|
387
|
+
allocateIntentIds = createEditorIntentIdAllocator();
|
|
388
|
+
mode = { type: "editing" };
|
|
389
|
+
allocatedBlockIds = /* @__PURE__ */ new Set();
|
|
390
|
+
nextBlockId = 1;
|
|
391
|
+
sourceOwners = /* @__PURE__ */ new Map();
|
|
392
|
+
storyProjections = /* @__PURE__ */ new Map();
|
|
393
|
+
currentSelection = null;
|
|
394
|
+
applied = [];
|
|
395
|
+
groupingBoundary = 0;
|
|
396
|
+
lifecycle = { type: "committed" };
|
|
397
|
+
undone = [];
|
|
398
|
+
styles;
|
|
399
|
+
constructor({ document, projection, styles }) {
|
|
400
|
+
this.currentDocument = document;
|
|
401
|
+
this.currentProjection = projection;
|
|
402
|
+
this.sourceBlockIds = new Set(packageParagraphIds(document.package));
|
|
403
|
+
for (const blockId of packageParagraphIds(document.package)) this.allocatedBlockIds.add(blockId.toUpperCase());
|
|
404
|
+
this.advanceBlockId();
|
|
405
|
+
this.styles = styles == null ? styles : structuredClone(styles);
|
|
406
|
+
this.rememberSources(document);
|
|
407
|
+
}
|
|
408
|
+
setMode(mode) {
|
|
409
|
+
if (this.mode.type !== mode.type || this.mode.type === "suggesting" && mode.type === "suggesting" && this.mode.author !== mode.author) this.breakUndoGroup();
|
|
410
|
+
this.mode = mode.type === "editing" ? { type: "editing" } : {
|
|
411
|
+
type: "suggesting",
|
|
412
|
+
author: mode.author
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
hasStyle(styleId) {
|
|
416
|
+
return (this.styles ?? this.currentDocument.package.styles)?.styles.some((style) => style.styleId === styleId) ?? false;
|
|
417
|
+
}
|
|
418
|
+
advanceBlockId() {
|
|
419
|
+
while (this.allocatedBlockIds.has(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase())) this.nextBlockId += 1;
|
|
420
|
+
}
|
|
421
|
+
freshBlockId() {
|
|
422
|
+
if (this.nextBlockId > 2147483647) return refuse("The paragraph identity space is exhausted.");
|
|
423
|
+
return Result.ok(this.nextBlockId.toString(16).padStart(8, "0").toUpperCase());
|
|
424
|
+
}
|
|
425
|
+
intentMode(document, intent) {
|
|
426
|
+
if (this.mode.type === "editing" && !this.intentNeedsIdentityIds(document, intent)) return {
|
|
427
|
+
type: "editing",
|
|
428
|
+
reservedBlockIds: this.allocatedBlockIds
|
|
429
|
+
};
|
|
430
|
+
const ids = this.allocateIntentIds(document, intent);
|
|
431
|
+
return this.mode.type === "editing" ? {
|
|
432
|
+
type: "editing",
|
|
433
|
+
newIds: ids.newIds,
|
|
434
|
+
reservedBlockIds: this.allocatedBlockIds
|
|
435
|
+
} : {
|
|
436
|
+
type: "suggesting",
|
|
437
|
+
reservedBlockIds: this.allocatedBlockIds,
|
|
438
|
+
revision: {
|
|
439
|
+
id: ids.revisionId,
|
|
440
|
+
author: this.mode.author,
|
|
441
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
442
|
+
},
|
|
443
|
+
newIds: ids.newIds
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
intentNeedsIdentityIds(document, intent) {
|
|
447
|
+
let blockIds;
|
|
448
|
+
switch (intent.type) {
|
|
449
|
+
case "table": return true;
|
|
450
|
+
case "generateTOC":
|
|
451
|
+
case "setList":
|
|
452
|
+
case "formatParagraph": return false;
|
|
453
|
+
case "setHyperlink":
|
|
454
|
+
case "removeHyperlink":
|
|
455
|
+
case "insertHyperlink":
|
|
456
|
+
case "replaceFragment":
|
|
457
|
+
case "moveFragment": return true;
|
|
458
|
+
case "replaceText":
|
|
459
|
+
case "insertAtom":
|
|
460
|
+
case "formatRun":
|
|
461
|
+
if (intent.from.blockId !== intent.to.blockId) return true;
|
|
462
|
+
blockIds = [intent.from.blockId];
|
|
463
|
+
break;
|
|
464
|
+
case "splitParagraph":
|
|
465
|
+
if (intent.to !== void 0 && intent.at.blockId !== intent.to.blockId) return true;
|
|
466
|
+
blockIds = [intent.at.blockId];
|
|
467
|
+
break;
|
|
468
|
+
case "joinParagraphs":
|
|
469
|
+
blockIds = [intent.blockId, intent.nextBlockId];
|
|
470
|
+
break;
|
|
471
|
+
default: return intent;
|
|
472
|
+
}
|
|
473
|
+
const hasIdentity = (value) => {
|
|
474
|
+
if (typeof value !== "object" || value === null) return false;
|
|
475
|
+
if (Array.isArray(value)) return value.some(hasIdentity);
|
|
476
|
+
if ("id" in value && typeof value.id === "number" && ("author" in value || "sdtType" in value)) return true;
|
|
477
|
+
return Object.values(value).some(hasIdentity);
|
|
478
|
+
};
|
|
479
|
+
const story = intentStory(intent);
|
|
480
|
+
return blockIds.some((blockId) => {
|
|
481
|
+
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);
|
|
482
|
+
return paragraph !== void 0 && hasIdentity(paragraph);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
rememberSources(document) {
|
|
486
|
+
for (const block of document.package.document.content) if (block.type === "paragraph" && block.paraId !== void 0) this.sourceOwners.set(block.paraId, block);
|
|
487
|
+
}
|
|
488
|
+
get document() {
|
|
489
|
+
if (this.isComposing) throw new CanonicalSessionError({
|
|
490
|
+
gap: CANONICAL_GAP.dispatch,
|
|
491
|
+
reason: "refused",
|
|
492
|
+
message: "Composition must finish before taking a snapshot."
|
|
493
|
+
});
|
|
494
|
+
return this.currentDocument;
|
|
495
|
+
}
|
|
496
|
+
captureSaveSnapshot() {
|
|
497
|
+
if (this.isComposing) throw new CanonicalSessionError({
|
|
498
|
+
gap: CANONICAL_GAP.save,
|
|
499
|
+
reason: "refused",
|
|
500
|
+
message: "Composition must finish before saving the canonical document."
|
|
501
|
+
});
|
|
502
|
+
const retainedBlockIdKeys = new Set([...this.sourceBlockIds, ...packageParagraphIds(this.currentDocument.package)].map((blockId) => blockId.toUpperCase()));
|
|
503
|
+
return {
|
|
504
|
+
document: cloneDocumentWithParagraphPropertySources(this.currentDocument),
|
|
505
|
+
version: this.currentVersion,
|
|
506
|
+
changedBlockIds: [...this.saveTouched].filter((blockId) => retainedBlockIdKeys.has(blockId.toUpperCase())),
|
|
507
|
+
structure: this.saveStructure
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
get projection() {
|
|
511
|
+
return this.currentProjection;
|
|
512
|
+
}
|
|
513
|
+
get selection() {
|
|
514
|
+
return this.currentSelection;
|
|
515
|
+
}
|
|
516
|
+
get version() {
|
|
517
|
+
return this.currentVersion;
|
|
518
|
+
}
|
|
519
|
+
get canUndo() {
|
|
520
|
+
return this.applied.length > 0;
|
|
521
|
+
}
|
|
522
|
+
get canRedo() {
|
|
523
|
+
return this.undone.length > 0;
|
|
524
|
+
}
|
|
525
|
+
get isComposing() {
|
|
526
|
+
return this.lifecycle.type === "composing";
|
|
527
|
+
}
|
|
528
|
+
breakUndoGroup() {
|
|
529
|
+
this.groupingBoundary += 1;
|
|
530
|
+
}
|
|
531
|
+
beginComposition() {
|
|
532
|
+
if (this.isComposing) return refuse("A canonical composition is already pending.");
|
|
533
|
+
this.lifecycle = { type: "composing" };
|
|
534
|
+
this.breakUndoGroup();
|
|
535
|
+
return Result.ok(void 0);
|
|
536
|
+
}
|
|
537
|
+
endComposition() {
|
|
538
|
+
this.lifecycle = { type: "committed" };
|
|
539
|
+
this.breakUndoGroup();
|
|
540
|
+
}
|
|
541
|
+
checkState(state, projection = this.projection) {
|
|
542
|
+
if (this.isComposing) return refuse("Composition must finish before committing another edit.");
|
|
543
|
+
if (!state.doc.eq(projection.doc)) return refuse("The input uses a stale canonical projection.");
|
|
544
|
+
return Result.ok(void 0);
|
|
545
|
+
}
|
|
546
|
+
prepareReplace(state, { from, to, text, story = OP_STORIES.MAIN, semantic = "replacement", compositionPhase, time = Date.now() }) {
|
|
547
|
+
const projected = this.projectStory(story);
|
|
548
|
+
if (projected.isErr()) return projected;
|
|
549
|
+
const projection = projected.value;
|
|
550
|
+
const checked = this.checkState(state, projection);
|
|
551
|
+
if (checked.isErr()) return checked;
|
|
552
|
+
if (!Number.isFinite(time)) return refuse("The input time is invalid.");
|
|
553
|
+
if (from > to || hasIllegalXmlCharacters(text) || /[\t\r\n]/u.test(text)) return refuse("Canonical input accepts a well-formed text replacement.");
|
|
554
|
+
if (from === to && text.length === 0) return noChange("The input makes no text change.");
|
|
555
|
+
const start = projection.inputAddressAt(from);
|
|
556
|
+
if (start.isErr()) return start;
|
|
557
|
+
const end = projection.inputAddressAt(to);
|
|
558
|
+
if (end.isErr()) return end;
|
|
559
|
+
const preSelection = projection.selectionAt(state);
|
|
560
|
+
if (preSelection.isErr()) return preSelection;
|
|
561
|
+
const intent = {
|
|
562
|
+
type: "replaceText",
|
|
563
|
+
from: start.value,
|
|
564
|
+
to: end.value,
|
|
565
|
+
text,
|
|
566
|
+
...state.storedMarks === null ? {} : { runPropsPatch: runFormattingPatchFromMarks(state.selection.$from.marks(), state.storedMarks) }
|
|
567
|
+
};
|
|
568
|
+
const compiled = compileEditorIntent(this.currentDocument, {
|
|
569
|
+
intent,
|
|
570
|
+
mode: this.intentMode(this.currentDocument, intent)
|
|
571
|
+
});
|
|
572
|
+
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
573
|
+
const { ops, selection: caret } = compiled.value;
|
|
574
|
+
const postSelection = {
|
|
575
|
+
type: "text",
|
|
576
|
+
anchor: caret,
|
|
577
|
+
head: caret
|
|
578
|
+
};
|
|
579
|
+
const preDocument = this.currentDocument;
|
|
580
|
+
const isCaret = state.selection.empty;
|
|
581
|
+
const deletion = isCaret && text.length === 0 && (semantic === "deleteBackward" || semantic === "deleteForward") ? deletionRange(state, semantic === "deleteBackward" ? "backward" : "forward") : null;
|
|
582
|
+
const grouping = isCaret && (semantic === "typing" && from === to && from === state.selection.head || deletion?.isOk() && deletion.value.from === from && deletion.value.to === to) ? "run" : "isolated";
|
|
583
|
+
return this.stage({
|
|
584
|
+
state,
|
|
585
|
+
story,
|
|
586
|
+
ops,
|
|
587
|
+
selection: postSelection,
|
|
588
|
+
origin: "input",
|
|
589
|
+
onPublish: (inverse, version) => {
|
|
590
|
+
const entry = {
|
|
591
|
+
type: "applied",
|
|
592
|
+
ops,
|
|
593
|
+
inverse,
|
|
594
|
+
preSelection: preSelection.value,
|
|
595
|
+
postSelection,
|
|
596
|
+
preDocument,
|
|
597
|
+
postDocument: this.currentDocument,
|
|
598
|
+
version,
|
|
599
|
+
origin: "input",
|
|
600
|
+
semantic,
|
|
601
|
+
grouping: compositionPhase === "correction" ? "compositionCorrection" : grouping,
|
|
602
|
+
time,
|
|
603
|
+
boundary: this.groupingBoundary
|
|
604
|
+
};
|
|
605
|
+
const group = this.applied.at(-1);
|
|
606
|
+
const previous = group?.entries.at(-1);
|
|
607
|
+
if (group !== void 0 && previous !== void 0 && continuesGroup(previous, entry)) {
|
|
608
|
+
group.entries.push(entry);
|
|
609
|
+
group.postSelection = postSelection;
|
|
610
|
+
} else this.applied.push({
|
|
611
|
+
entries: [entry],
|
|
612
|
+
undo: { type: "entries" },
|
|
613
|
+
preSelection: preSelection.value,
|
|
614
|
+
postSelection
|
|
615
|
+
});
|
|
616
|
+
this.undone.length = 0;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
prepareIntent(state, intent) {
|
|
621
|
+
return this.prepareIntents(state, { intents: [intent] });
|
|
622
|
+
}
|
|
623
|
+
prepareIntents(state, { intents, resourceOps = [], semantic = "structure" }) {
|
|
624
|
+
const first = intents.at(0);
|
|
625
|
+
if (first === void 0) return noChange("The intent batch is empty.");
|
|
626
|
+
const story = intentStory(first);
|
|
627
|
+
if (intents.some((intent) => !sameStory(story, intentStory(intent)))) return refuse("A compound input must address one story.");
|
|
628
|
+
const projected = this.projectStory(story);
|
|
629
|
+
if (projected.isErr()) return projected;
|
|
630
|
+
const checked = this.checkState(state, projected.value);
|
|
631
|
+
if (checked.isErr()) return checked;
|
|
632
|
+
const preSelection = projected.value.selectionAt(state);
|
|
633
|
+
if (preSelection.isErr()) return preSelection;
|
|
634
|
+
let document = this.currentDocument;
|
|
635
|
+
let postSelection = preSelection.value;
|
|
636
|
+
const ops = [...resourceOps];
|
|
637
|
+
const edits = [];
|
|
638
|
+
if (resourceOps.length > 0) {
|
|
639
|
+
const imported = applyDocumentOps(document, resourceOps);
|
|
640
|
+
if (imported.isErr()) return refuse(imported.error.message);
|
|
641
|
+
document = imported.value.document;
|
|
642
|
+
edits.push(imported.value);
|
|
643
|
+
}
|
|
644
|
+
for (const intent of intents) {
|
|
645
|
+
if (this.mode.type === "suggesting" && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink" || intent.type === "generateTOC")) return Result.err(new CanonicalSessionError({
|
|
646
|
+
gap: CANONICAL_GAP.trackedHyperlinkResolution,
|
|
647
|
+
reason: "refused",
|
|
648
|
+
message: "Hyperlink and TOC suggestions require serializable wrapper review provenance."
|
|
649
|
+
}));
|
|
650
|
+
const compiled = compileEditorIntent(document, {
|
|
651
|
+
intent,
|
|
652
|
+
mode: this.intentMode(document, intent),
|
|
653
|
+
firstBlockId: this.nextBlockId
|
|
654
|
+
});
|
|
655
|
+
if (compiled.isErr()) return refuse(compiled.error.message);
|
|
656
|
+
const applied = applyDocumentOps(document, compiled.value.ops);
|
|
657
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
658
|
+
document = applied.value.document;
|
|
659
|
+
edits.push(applied.value);
|
|
660
|
+
ops.push(...compiled.value.ops);
|
|
661
|
+
if (intent.type === "generateTOC") {
|
|
662
|
+
const mapping = {
|
|
663
|
+
at: intent.at,
|
|
664
|
+
after: compiled.value.selection,
|
|
665
|
+
ops: compiled.value.ops
|
|
666
|
+
};
|
|
667
|
+
postSelection = Object.assign({}, postSelection, {
|
|
668
|
+
anchor: mapTocSelection(postSelection.anchor, mapping),
|
|
669
|
+
head: mapTocSelection(postSelection.head, mapping)
|
|
670
|
+
});
|
|
671
|
+
} else if (intent.type !== "setHyperlink" && intent.type !== "removeHyperlink" && intent.type !== "formatRun" && intent.type !== "formatParagraph" && intent.type !== "setList") postSelection = {
|
|
672
|
+
type: "text",
|
|
673
|
+
anchor: compiled.value.selection,
|
|
674
|
+
head: compiled.value.selection
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const stagedApplied = {
|
|
678
|
+
...combineEdits(this.currentDocument, edits),
|
|
679
|
+
revisions: edits.flatMap(({ revisions }) => revisions)
|
|
680
|
+
};
|
|
681
|
+
return this.prepareJournalledOps({
|
|
682
|
+
state,
|
|
683
|
+
ops,
|
|
684
|
+
postSelection,
|
|
685
|
+
stagedApplied,
|
|
686
|
+
semantic
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
prepareOps(state, ops, postSelection) {
|
|
690
|
+
return this.prepareJournalledOps({
|
|
691
|
+
state,
|
|
692
|
+
ops,
|
|
693
|
+
postSelection
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
prepareJournalledOps({ state, ops, postSelection, stagedApplied, semantic = "structure" }) {
|
|
697
|
+
const story = postSelection.anchor.story;
|
|
698
|
+
const projected = this.projectStory(story);
|
|
699
|
+
if (projected.isErr()) return projected;
|
|
700
|
+
const preSelection = projected.value.selectionAt(state);
|
|
701
|
+
if (preSelection.isErr()) return preSelection;
|
|
702
|
+
if (ops.length === 0 || stagedApplied?.inverse.length === 0) return noChange("The intent makes no document change.");
|
|
703
|
+
const preDocument = this.currentDocument;
|
|
704
|
+
return this.stage({
|
|
705
|
+
state,
|
|
706
|
+
story,
|
|
707
|
+
ops,
|
|
708
|
+
selection: postSelection,
|
|
709
|
+
origin: "input",
|
|
710
|
+
...stagedApplied === void 0 ? {} : { stagedApplied },
|
|
711
|
+
onPublish: (inverse, version) => {
|
|
712
|
+
const entry = {
|
|
713
|
+
type: "applied",
|
|
714
|
+
ops,
|
|
715
|
+
inverse,
|
|
716
|
+
preSelection: preSelection.value,
|
|
717
|
+
postSelection,
|
|
718
|
+
preDocument,
|
|
719
|
+
postDocument: this.currentDocument,
|
|
720
|
+
version,
|
|
721
|
+
origin: "input",
|
|
722
|
+
semantic,
|
|
723
|
+
grouping: "isolated",
|
|
724
|
+
time: Date.now(),
|
|
725
|
+
boundary: this.groupingBoundary
|
|
726
|
+
};
|
|
727
|
+
this.applied.push({
|
|
728
|
+
entries: [entry],
|
|
729
|
+
undo: { type: "entries" },
|
|
730
|
+
preSelection: preSelection.value,
|
|
731
|
+
postSelection
|
|
732
|
+
});
|
|
733
|
+
this.undone.length = 0;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
prepareSplit(state) {
|
|
738
|
+
const from = this.projection.inputAddressAt(state.selection.from);
|
|
739
|
+
if (from.isErr()) return from;
|
|
740
|
+
const to = this.projection.inputAddressAt(state.selection.to);
|
|
741
|
+
if (to.isErr()) return to;
|
|
742
|
+
const source = this.projection.paragraph(from.value.blockId);
|
|
743
|
+
const nextStyle = (this.styles ?? this.currentDocument.package.styles)?.styles.find(({ styleId }) => styleId === source?.source.formatting?.styleId)?.next;
|
|
744
|
+
const fresh = this.freshBlockId();
|
|
745
|
+
if (fresh.isErr()) return fresh;
|
|
746
|
+
const intents = [{
|
|
747
|
+
type: "splitParagraph",
|
|
748
|
+
at: from.value,
|
|
749
|
+
to: to.value,
|
|
750
|
+
newBlockId: fresh.value
|
|
751
|
+
}];
|
|
752
|
+
if (state.selection.empty && source !== void 0 && from.value.offset === source.text.length && nextStyle !== void 0 && nextStyle !== source.source.formatting?.styleId) intents.push({
|
|
753
|
+
type: "formatParagraph",
|
|
754
|
+
at: {
|
|
755
|
+
...from.value,
|
|
756
|
+
offset: 0
|
|
757
|
+
},
|
|
758
|
+
patch: { styleId: nextStyle }
|
|
759
|
+
});
|
|
760
|
+
return this.prepareIntents(state, { intents });
|
|
761
|
+
}
|
|
762
|
+
prepareJoin(state, direction) {
|
|
763
|
+
const checked = this.checkState(state);
|
|
764
|
+
if (checked.isErr()) return checked;
|
|
765
|
+
if (!state.selection.empty) return refuse("Join requires a collapsed text selection.");
|
|
766
|
+
const at = this.projection.inputAddressAt(state.selection.from);
|
|
767
|
+
if (at.isErr()) return at;
|
|
768
|
+
const groups = editorParagraphGroups(this.currentDocument, at.value.story);
|
|
769
|
+
const index = groups.findIndex(({ paragraphs }) => paragraphs.some((paragraph) => paragraph.paraId === at.value.blockId));
|
|
770
|
+
if (index < 0 || direction === "backward" && index === 0) return noChange("There is no adjacent paragraph to join.");
|
|
771
|
+
const firstGroup = groups.at(direction === "backward" ? index - 1 : index);
|
|
772
|
+
const secondGroup = groups.at(direction === "backward" ? index : index + 1);
|
|
773
|
+
const first = firstGroup?.paragraphs.at(-1);
|
|
774
|
+
const second = secondGroup?.paragraphs.at(0);
|
|
775
|
+
if (!first?.paraId || !second?.paraId) return noChange("There is no adjacent paragraph to join.");
|
|
776
|
+
if (!isCanonicalJoinBoundary(state, direction)) return refuse("Join requires a caret at a paragraph boundary.");
|
|
777
|
+
return this.prepareIntent(state, {
|
|
778
|
+
type: "joinParagraphs",
|
|
779
|
+
story: at.value.story,
|
|
780
|
+
blockId: first.paraId,
|
|
781
|
+
nextBlockId: second.paraId
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
prepareResolve(state, { revisionIds, resolution }) {
|
|
785
|
+
const checked = this.checkState(state);
|
|
786
|
+
if (checked.isErr()) return checked;
|
|
787
|
+
const preSelection = this.projection.selectionAt(state);
|
|
788
|
+
if (preSelection.isErr()) return preSelection;
|
|
789
|
+
const ops = [{
|
|
790
|
+
type: DOCUMENT_OP_TYPES.RESOLVE_REVISION,
|
|
791
|
+
story: OP_STORIES.MAIN,
|
|
792
|
+
revisionIds,
|
|
793
|
+
decision: resolution
|
|
794
|
+
}];
|
|
795
|
+
const applied = applyDocumentOps(this.currentDocument, ops);
|
|
796
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
797
|
+
const projected = project({
|
|
798
|
+
document: applied.value.document,
|
|
799
|
+
styles: this.styles
|
|
800
|
+
});
|
|
801
|
+
if (projected.isErr()) return projected;
|
|
802
|
+
const surviving = projected.value.paragraph(preSelection.value.head.blockId);
|
|
803
|
+
const first = applied.value.document.package.document.content.at(0);
|
|
804
|
+
if (first?.type !== "paragraph" || first.paraId === void 0) return refuse("Resolution left no paragraph.");
|
|
805
|
+
let offset = surviving ? Math.min(preSelection.value.head.offset, surviving.text.length) : 0;
|
|
806
|
+
if (surviving && splitsSurrogatePair(surviving.text, offset)) offset -= 1;
|
|
807
|
+
const caret = {
|
|
808
|
+
story: OP_STORIES.MAIN,
|
|
809
|
+
blockId: surviving?.blockId ?? first.paraId,
|
|
810
|
+
offset
|
|
811
|
+
};
|
|
812
|
+
return this.prepareJournalledOps({
|
|
813
|
+
state,
|
|
814
|
+
ops,
|
|
815
|
+
postSelection: {
|
|
816
|
+
type: "text",
|
|
817
|
+
anchor: caret,
|
|
818
|
+
head: caret
|
|
819
|
+
},
|
|
820
|
+
stagedApplied: applied.value
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
projectStory(story) {
|
|
824
|
+
if (story === OP_STORIES.MAIN) return Result.ok(this.projection);
|
|
825
|
+
const key = storyProjectionKey(story);
|
|
826
|
+
const body = findStoryBody(this.currentDocument, story);
|
|
827
|
+
if (body === void 0) {
|
|
828
|
+
this.storyProjections.delete(key);
|
|
829
|
+
return refuse("The canonical story no longer exists.");
|
|
830
|
+
}
|
|
831
|
+
const cached = this.storyProjections.get(key);
|
|
832
|
+
const theme = this.currentDocument.package.theme;
|
|
833
|
+
if (cached?.content === body.content && cached.theme === theme) return Result.ok(cached.projection);
|
|
834
|
+
const projected = project({
|
|
835
|
+
document: this.currentDocument,
|
|
836
|
+
styles: this.styles,
|
|
837
|
+
story
|
|
838
|
+
});
|
|
839
|
+
if (projected.isOk()) this.storyProjections.set(key, {
|
|
840
|
+
content: body.content,
|
|
841
|
+
theme,
|
|
842
|
+
projection: projected.value
|
|
843
|
+
});
|
|
844
|
+
return projected;
|
|
845
|
+
}
|
|
846
|
+
prepareOperations(state, ops) {
|
|
847
|
+
if (ops.length === 0) return refuse("The operation batch is empty.");
|
|
848
|
+
const selected = this.projection.selectionAt(state);
|
|
849
|
+
if (selected.isErr()) return selected;
|
|
850
|
+
const preDocument = this.currentDocument;
|
|
851
|
+
return this.stage({
|
|
852
|
+
state,
|
|
853
|
+
ops,
|
|
854
|
+
selection: selected.value,
|
|
855
|
+
origin: "input",
|
|
856
|
+
onPublish: (inverse, version) => {
|
|
857
|
+
this.applied.push({
|
|
858
|
+
entries: [{
|
|
859
|
+
type: "applied",
|
|
860
|
+
ops,
|
|
861
|
+
inverse,
|
|
862
|
+
preSelection: selected.value,
|
|
863
|
+
postSelection: selected.value,
|
|
864
|
+
preDocument,
|
|
865
|
+
postDocument: this.currentDocument,
|
|
866
|
+
version,
|
|
867
|
+
origin: "input",
|
|
868
|
+
semantic: "structure",
|
|
869
|
+
grouping: "isolated",
|
|
870
|
+
time: Date.now(),
|
|
871
|
+
boundary: this.groupingBoundary
|
|
872
|
+
}],
|
|
873
|
+
undo: { type: "entries" },
|
|
874
|
+
preSelection: selected.value,
|
|
875
|
+
postSelection: selected.value
|
|
876
|
+
});
|
|
877
|
+
this.undone.length = 0;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
prepareUndo(state, story = OP_STORIES.MAIN) {
|
|
882
|
+
const group = this.applied.at(-1);
|
|
883
|
+
if (group === void 0) return refuse("There is no canonical edit to undo.");
|
|
884
|
+
const propertySourceDocument = group.entries.at(0)?.preDocument;
|
|
885
|
+
return this.stage({
|
|
886
|
+
state,
|
|
887
|
+
story,
|
|
888
|
+
ops: group.undo.type === "replayed" ? group.undo.inverse : group.entries.toReversed().flatMap((entry) => entry.inverse),
|
|
889
|
+
selection: group.preSelection,
|
|
890
|
+
origin: "undo",
|
|
891
|
+
...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
|
|
892
|
+
onPublish: (redoOps) => {
|
|
893
|
+
this.applied.pop();
|
|
894
|
+
this.undone.push({
|
|
895
|
+
type: "undone",
|
|
896
|
+
group,
|
|
897
|
+
redoOps
|
|
898
|
+
});
|
|
899
|
+
this.breakUndoGroup();
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
prepareRedo(state, story = OP_STORIES.MAIN) {
|
|
904
|
+
const undone = this.undone.at(-1);
|
|
905
|
+
if (undone === void 0) return refuse("There is no canonical edit to redo.");
|
|
906
|
+
const propertySourceDocument = undone.group.entries.at(-1)?.postDocument;
|
|
907
|
+
return this.stage({
|
|
908
|
+
state,
|
|
909
|
+
story,
|
|
910
|
+
ops: undone.redoOps,
|
|
911
|
+
selection: undone.group.postSelection,
|
|
912
|
+
origin: "redo",
|
|
913
|
+
...propertySourceDocument === void 0 ? {} : { propertySourceDocument },
|
|
914
|
+
onPublish: (inverse) => {
|
|
915
|
+
this.undone.pop();
|
|
916
|
+
this.applied.push({
|
|
917
|
+
entries: undone.group.entries,
|
|
918
|
+
undo: {
|
|
919
|
+
type: "replayed",
|
|
920
|
+
inverse
|
|
921
|
+
},
|
|
922
|
+
preSelection: undone.group.preSelection,
|
|
923
|
+
postSelection: undone.group.postSelection
|
|
924
|
+
});
|
|
925
|
+
this.breakUndoGroup();
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
stage({ state, ops, selection, origin, onPublish, propertySourceDocument, stagedApplied, story = OP_STORIES.MAIN, previousProjection }) {
|
|
930
|
+
const previous = previousProjection === void 0 ? this.projectStory(story) : Result.ok(previousProjection);
|
|
931
|
+
if (previous.isErr()) return previous;
|
|
932
|
+
const checked = this.checkState(state, previous.value);
|
|
933
|
+
if (checked.isErr()) return checked;
|
|
934
|
+
const applied = stagedApplied === void 0 ? applyDocumentOps(this.currentDocument, ops) : Result.ok(stagedApplied);
|
|
935
|
+
if (applied.isErr()) return refuse(applied.error.message);
|
|
936
|
+
if (applied.value.inverse.length === 0) return noChange("The intent makes no document change.");
|
|
937
|
+
const changed = changedStories({
|
|
938
|
+
document: applied.value.document,
|
|
939
|
+
previous: this.currentDocument
|
|
940
|
+
});
|
|
941
|
+
const unsupported = unsupportedSeedReason(applied.value.document, changed);
|
|
942
|
+
if (unsupported !== null) return refuse(unsupported);
|
|
943
|
+
preservePropertySources({
|
|
944
|
+
target: applied.value.document,
|
|
945
|
+
source: this.currentDocument,
|
|
946
|
+
stories: changed
|
|
947
|
+
});
|
|
948
|
+
if (propertySourceDocument !== void 0) preservePropertySources({
|
|
949
|
+
target: applied.value.document,
|
|
950
|
+
source: propertySourceDocument
|
|
951
|
+
});
|
|
952
|
+
const stagedSources = new Map(this.sourceOwners);
|
|
953
|
+
for (const block of applied.value.document.package.document.content) {
|
|
954
|
+
if (block.type !== "paragraph" || block.paraId === void 0) continue;
|
|
955
|
+
const source = stagedSources.get(block.paraId);
|
|
956
|
+
if (source && block !== source) copyParagraphPropertySource(block, source);
|
|
957
|
+
}
|
|
958
|
+
const projectedStory = findStoryBody(applied.value.document, story) ? story : OP_STORIES.MAIN;
|
|
959
|
+
const projected = project({
|
|
960
|
+
document: applied.value.document,
|
|
961
|
+
styles: this.styles,
|
|
962
|
+
story: projectedStory
|
|
963
|
+
});
|
|
964
|
+
if (projected.isErr()) return projected;
|
|
965
|
+
const bodyProjection = (() => {
|
|
966
|
+
if (sameStory(projectedStory, OP_STORIES.MAIN)) return projected;
|
|
967
|
+
if (ops.some(operationChangesBodyProjection)) return project({
|
|
968
|
+
document: applied.value.document,
|
|
969
|
+
styles: this.styles
|
|
970
|
+
});
|
|
971
|
+
return Result.ok(this.currentProjection);
|
|
972
|
+
})();
|
|
973
|
+
if (bodyProjection.isErr()) return bodyProjection;
|
|
974
|
+
const transaction = state.tr;
|
|
975
|
+
const before = state.doc;
|
|
976
|
+
const after = projected.value.doc;
|
|
977
|
+
let prefix = 0;
|
|
978
|
+
let from = 0;
|
|
979
|
+
while (prefix < before.childCount && prefix < after.childCount && before.child(prefix).eq(after.child(prefix))) {
|
|
980
|
+
from += before.child(prefix).nodeSize;
|
|
981
|
+
prefix += 1;
|
|
982
|
+
}
|
|
983
|
+
let oldEnd = before.childCount;
|
|
984
|
+
let newEnd = after.childCount;
|
|
985
|
+
let to = before.content.size;
|
|
986
|
+
while (oldEnd > prefix && newEnd > prefix && before.child(oldEnd - 1).eq(after.child(newEnd - 1))) {
|
|
987
|
+
oldEnd -= 1;
|
|
988
|
+
newEnd -= 1;
|
|
989
|
+
to -= before.child(oldEnd).nodeSize;
|
|
990
|
+
}
|
|
991
|
+
if (oldEnd !== prefix || newEnd !== prefix) {
|
|
992
|
+
const replacement = [];
|
|
993
|
+
for (let index = prefix; index < newEnd; index += 1) replacement.push(after.child(index));
|
|
994
|
+
transaction.replaceWith(from, to, Fragment.fromArray(replacement));
|
|
995
|
+
}
|
|
996
|
+
if (!transaction.doc.eq(projected.value.doc)) transaction.replaceWith(0, transaction.doc.content.size, projected.value.doc.content);
|
|
997
|
+
if (sameStory(selection.anchor.story, story) && sameStory(selection.head.story, story)) {
|
|
998
|
+
const anchor = projected.value.positionAt(selection.anchor);
|
|
999
|
+
const head = projected.value.positionAt(selection.head);
|
|
1000
|
+
switch (selection.type) {
|
|
1001
|
+
case "all":
|
|
1002
|
+
transaction.setSelection(new AllSelection(transaction.doc));
|
|
1003
|
+
break;
|
|
1004
|
+
case "text":
|
|
1005
|
+
if (anchor.isOk() && head.isOk()) {
|
|
1006
|
+
transaction.setSelection(TextSelection.create(transaction.doc, anchor.value, head.value));
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(state.selection.anchor, transaction.doc.content.size))));
|
|
1010
|
+
break;
|
|
1011
|
+
default: {
|
|
1012
|
+
const exhaustive = selection.type;
|
|
1013
|
+
return panic(`Unknown canonical selection type: ${exhaustive}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
transaction.setMeta(CANONICAL_PROJECTION_META, {
|
|
1018
|
+
type: "canonical",
|
|
1019
|
+
origin,
|
|
1020
|
+
version: this.version + 1
|
|
1021
|
+
});
|
|
1022
|
+
if (ops.some(operationChangesPackage)) markPackageChange(transaction);
|
|
1023
|
+
transaction.setMeta("addToHistory", false);
|
|
1024
|
+
const baseVersion = this.version;
|
|
1025
|
+
const baseBoundary = this.groupingBoundary;
|
|
1026
|
+
authorizedProjections.set(transaction, {
|
|
1027
|
+
session: this,
|
|
1028
|
+
version: baseVersion,
|
|
1029
|
+
doc: projected.value.doc
|
|
1030
|
+
});
|
|
1031
|
+
return Result.ok({
|
|
1032
|
+
transaction,
|
|
1033
|
+
document: applied.value.document,
|
|
1034
|
+
projection: projected.value,
|
|
1035
|
+
bodyProjection: bodyProjection.value,
|
|
1036
|
+
touched: applied.value.touched,
|
|
1037
|
+
version: baseVersion + 1,
|
|
1038
|
+
origin,
|
|
1039
|
+
publish: () => {
|
|
1040
|
+
if (this.isComposing || this.groupingBoundary !== baseBoundary) return refuse("The staged canonical commit crossed a composition or selection boundary.");
|
|
1041
|
+
if (this.version !== baseVersion) return refuse("The staged canonical commit is stale.");
|
|
1042
|
+
if (!isCanonicalProjectionTransaction(transaction, this)) return refuse("The staged canonical projection was changed after preparation.");
|
|
1043
|
+
for (const blockId of applied.value.touched.inserted) this.allocatedBlockIds.add(blockId.toUpperCase());
|
|
1044
|
+
for (const op of ops) if (op.type === DOCUMENT_OP_TYPES.SPLIT_BLOCK) this.allocatedBlockIds.add(op.newBlockId.toUpperCase());
|
|
1045
|
+
this.advanceBlockId();
|
|
1046
|
+
this.rememberSources(applied.value.document);
|
|
1047
|
+
if (projectedStory !== OP_STORIES.MAIN) {
|
|
1048
|
+
const body = findStoryBody(applied.value.document, projectedStory);
|
|
1049
|
+
if (body === void 0) panic("A staged story projection lost its source.");
|
|
1050
|
+
this.storyProjections.set(storyProjectionKey(projectedStory), {
|
|
1051
|
+
content: body.content,
|
|
1052
|
+
theme: applied.value.document.package.theme,
|
|
1053
|
+
projection: projected.value
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
this.currentSelection = selection;
|
|
1057
|
+
this.currentDocument = applied.value.document;
|
|
1058
|
+
this.currentProjection = bodyProjection.value;
|
|
1059
|
+
for (const ids of [
|
|
1060
|
+
applied.value.touched.modified,
|
|
1061
|
+
applied.value.touched.inserted,
|
|
1062
|
+
applied.value.touched.removed
|
|
1063
|
+
]) for (const id of ids) this.saveTouched.add(id);
|
|
1064
|
+
if (applied.value.touched.inserted.length > 0 || applied.value.touched.removed.length > 0 || ops.some(operationChangesPackage) || ops.some(operationChangesStructure)) this.saveStructure = "changed";
|
|
1065
|
+
this.currentVersion = baseVersion + 1;
|
|
1066
|
+
onPublish(applied.value.inverse, this.currentVersion);
|
|
1067
|
+
return Result.ok(void 0);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const createCanonicalSession = (document, styles) => {
|
|
1073
|
+
const unsupported = unsupportedSeedReason(document);
|
|
1074
|
+
if (unsupported !== null) return refuse(unsupported);
|
|
1075
|
+
const owned = cloneDocumentWithParagraphPropertySources(document);
|
|
1076
|
+
const normalized = normalizeForOps(owned);
|
|
1077
|
+
preservePropertySources({
|
|
1078
|
+
target: normalized,
|
|
1079
|
+
source: owned
|
|
1080
|
+
});
|
|
1081
|
+
const validated = validateOpsDocument(normalized);
|
|
1082
|
+
if (validated.isErr()) return refuse(validated.error.message);
|
|
1083
|
+
const projected = project({
|
|
1084
|
+
document: normalized,
|
|
1085
|
+
styles
|
|
1086
|
+
});
|
|
1087
|
+
if (projected.isErr()) return projected;
|
|
1088
|
+
return Result.ok(new CanonicalSession({
|
|
1089
|
+
document: normalized,
|
|
1090
|
+
projection: projected.value,
|
|
1091
|
+
styles
|
|
1092
|
+
}));
|
|
1093
|
+
};
|
|
1094
|
+
const hasDeletedParagraphMark = (node) => {
|
|
1095
|
+
const mark = node.attrs["pPrMark"];
|
|
1096
|
+
return typeof mark === "object" && mark !== null && "kind" in mark && (mark.kind === "del" || mark.kind === "moveFrom");
|
|
1097
|
+
};
|
|
1098
|
+
/** Visible graphemes concatenate across retained deletions and deleted paragraph marks. */
|
|
1099
|
+
const visibleDeletionContext = (state) => {
|
|
1100
|
+
const { selection } = state;
|
|
1101
|
+
const index = selection.$from.index(0);
|
|
1102
|
+
let first = index;
|
|
1103
|
+
while (first > 0 && hasDeletedParagraphMark(state.doc.child(first - 1))) first -= 1;
|
|
1104
|
+
let last = index;
|
|
1105
|
+
while (last + 1 < state.doc.childCount && hasDeletedParagraphMark(state.doc.child(last))) last += 1;
|
|
1106
|
+
let text = "";
|
|
1107
|
+
const physicalStarts = [];
|
|
1108
|
+
const physicalEnds = [];
|
|
1109
|
+
let visibleOffset = 0;
|
|
1110
|
+
let issue;
|
|
1111
|
+
state.doc.forEach((paragraph, paragraphOffset, paragraphIndex) => {
|
|
1112
|
+
if (paragraphIndex < first || paragraphIndex > last) return;
|
|
1113
|
+
const native = canonicalNativeCells(paragraph);
|
|
1114
|
+
if (native.isErr()) {
|
|
1115
|
+
issue = new CanonicalSessionError({
|
|
1116
|
+
gap: CANONICAL_GAP.dispatch,
|
|
1117
|
+
reason: "refused",
|
|
1118
|
+
message: native.error.message
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
for (const cell of native.value) {
|
|
1123
|
+
if (cell.type === "zeroWidth" || cell.node.marks.some((mark) => mark.type.name === "deletion")) continue;
|
|
1124
|
+
const value = cell.type === "text" ? cell.text : "";
|
|
1125
|
+
const from = paragraphOffset + 1 + cell.from;
|
|
1126
|
+
const to = paragraphOffset + 1 + cell.to;
|
|
1127
|
+
if (cell.type !== "text" && selection.from > from && selection.from < to) {
|
|
1128
|
+
issue = new CanonicalSessionError({
|
|
1129
|
+
gap: CANONICAL_GAP.dispatch,
|
|
1130
|
+
reason: "refused",
|
|
1131
|
+
message: "Character deletion cannot start inside an inline atom."
|
|
1132
|
+
});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
for (let unit = 0; unit < value.length; unit += 1) {
|
|
1136
|
+
const position = from + (cell.type === "text" ? unit : 0);
|
|
1137
|
+
physicalStarts.push(position);
|
|
1138
|
+
text += value.charAt(unit);
|
|
1139
|
+
physicalEnds.push(cell.type === "text" ? position + 1 : to);
|
|
1140
|
+
if (position < selection.from) visibleOffset += 1;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
return issue === void 0 ? Result.ok({
|
|
1145
|
+
text,
|
|
1146
|
+
physicalStarts,
|
|
1147
|
+
physicalEnds,
|
|
1148
|
+
visibleOffset
|
|
1149
|
+
}) : Result.err(issue);
|
|
1150
|
+
};
|
|
1151
|
+
const isCanonicalJoinBoundary = (state, direction) => {
|
|
1152
|
+
if (!(state.selection instanceof TextSelection) || !state.selection.empty) return false;
|
|
1153
|
+
const context = visibleDeletionContext(state);
|
|
1154
|
+
if (context.isErr()) return false;
|
|
1155
|
+
const { text, visibleOffset } = context.value;
|
|
1156
|
+
return visibleOffset === (direction === "backward" ? 0 : text.length);
|
|
1157
|
+
};
|
|
1158
|
+
/** Native character deletion consumes one whole visible grapheme. */
|
|
1159
|
+
const deletionRange = (state, direction) => {
|
|
1160
|
+
const { selection } = state;
|
|
1161
|
+
if (!(selection instanceof TextSelection)) return refuse("Canonical deletion requires a text selection.");
|
|
1162
|
+
if (!selection.empty) return Result.ok({
|
|
1163
|
+
from: selection.from,
|
|
1164
|
+
to: selection.to
|
|
1165
|
+
});
|
|
1166
|
+
const parent = selection.$from.parent;
|
|
1167
|
+
const offset = selection.$from.parentOffset;
|
|
1168
|
+
if ((direction === "backward" ? parent.childBefore(offset).node : parent.childAfter(offset).node)?.type.name === "noteMarker") return refuse("Automatic note marks cannot be deleted as characters.");
|
|
1169
|
+
const context = visibleDeletionContext(state);
|
|
1170
|
+
if (context.isErr()) return context;
|
|
1171
|
+
const { text, physicalStarts, physicalEnds, visibleOffset } = context.value;
|
|
1172
|
+
if (splitsSurrogatePair(text, visibleOffset)) return refuse("The deletion would split a surrogate pair.");
|
|
1173
|
+
if (splitsGraphemeCluster(text, visibleOffset)) return refuse("Character deletion requires a caret at a grapheme boundary.");
|
|
1174
|
+
if (direction === "backward" && visibleOffset === 0 || direction === "forward" && visibleOffset === text.length) return refuse("There is no visible character to delete in this paragraph.");
|
|
1175
|
+
let boundary = visibleOffset + (direction === "backward" ? -1 : 1);
|
|
1176
|
+
while (splitsGraphemeCluster(text, boundary)) boundary += direction === "backward" ? -1 : 1;
|
|
1177
|
+
const from = physicalStarts.at(direction === "backward" ? boundary : visibleOffset);
|
|
1178
|
+
const to = physicalEnds.at((direction === "backward" ? visibleOffset : boundary) - 1);
|
|
1179
|
+
if (from === void 0 || to === void 0) panic("Canonical deletion lost a grapheme boundary.");
|
|
1180
|
+
return Result.ok({
|
|
1181
|
+
from,
|
|
1182
|
+
to
|
|
1183
|
+
});
|
|
1184
|
+
};
|
|
1185
|
+
//#endregion
|
|
1186
|
+
export { CANONICAL_PROJECTION_META, CanonicalSessionError, createCanonicalSession, deletionRange, isCanonicalJoinBoundary, isCanonicalProjectionTransaction, publishCanonicalProjection };
|