@stll/folio-core 0.54.1 → 0.54.2
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.d.ts +1 -1
- package/dist/ai-edits/apply.js +18 -31
- package/dist/ai-edits/headless.d.ts +1 -2
- package/dist/ai-edits/headless.js +12 -27
- package/dist/ai-edits/read.d.ts +1 -0
- package/dist/ai-edits/read.js +7 -2
- package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
- package/dist/ai-edits/table-mutation-plan.js +39 -1
- package/dist/ai-suggestions/apply.d.ts +1 -1
- package/dist/ai-suggestions/apply.js +3 -2
- package/dist/compare/compare.js +2 -1
- package/dist/docx/bookmarkIds.d.ts +11 -0
- package/dist/docx/bookmarkIds.js +33 -0
- package/dist/docx/documentParser.d.ts +14 -1
- package/dist/docx/documentParser.js +17 -4
- package/dist/docx/drawingIdNormalization.js +23 -8
- package/dist/docx/ensureParaIds.d.ts +5 -0
- package/dist/docx/ensureParaIds.js +10 -2
- package/dist/docx/listNumberingInstances.js +11 -9
- package/dist/docx/noteIds.d.ts +4 -0
- package/dist/docx/noteIds.js +13 -0
- package/dist/docx/numberingIds.d.ts +14 -0
- package/dist/docx/numberingIds.js +16 -0
- package/dist/docx/numericIdAllocator.d.ts +12 -0
- package/dist/docx/numericIdAllocator.js +33 -0
- package/dist/docx/numericIdNormalization.d.ts +16 -0
- package/dist/docx/numericIdNormalization.js +174 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
- package/dist/docx/parser.js +17 -5
- package/dist/docx/rasterMime.d.ts +64 -0
- package/dist/docx/rasterMime.js +116 -0
- package/dist/docx/replyToComment.js +13 -6
- package/dist/docx/rezip.js +14 -7
- package/dist/docx/selectiveSave.js +10 -0
- package/dist/docx/serializer/partNamespaces.js +4 -2
- package/dist/docx/serializer/runSerializer.js +5 -4
- package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
- package/dist/docx/server/build.js +14 -3
- package/dist/docx/server/createBilingualDocument.js +10 -9
- package/dist/docx/streamingXmlParser.d.ts +10 -1
- package/dist/docx/streamingXmlParser.js +88 -19
- package/dist/docx/unzip.d.ts +3 -1
- package/dist/docx/unzip.js +36 -16
- package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
- package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
- package/dist/layout-bridge/convert/tableConversion.js +4 -1
- package/dist/layout-painter/index.js +2 -5
- package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
- package/dist/paged-layout/editorScrollRoot.js +72 -0
- package/dist/paged-layout/scrollToPmPosition.js +8 -22
- package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
- package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
- package/dist/prosemirror/commentIdAllocator.js +50 -25
- package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
- package/dist/prosemirror/plugins/revisionIds.js +110 -29
- package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
- package/dist/prosemirror/plugins/suggestionMode.js +196 -40
- package/dist/prosemirror/storyListNumbering.js +11 -9
- package/dist/prosemirror/textInput.d.ts +6 -1
- package/dist/prosemirror/textInput.js +9 -2
- package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
- package/dist/utils/mergeDocumentContent.d.ts +2 -2
- package/dist/utils/mergeDocumentContent.js +11 -8
- package/package.json +2 -2
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
import { Node } from "prosemirror-model";
|
|
2
2
|
//#region src/prosemirror/plugins/revisionIds.d.ts
|
|
3
|
+
declare const RevisionIdAllocationError_base: import("better-result").TaggedErrorClass<"RevisionIdAllocationError">;
|
|
4
|
+
declare class RevisionIdAllocationError extends RevisionIdAllocationError_base<{
|
|
5
|
+
message: string;
|
|
6
|
+
first: number;
|
|
7
|
+
next: number;
|
|
8
|
+
}> {}
|
|
9
|
+
/** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
|
|
10
|
+
declare const revisionIdSeedAbove: (existingIds: Iterable<number>) => number;
|
|
11
|
+
/** The next free id, including after the signed 32-bit boundary wraps. */
|
|
12
|
+
declare const nextRevisionId: () => number;
|
|
3
13
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
14
|
+
* A shared batch increments its seed locally, so a single free id is not
|
|
15
|
+
* enough: start in the largest free interval. Every contiguous batch that
|
|
16
|
+
* can fit anywhere fits here, including after loaded ids shorten the tail.
|
|
17
|
+
* Continue within this interval until another producer changes the cursor.
|
|
6
18
|
*/
|
|
19
|
+
declare const nextRevisionIdRange: () => number;
|
|
20
|
+
/** Reserve a deterministic batch without changing its caller-supplied ids. */
|
|
21
|
+
declare const reserveRevisionIds: (first: number, next: number) => void;
|
|
22
|
+
/** Shared producers must never cross a loaded or already issued id on wrap. */
|
|
23
|
+
declare const claimRevisionIds: (first: number, next: number) => void;
|
|
24
|
+
/** Mint a bounded id without reusing loaded or previously issued ids. */
|
|
7
25
|
declare function mintRevisionId(): number;
|
|
8
|
-
/**
|
|
9
|
-
* Raise the counter above `maxId` so freshly minted ids cannot collide with
|
|
10
|
-
* ids already present in a loaded document. Only ever raises.
|
|
11
|
-
*
|
|
12
|
-
* `maxId` is derived from file content, so it is untrusted: a malformed or
|
|
13
|
-
* out-of-range value is ignored outright rather than folded into range.
|
|
14
|
-
*/
|
|
26
|
+
/** Reserve a valid loaded id and prefer the range above it. */
|
|
15
27
|
declare function seedRevisionIdsAbove(maxId: number): void;
|
|
16
|
-
|
|
17
|
-
* The highest id in `doc` from any part of the annotation space.
|
|
18
|
-
*
|
|
19
|
-
* A revision, a comment and a bookmark draw their `w:id` from one space, so a
|
|
20
|
-
* counter seeded above its own kind alone still mints over another kind's id.
|
|
21
|
-
* Both allocators seed from this, and the save-time normalization avoids the
|
|
22
|
-
* same space, so the three agree on what is taken.
|
|
23
|
-
*/
|
|
24
|
-
declare function maxAnnotationIdInDoc(doc: Node): number;
|
|
28
|
+
declare const maxAnnotationIdInDoc: (doc: Node) => number;
|
|
25
29
|
/**
|
|
26
30
|
* Raise the counter above every annotation id already present in `doc`.
|
|
27
31
|
* Called from the suggestion-mode plugin's `state.init`.
|
|
28
32
|
*/
|
|
29
33
|
declare function seedRevisionIdsFromDoc(doc: Node): void;
|
|
30
34
|
//#endregion
|
|
31
|
-
export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
35
|
+
export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
1
2
|
import { MAX_REVISION_ID } from "@stll/docx-core/model";
|
|
2
3
|
//#region src/prosemirror/plugins/revisionIds.ts
|
|
3
4
|
/**
|
|
@@ -9,23 +10,98 @@ import { MAX_REVISION_ID } from "@stll/docx-core/model";
|
|
|
9
10
|
* int consumers read `w:id` into.
|
|
10
11
|
*/
|
|
11
12
|
let counter = 1;
|
|
13
|
+
const occupiedIds = /* @__PURE__ */ new Set();
|
|
14
|
+
var RevisionIdAllocationError = class extends TaggedError("RevisionIdAllocationError") {};
|
|
15
|
+
/** Batches use contiguous ids; on rollover prefer the largest unused interval. */
|
|
16
|
+
const largestFreeIntervalStart = (reservedIds) => {
|
|
17
|
+
let first = 1;
|
|
18
|
+
let bestFirst = 1;
|
|
19
|
+
let bestSize = 0;
|
|
20
|
+
const boundaries = [...reservedIds].filter((id) => id > 0).toSorted((a, b) => a - b);
|
|
21
|
+
boundaries.push(MAX_REVISION_ID + 1);
|
|
22
|
+
for (const boundary of boundaries) {
|
|
23
|
+
const size = boundary - first;
|
|
24
|
+
if (size > bestSize) {
|
|
25
|
+
bestFirst = first;
|
|
26
|
+
bestSize = size;
|
|
27
|
+
}
|
|
28
|
+
first = boundary + 1;
|
|
29
|
+
}
|
|
30
|
+
if (bestSize === 0) throw new RevisionIdAllocationError({
|
|
31
|
+
message: "Revision id space exhausted",
|
|
32
|
+
first: 1,
|
|
33
|
+
next: 1
|
|
34
|
+
});
|
|
35
|
+
return bestFirst;
|
|
36
|
+
};
|
|
37
|
+
/** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
|
|
38
|
+
const revisionIdSeedAbove = (existingIds) => {
|
|
39
|
+
const reservedIds = /* @__PURE__ */ new Set();
|
|
40
|
+
let maxId = 0;
|
|
41
|
+
for (const id of existingIds) {
|
|
42
|
+
if (!Number.isInteger(id) || id < 0 || id > MAX_REVISION_ID) continue;
|
|
43
|
+
reservedIds.add(id);
|
|
44
|
+
maxId = Math.max(maxId, id);
|
|
45
|
+
}
|
|
46
|
+
return maxId < MAX_REVISION_ID ? maxId + 1 : largestFreeIntervalStart(reservedIds);
|
|
47
|
+
};
|
|
48
|
+
/** The next free id, including after the signed 32-bit boundary wraps. */
|
|
49
|
+
const nextRevisionId = () => {
|
|
50
|
+
if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
|
|
51
|
+
while (occupiedIds.has(counter)) {
|
|
52
|
+
counter += 1;
|
|
53
|
+
if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
|
|
54
|
+
if (occupiedIds.size >= MAX_REVISION_ID) throw new RevisionIdAllocationError({
|
|
55
|
+
message: "Revision id space exhausted",
|
|
56
|
+
first: counter,
|
|
57
|
+
next: counter
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return counter;
|
|
61
|
+
};
|
|
12
62
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
63
|
+
* A shared batch increments its seed locally, so a single free id is not
|
|
64
|
+
* enough: start in the largest free interval. Every contiguous batch that
|
|
65
|
+
* can fit anywhere fits here, including after loaded ids shorten the tail.
|
|
66
|
+
* Continue within this interval until another producer changes the cursor.
|
|
15
67
|
*/
|
|
68
|
+
const nextRevisionIdRange = () => {
|
|
69
|
+
counter = largestFreeIntervalStart(occupiedIds);
|
|
70
|
+
return counter;
|
|
71
|
+
};
|
|
72
|
+
const validateRevisionIdRange = (first, next) => {
|
|
73
|
+
if (!Number.isInteger(first) || !Number.isInteger(next) || first < 0 || next < first || next > MAX_REVISION_ID + 1) throw new RevisionIdAllocationError({
|
|
74
|
+
message: "Revision id range exceeds the OOXML limit",
|
|
75
|
+
first,
|
|
76
|
+
next
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
/** Reserve a deterministic batch without changing its caller-supplied ids. */
|
|
80
|
+
const reserveRevisionIds = (first, next) => {
|
|
81
|
+
validateRevisionIdRange(first, next);
|
|
82
|
+
for (let id = first; id < next; id += 1) occupiedIds.add(id);
|
|
83
|
+
if (next > counter) counter = next;
|
|
84
|
+
};
|
|
85
|
+
/** Shared producers must never cross a loaded or already issued id on wrap. */
|
|
86
|
+
const claimRevisionIds = (first, next) => {
|
|
87
|
+
validateRevisionIdRange(first, next);
|
|
88
|
+
for (let id = first; id < next && id <= MAX_REVISION_ID; id += 1) if (occupiedIds.has(id)) throw new RevisionIdAllocationError({
|
|
89
|
+
message: "Revision id range overlaps an occupied id",
|
|
90
|
+
first,
|
|
91
|
+
next
|
|
92
|
+
});
|
|
93
|
+
reserveRevisionIds(first, next);
|
|
94
|
+
};
|
|
95
|
+
/** Mint a bounded id without reusing loaded or previously issued ids. */
|
|
16
96
|
function mintRevisionId() {
|
|
17
|
-
|
|
18
|
-
|
|
97
|
+
const id = nextRevisionId();
|
|
98
|
+
claimRevisionIds(id, id + 1);
|
|
99
|
+
return id;
|
|
19
100
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Raise the counter above `maxId` so freshly minted ids cannot collide with
|
|
22
|
-
* ids already present in a loaded document. Only ever raises.
|
|
23
|
-
*
|
|
24
|
-
* `maxId` is derived from file content, so it is untrusted: a malformed or
|
|
25
|
-
* out-of-range value is ignored outright rather than folded into range.
|
|
26
|
-
*/
|
|
101
|
+
/** Reserve a valid loaded id and prefer the range above it. */
|
|
27
102
|
function seedRevisionIdsAbove(maxId) {
|
|
28
|
-
if (!Number.isInteger(maxId) || maxId < 0 || maxId
|
|
103
|
+
if (!Number.isInteger(maxId) || maxId < 0 || maxId > MAX_REVISION_ID) return;
|
|
104
|
+
occupiedIds.add(maxId);
|
|
29
105
|
if (maxId >= counter) counter = maxId + 1;
|
|
30
106
|
}
|
|
31
107
|
/** Node attrs that carry a revision id directly (`revisionId` field). */
|
|
@@ -33,40 +109,45 @@ const DIRECT_REVISION_ATTR_KEYS = ["trIns", "trDel"];
|
|
|
33
109
|
/**
|
|
34
110
|
* The highest id in `doc` from any part of the annotation space.
|
|
35
111
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Both allocators seed from this, and the save-time normalization avoids the
|
|
39
|
-
* same space, so the three agree on what is taken.
|
|
112
|
+
* Preserve the editor's conservative annotation seeding: revisions reserve
|
|
113
|
+
* comment and bookmark ids too, although OOXML defines separate id spaces.
|
|
40
114
|
*/
|
|
41
|
-
|
|
115
|
+
const visitAnnotationIds = (doc, visit) => {
|
|
42
116
|
let max = 0;
|
|
43
117
|
const consider = (id) => {
|
|
44
|
-
if (typeof id === "number" && Number.isInteger(id) && id
|
|
118
|
+
if (typeof id === "number" && Number.isInteger(id) && id >= 0 && id <= MAX_REVISION_ID) {
|
|
119
|
+
visit(id);
|
|
120
|
+
max = Math.max(max, id);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const considerMetadata = (value) => {
|
|
124
|
+
if (typeof value !== "object" || value === null) return;
|
|
125
|
+
for (const [key, child] of Object.entries(value)) if (key === "id" || key === "revisionId" || key === "commentId") consider(child);
|
|
126
|
+
else considerMetadata(child);
|
|
45
127
|
};
|
|
46
128
|
doc.descendants((node) => {
|
|
47
129
|
for (const mark of node.marks) {
|
|
48
130
|
consider(mark.attrs["revisionId"]);
|
|
49
131
|
consider(mark.attrs["commentId"]);
|
|
132
|
+
considerMetadata(mark.attrs["_docxRevisionAncestors"]);
|
|
133
|
+
considerMetadata(mark.attrs["changes"]);
|
|
50
134
|
}
|
|
51
135
|
if (node.type.name === "bookmarkBoundary") consider(node.attrs["id"]);
|
|
52
136
|
const attrs = node.attrs;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const key of DIRECT_REVISION_ATTR_KEYS)
|
|
56
|
-
|
|
57
|
-
if (value) consider(value.revisionId);
|
|
58
|
-
}
|
|
59
|
-
const cellMarker = attrs["cellMarker"];
|
|
60
|
-
if (cellMarker?.info) consider(cellMarker.info.revisionId);
|
|
137
|
+
considerMetadata(attrs["_propertyChanges"]);
|
|
138
|
+
considerMetadata(attrs["pPrMark"]);
|
|
139
|
+
for (const key of DIRECT_REVISION_ATTR_KEYS) considerMetadata(attrs[key]);
|
|
140
|
+
considerMetadata(attrs["cellMarker"]);
|
|
61
141
|
});
|
|
62
142
|
return max;
|
|
63
|
-
}
|
|
143
|
+
};
|
|
144
|
+
const maxAnnotationIdInDoc = (doc) => visitAnnotationIds(doc, () => {});
|
|
64
145
|
/**
|
|
65
146
|
* Raise the counter above every annotation id already present in `doc`.
|
|
66
147
|
* Called from the suggestion-mode plugin's `state.init`.
|
|
67
148
|
*/
|
|
68
149
|
function seedRevisionIdsFromDoc(doc) {
|
|
69
|
-
|
|
150
|
+
visitAnnotationIds(doc, seedRevisionIdsAbove);
|
|
70
151
|
}
|
|
71
152
|
//#endregion
|
|
72
|
-
export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
153
|
+
export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
|
|
@@ -66,8 +66,8 @@ declare function handleSuggestionEnter(view: EditorView, pluginState: Suggestion
|
|
|
66
66
|
* - Delete-at-paragraph-end → current paragraph.
|
|
67
67
|
*
|
|
68
68
|
* Returns `null` when there is no adjacent sibling paragraph (the join would
|
|
69
|
-
* not produce paragraph-mark merging
|
|
70
|
-
*
|
|
69
|
+
* not produce paragraph-mark merging, e.g. doc start, doc end, or a table).
|
|
70
|
+
* Bookmark markers at an edge do not hide the adjacent paragraph break.
|
|
71
71
|
*/
|
|
72
72
|
declare function paragraphBoundaryTarget(state: EditorState, direction: "backward" | "forward"): number | null;
|
|
73
73
|
/** Apply the editor's selected-content deletion path for a host cut action. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expectParagraphAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
|
|
2
|
+
import { clearIndentOnBackspace } from "../commands/clearParagraphIndent.js";
|
|
2
3
|
import { paragraphPropertiesSnapshot } from "../commands/propertyChangeScope.js";
|
|
3
4
|
import { paragraphEndsItsContainer } from "../containerFinalParagraph.js";
|
|
4
5
|
import { encloseWholeControls } from "../contentControlRevisions.js";
|
|
@@ -10,14 +11,16 @@ import { rebaseParagraphRuns } from "../rebaseParagraphRunFormatting.js";
|
|
|
10
11
|
import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
|
|
11
12
|
import { paragraphRunStyleContext } from "../runStyleFormatting.js";
|
|
12
13
|
import { cellPasteRange, pasteTableCells } from "../tableCellPaste.js";
|
|
13
|
-
import { handleEditorBeforeInput } from "../textInput.js";
|
|
14
|
+
import { afterNativeCompositionFlush, handleEditorBeforeInput } from "../textInput.js";
|
|
14
15
|
import { canCarryTrackedRunMark } from "../trackedRunInlineAtoms.js";
|
|
15
16
|
import { getDocumentNumbering } from "./documentNumbering.js";
|
|
16
|
-
import { getDocumentStyleResolver } from "./documentStyleState.js";
|
|
17
|
+
import { documentStylesKey, getDocumentStyleResolver } from "./documentStyleState.js";
|
|
17
18
|
import { mintRevisionId, seedRevisionIdsFromDoc } from "./revisionIds.js";
|
|
18
19
|
import { Slice } from "prosemirror-model";
|
|
20
|
+
import { CellSelection } from "prosemirror-tables";
|
|
19
21
|
import { Mapping, ReplaceStep } from "prosemirror-transform";
|
|
20
|
-
import { AllSelection, Plugin, PluginKey, Selection, TextSelection } from "prosemirror-state";
|
|
22
|
+
import { AllSelection, EditorState, Plugin, PluginKey, Selection, TextSelection } from "prosemirror-state";
|
|
23
|
+
import { joinBackward, joinForward, selectNodeBackward, selectNodeForward } from "prosemirror-commands";
|
|
21
24
|
import { isHistoryTransaction } from "prosemirror-history";
|
|
22
25
|
import { undoInputRule } from "prosemirror-inputrules";
|
|
23
26
|
//#region src/prosemirror/plugins/suggestionMode.ts
|
|
@@ -241,10 +244,44 @@ function markRangeAsInserted(tr, doc, from, to, insertionType, deletionType, att
|
|
|
241
244
|
function handleSuggestionPaste(view, slice, pluginState) {
|
|
242
245
|
const { selection } = view.state;
|
|
243
246
|
const selectsAll = selection instanceof AllSelection;
|
|
244
|
-
if (!(selection instanceof TextSelection || selectsAll)
|
|
247
|
+
if (!(selection instanceof TextSelection || selectsAll)) return false;
|
|
245
248
|
const insertionType = view.state.schema.marks["insertion"];
|
|
246
249
|
const deletionType = view.state.schema.marks["deletion"];
|
|
247
250
|
if (!insertionType || !deletionType) return false;
|
|
251
|
+
if (selection.empty) {
|
|
252
|
+
const { $from } = selection;
|
|
253
|
+
const closedTable = slice.openStart === 0 && slice.openEnd === 0 && slice.content.firstChild?.type.name === "table" && slice.content.lastChild?.type.name === "table";
|
|
254
|
+
const prefix = $from.parent.content.cut(0, $from.parentOffset);
|
|
255
|
+
let deletedPrefix = prefix.size > 0;
|
|
256
|
+
prefix.forEach((node) => {
|
|
257
|
+
if (!node.marks.some((mark) => mark.type === deletionType)) deletedPrefix = false;
|
|
258
|
+
});
|
|
259
|
+
if (!closedTable || $from.parent.type.name !== "paragraph" || !deletedPrefix) return false;
|
|
260
|
+
const suffix = $from.parent.content.cut($from.parentOffset);
|
|
261
|
+
let deletedSuffix = true;
|
|
262
|
+
suffix.forEach((node) => {
|
|
263
|
+
if (!node.marks.some((mark) => mark.type === deletionType)) deletedSuffix = false;
|
|
264
|
+
});
|
|
265
|
+
const tr = view.state.tr.setMeta(SUGGESTION_META, true).setMeta("paste", true);
|
|
266
|
+
const at = $from.before();
|
|
267
|
+
tr.replaceRange(at, at, slice);
|
|
268
|
+
const revision = makeMarkAttrs(pluginState);
|
|
269
|
+
const end = tr.mapping.map(at, 1);
|
|
270
|
+
enclosePastedRunRevisions({
|
|
271
|
+
tr,
|
|
272
|
+
from: at,
|
|
273
|
+
to: end,
|
|
274
|
+
revision
|
|
275
|
+
});
|
|
276
|
+
markRangeAsInserted(tr, tr.doc, at, end, insertionType, deletionType, revision);
|
|
277
|
+
if (deletedSuffix && $from.parent.attrs["pPrMark"] == null && !paragraphEndsItsContainer($from.doc.resolve($from.before()), "paragraph")) tr.setNodeAttribute(tr.mapping.map($from.before()), "pPrMark", {
|
|
278
|
+
kind: "del",
|
|
279
|
+
info: makeParagraphMarkInfo(pluginState)
|
|
280
|
+
});
|
|
281
|
+
tr.setSelection(TextSelection.create(tr.doc, tr.mapping.map(selection.from)));
|
|
282
|
+
view.dispatch(tr.scrollIntoView());
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
248
285
|
if (selectsAll && (slice.openStart > 0 || slice.openEnd > 0)) slice = new Slice(view.state.tr.replaceSelection(slice).doc.content, 0, 0);
|
|
249
286
|
const { from, to } = selectsAll ? {
|
|
250
287
|
from: Selection.atStart(view.state.doc).from,
|
|
@@ -372,16 +409,54 @@ function handleSuggestionTableCellPaste(view, slice, pluginState) {
|
|
|
372
409
|
const insertFrom = tr.mapping.slice(mapFrom).map(to);
|
|
373
410
|
const sizeBefore = tr.doc.content.size;
|
|
374
411
|
tr.replaceRange(insertFrom, insertFrom, content);
|
|
375
|
-
|
|
412
|
+
let insertTo = insertFrom + (tr.doc.content.size - sizeBefore);
|
|
413
|
+
let pastedFrom = insertFrom;
|
|
414
|
+
const finalParagraph = cell.lastChild;
|
|
415
|
+
const $pasteFrom = tr.doc.resolve(pastedFrom);
|
|
416
|
+
const $pasteTo = tr.doc.resolve(insertTo);
|
|
417
|
+
if (content.openStart > 0 && content.openEnd > 0 && finalParagraph?.type.name === "paragraph" && $pasteFrom.parent.type.name === "paragraph" && $pasteTo.parent.type.name === "paragraph" && $pasteFrom.before() !== $pasteTo.before()) {
|
|
418
|
+
tr.setNodeAttribute($pasteFrom.before(), "pPrMark", {
|
|
419
|
+
kind: "ins",
|
|
420
|
+
info: {
|
|
421
|
+
id: revision.revisionId,
|
|
422
|
+
author: revision.author,
|
|
423
|
+
date: revision.date
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
tr.setNodeAttribute($pasteTo.before(), "pPrMark", finalParagraph.attrs["pPrMark"]);
|
|
427
|
+
recordReplacedParagraphProperties({
|
|
428
|
+
tr,
|
|
429
|
+
position: $pasteTo.before(),
|
|
430
|
+
replaced: finalParagraph,
|
|
431
|
+
revision: {
|
|
432
|
+
id: revision.revisionId,
|
|
433
|
+
author: revision.author,
|
|
434
|
+
date: revision.date
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const emptyInsertions = [];
|
|
439
|
+
tr.doc.nodeAt(cellPos)?.forEach((node, offset) => {
|
|
440
|
+
const pos = cellPos + 1 + offset;
|
|
441
|
+
if (pos < pastedFrom && node.type.name === "paragraph" && node.content.size === 0 && isCurrentAuthorParagraphInsertion(node.attrs["pPrMark"], pluginState.author)) emptyInsertions.push({
|
|
442
|
+
from: pos,
|
|
443
|
+
to: pos + node.nodeSize
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
const retraction = tr.mapping.maps.length;
|
|
447
|
+
for (const range of emptyInsertions.toReversed()) tr.delete(range.from, range.to);
|
|
448
|
+
const retracted = tr.mapping.slice(retraction);
|
|
449
|
+
pastedFrom = retracted.map(pastedFrom);
|
|
450
|
+
insertTo = retracted.map(insertTo);
|
|
376
451
|
enclosePastedRunRevisions({
|
|
377
452
|
tr,
|
|
378
|
-
from:
|
|
453
|
+
from: pastedFrom,
|
|
379
454
|
to: insertTo,
|
|
380
455
|
revision
|
|
381
456
|
});
|
|
382
|
-
markRangeAsInserted(tr, tr.doc,
|
|
383
|
-
tr.doc.nodesBetween(
|
|
384
|
-
if (node.type.name === "paragraph" && pos >=
|
|
457
|
+
markRangeAsInserted(tr, tr.doc, pastedFrom, insertTo, insertionType, deletionType, revision);
|
|
458
|
+
tr.doc.nodesBetween(pastedFrom, insertTo, (node, pos) => {
|
|
459
|
+
if (node.type.name === "paragraph" && pos >= pastedFrom && pos + node.nodeSize <= insertTo && node.attrs["pPrMark"] == null) tr.setNodeAttribute(pos, "pPrMark", {
|
|
385
460
|
kind: "ins",
|
|
386
461
|
info: {
|
|
387
462
|
id: revision.revisionId,
|
|
@@ -533,15 +608,38 @@ function suggestRangeDeletion(state, tr, from, to) {
|
|
|
533
608
|
* prior author's revision must not be silently overwritten.
|
|
534
609
|
*/
|
|
535
610
|
function handleSuggestionEnter(view, pluginState) {
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
611
|
+
const state = view.state;
|
|
612
|
+
if (state.selection.$from.parent.type.name !== "paragraph") return false;
|
|
613
|
+
const replacement = { tr: null };
|
|
614
|
+
let splitState = state;
|
|
615
|
+
if (state.selection instanceof TextSelection && !state.selection.empty) {
|
|
616
|
+
if (!handleSuggestionDelete(state, (tr) => {
|
|
617
|
+
replacement.tr = tr;
|
|
618
|
+
const styles = documentStylesKey.get(state);
|
|
619
|
+
const plugins = [];
|
|
620
|
+
if (styles) {
|
|
621
|
+
const spec = { ...styles.spec };
|
|
622
|
+
delete spec.appendTransaction;
|
|
623
|
+
delete spec.filterTransaction;
|
|
624
|
+
plugins.push(new Plugin(spec));
|
|
625
|
+
}
|
|
626
|
+
splitState = state.reconfigure({ plugins }).apply(tr);
|
|
627
|
+
}, "forward")) return false;
|
|
628
|
+
}
|
|
629
|
+
const { $from } = splitState.selection;
|
|
538
630
|
const sourcePos = $from.before();
|
|
539
631
|
const sourceAttrs = $from.parent.attrs;
|
|
540
632
|
const captured = { tr: null };
|
|
541
|
-
if (!splitBlockClearBorders(
|
|
633
|
+
if (!splitBlockClearBorders(splitState, (tr) => {
|
|
542
634
|
captured.tr = tr;
|
|
543
635
|
}, view) || !captured.tr) return false;
|
|
544
|
-
const tr = captured.tr;
|
|
636
|
+
const tr = replacement.tr ?? captured.tr;
|
|
637
|
+
if (replacement.tr) {
|
|
638
|
+
for (const step of captured.tr.steps) tr.step(step);
|
|
639
|
+
tr.setSelection(captured.tr.selection.getBookmark().resolve(tr.doc));
|
|
640
|
+
tr.setStoredMarks(captured.tr.storedMarks);
|
|
641
|
+
}
|
|
642
|
+
tr.setMeta(SUGGESTION_META, true);
|
|
545
643
|
if (sourceAttrs["pPrMark"] == null) {
|
|
546
644
|
if (tr.doc.nodeAt(sourcePos)?.type.name === "paragraph") {
|
|
547
645
|
const markInfo = {
|
|
@@ -561,6 +659,24 @@ function handleSuggestionEnter(view, pluginState) {
|
|
|
561
659
|
view.dispatch(tr.scrollIntoView());
|
|
562
660
|
return true;
|
|
563
661
|
}
|
|
662
|
+
/** Find the adjacent visible unit without consuming a bookmark range marker. */
|
|
663
|
+
function caretDeleteTarget(state, direction) {
|
|
664
|
+
const { $from } = state.selection;
|
|
665
|
+
const backward = direction === "backward";
|
|
666
|
+
let from = backward ? $from.pos - 1 : $from.pos;
|
|
667
|
+
let to = backward ? $from.pos : $from.pos + 1;
|
|
668
|
+
while (from >= $from.start() && to <= $from.end()) {
|
|
669
|
+
const adjacent = state.doc.resolve(from).nodeAfter;
|
|
670
|
+
if (adjacent?.type.name !== "bookmarkBoundary") return {
|
|
671
|
+
type: "inlineUnit",
|
|
672
|
+
from,
|
|
673
|
+
to
|
|
674
|
+
};
|
|
675
|
+
from += backward ? -adjacent.nodeSize : adjacent.nodeSize;
|
|
676
|
+
to += backward ? -adjacent.nodeSize : adjacent.nodeSize;
|
|
677
|
+
}
|
|
678
|
+
return { type: "paragraphEdge" };
|
|
679
|
+
}
|
|
564
680
|
/**
|
|
565
681
|
* Detect a caret-at-paragraph-boundary scenario where Backspace/Delete should
|
|
566
682
|
* record a tracked paragraph-mark deletion instead of joining paragraphs.
|
|
@@ -572,8 +688,8 @@ function handleSuggestionEnter(view, pluginState) {
|
|
|
572
688
|
* - Delete-at-paragraph-end → current paragraph.
|
|
573
689
|
*
|
|
574
690
|
* Returns `null` when there is no adjacent sibling paragraph (the join would
|
|
575
|
-
* not produce paragraph-mark merging
|
|
576
|
-
*
|
|
691
|
+
* not produce paragraph-mark merging, e.g. doc start, doc end, or a table).
|
|
692
|
+
* Bookmark markers at an edge do not hide the adjacent paragraph break.
|
|
577
693
|
*/
|
|
578
694
|
function paragraphBoundaryTarget(state, direction) {
|
|
579
695
|
const { $from, empty } = state.selection;
|
|
@@ -581,14 +697,13 @@ function paragraphBoundaryTarget(state, direction) {
|
|
|
581
697
|
if ($from.parent.type.name !== "paragraph") return null;
|
|
582
698
|
const paragraphStart = $from.before();
|
|
583
699
|
const paragraphEnd = $from.after();
|
|
700
|
+
if (caretDeleteTarget(state, direction).type !== "paragraphEdge") return null;
|
|
584
701
|
if (direction === "backward") {
|
|
585
|
-
if ($from.parentOffset !== 0) return null;
|
|
586
702
|
if (paragraphStart === 0) return null;
|
|
587
703
|
const prev = state.doc.resolve(paragraphStart).nodeBefore;
|
|
588
704
|
if (!prev || prev.type.name !== "paragraph") return null;
|
|
589
705
|
return paragraphStart - prev.nodeSize;
|
|
590
706
|
}
|
|
591
|
-
if ($from.parentOffset !== $from.parent.content.size) return null;
|
|
592
707
|
if (paragraphEnd >= state.doc.content.size) return null;
|
|
593
708
|
const next = state.doc.resolve(paragraphEnd).nodeAfter;
|
|
594
709
|
if (!next || next.type.name !== "paragraph") return null;
|
|
@@ -612,7 +727,13 @@ function applyPPrDel(view, targetParagraphPos, pluginState) {
|
|
|
612
727
|
const joined = view.state.doc.nodeAt(joinPos);
|
|
613
728
|
try {
|
|
614
729
|
tr.join(joinPos);
|
|
615
|
-
tr.setNodeAttribute(targetParagraphPos, "pPrMark", null);
|
|
730
|
+
tr.setNodeAttribute(targetParagraphPos, "pPrMark", joined?.type === targetNode.type ? expectParagraphAttrs(joined).pPrMark : null);
|
|
731
|
+
if (joined?.type === targetNode.type) recordReplacedParagraphProperties({
|
|
732
|
+
tr,
|
|
733
|
+
position: targetParagraphPos,
|
|
734
|
+
replaced: joined,
|
|
735
|
+
revision: existingMark.info
|
|
736
|
+
});
|
|
616
737
|
const styleResolver = getDocumentStyleResolver(view.state);
|
|
617
738
|
if (styleResolver && joined?.type === targetNode.type && joined.content.size > 0) rebaseParagraphRuns({
|
|
618
739
|
previousContext: paragraphRunStyleContext(joined, styleResolver),
|
|
@@ -671,15 +792,28 @@ function handleSuggestionDelete(state, dispatch, direction) {
|
|
|
671
792
|
to: rangeTo.pos
|
|
672
793
|
}).sort((a, b) => b.from - a.from);
|
|
673
794
|
for (const { from, to } of ranges) markRangeAsDeleted(tr, state.doc, from, to, insertionType, deletionType, pluginState, state);
|
|
674
|
-
|
|
675
|
-
|
|
795
|
+
if (state.selection instanceof CellSelection) tr.setSelection(state.selection.map(tr.doc, tr.mapping));
|
|
796
|
+
else {
|
|
797
|
+
const cursorPos = tr.mapping.map($to.pos);
|
|
798
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(cursorPos)));
|
|
799
|
+
}
|
|
676
800
|
dispatch(tr.scrollIntoView());
|
|
677
801
|
return true;
|
|
678
802
|
}
|
|
679
803
|
const isBackward = direction === "backward";
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
804
|
+
const target = caretDeleteTarget(state, direction);
|
|
805
|
+
if (target.type === "paragraphEdge") {
|
|
806
|
+
const edge = isBackward ? $from.start() : $from.end();
|
|
807
|
+
if ($from.pos === edge) return false;
|
|
808
|
+
const virtual = EditorState.create({
|
|
809
|
+
doc: state.doc,
|
|
810
|
+
selection: TextSelection.create(state.doc, edge)
|
|
811
|
+
});
|
|
812
|
+
if ((isBackward ? joinBackward : joinForward)(virtual, dispatch)) return true;
|
|
813
|
+
return (isBackward ? selectNodeBackward : selectNodeForward)(virtual, dispatch);
|
|
814
|
+
}
|
|
815
|
+
const deletePos = target.from;
|
|
816
|
+
const deleteEnd = target.to;
|
|
683
817
|
const noteRange = expandNoteReferenceDeletionRange(state.doc, deletePos, deleteEnd);
|
|
684
818
|
const rangeFrom = noteRange?.from ?? deletePos;
|
|
685
819
|
const rangeTo = noteRange?.to ?? deleteEnd;
|
|
@@ -701,19 +835,7 @@ function handleSuggestionDelete(state, dispatch, direction) {
|
|
|
701
835
|
}
|
|
702
836
|
/** Apply the editor's selected-content deletion path for a host cut action. */
|
|
703
837
|
const deleteSelectionAsSuggestion = (state, dispatch) => state.selection.empty ? false : handleSuggestionDelete(state, dispatch, "forward");
|
|
704
|
-
|
|
705
|
-
* Mark the text committed by an IME composition as a tracked insertion.
|
|
706
|
-
*
|
|
707
|
-
* Invoked from `compositionend` (deferred to a microtask) rather than from the
|
|
708
|
-
* `appendTransaction` catch-all. ProseMirror commits composed text as part of
|
|
709
|
-
* its own composition-end flush; adding the insertion mark *synchronously*
|
|
710
|
-
* during that flush re-wraps the very text node the IME just finalized, which
|
|
711
|
-
* corrupts CJK (Japanese / Chinese / Korean) input — characters duplicate or
|
|
712
|
-
* garble. Running one tick later, after the composition has settled, wraps the
|
|
713
|
-
* committed range safely. `markRangeAsInserted` skips nodes that already carry a
|
|
714
|
-
* tracked-change mark, so this is idempotent. eigenpal/docx-editor#938.
|
|
715
|
-
*/
|
|
716
|
-
function markComposedAsInsertion(view, from, pluginState, replaced) {
|
|
838
|
+
function markComposedAsInsertion({ view, from, pluginState, replaced, compositionId }) {
|
|
717
839
|
const insertionType = view.state.schema.marks["insertion"];
|
|
718
840
|
const deletionType = view.state.schema.marks["deletion"];
|
|
719
841
|
if (!insertionType || !deletionType) return;
|
|
@@ -721,9 +843,16 @@ function markComposedAsInsertion(view, from, pluginState, replaced) {
|
|
|
721
843
|
if (to <= from && !replaced) return;
|
|
722
844
|
const tr = view.state.tr;
|
|
723
845
|
tr.setMeta(SUGGESTION_META, true);
|
|
846
|
+
if (compositionId !== null) tr.setMeta("composition", compositionId);
|
|
724
847
|
if (replaced) {
|
|
725
|
-
tr.
|
|
726
|
-
markRangeAsDeleted(tr, tr.doc, from,
|
|
848
|
+
tr.replaceRange(from, from, replaced);
|
|
849
|
+
markRangeAsDeleted(tr, tr.doc, from, tr.mapping.map(from, 1), insertionType, deletionType, pluginState);
|
|
850
|
+
const restoredEnd = tr.doc.resolve(tr.mapping.map(from, 1));
|
|
851
|
+
const restoredStart = tr.doc.resolve(from);
|
|
852
|
+
if (restoredStart.parent.type.name === "paragraph" && restoredEnd.parent.type.name === "paragraph" && restoredStart.parent !== restoredEnd.parent) {
|
|
853
|
+
const mark = expectParagraphAttrs(restoredStart.parent).pPrMark;
|
|
854
|
+
if (mark?.kind === "del") carryIntoParagraphLeft(view.state, tr, view.state.doc.resolve(from).parent, restoredEnd.before(), mark.info);
|
|
855
|
+
}
|
|
727
856
|
}
|
|
728
857
|
const insertionFrom = tr.mapping.map(from, 1);
|
|
729
858
|
const insertionTo = tr.mapping.map(to, 1);
|
|
@@ -773,6 +902,7 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
|
|
|
773
902
|
let composing = false;
|
|
774
903
|
let compositionFrom = null;
|
|
775
904
|
let compositionReplaced = null;
|
|
905
|
+
let compositionId = null;
|
|
776
906
|
return new Plugin({
|
|
777
907
|
key: suggestionModeKey,
|
|
778
908
|
state: {
|
|
@@ -784,6 +914,8 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
|
|
|
784
914
|
};
|
|
785
915
|
},
|
|
786
916
|
apply(tr, state) {
|
|
917
|
+
const nativeCompositionId = tr.getMeta("composition");
|
|
918
|
+
if (composing && tr.docChanged && typeof nativeCompositionId === "number") compositionId = nativeCompositionId;
|
|
787
919
|
const meta = tr.getMeta(suggestionModeKey);
|
|
788
920
|
if (meta) return {
|
|
789
921
|
...state,
|
|
@@ -809,6 +941,7 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
|
|
|
809
941
|
if (!suggestionModeKey.getState(view.state)?.active) return false;
|
|
810
942
|
if (composing) return false;
|
|
811
943
|
composing = true;
|
|
944
|
+
compositionId = null;
|
|
812
945
|
const { from, to } = view.state.selection;
|
|
813
946
|
compositionFrom = from;
|
|
814
947
|
compositionReplaced = from === to ? null : view.state.doc.slice(from, to);
|
|
@@ -822,14 +955,23 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
|
|
|
822
955
|
compositionReplaced = null;
|
|
823
956
|
if (!pluginState?.active || from == null) {
|
|
824
957
|
composing = false;
|
|
958
|
+
compositionId = null;
|
|
825
959
|
return false;
|
|
826
960
|
}
|
|
827
|
-
|
|
961
|
+
afterNativeCompositionFlush(() => {
|
|
828
962
|
try {
|
|
829
963
|
const current = suggestionModeKey.getState(view.state);
|
|
830
|
-
|
|
964
|
+
const nativeCompositionId = compositionId;
|
|
965
|
+
if (current?.active) markComposedAsInsertion({
|
|
966
|
+
view,
|
|
967
|
+
from,
|
|
968
|
+
pluginState: current,
|
|
969
|
+
replaced,
|
|
970
|
+
compositionId: nativeCompositionId
|
|
971
|
+
});
|
|
831
972
|
} finally {
|
|
832
973
|
composing = false;
|
|
974
|
+
compositionId = null;
|
|
833
975
|
}
|
|
834
976
|
});
|
|
835
977
|
return false;
|
|
@@ -851,6 +993,20 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
|
|
|
851
993
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
852
994
|
const { $from, empty } = view.state.selection;
|
|
853
995
|
if (event.key === "Backspace" && empty && $from.parentOffset === 0 && $from.parent.type.name === "paragraph" && expectParagraphAttrs($from.parent).numPr?.kind === "reference") return false;
|
|
996
|
+
if (event.key === "Backspace" && clearIndentOnBackspace(view.state, (tr) => {
|
|
997
|
+
const revision = makeMarkAttrs(pluginState);
|
|
998
|
+
recordReplacedParagraphProperties({
|
|
999
|
+
tr,
|
|
1000
|
+
position: $from.before(),
|
|
1001
|
+
replaced: $from.parent,
|
|
1002
|
+
revision: {
|
|
1003
|
+
id: revision.revisionId,
|
|
1004
|
+
author: revision.author,
|
|
1005
|
+
date: revision.date
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
view.dispatch(tr.setMeta("suggestionModeApplied", true));
|
|
1009
|
+
})) return true;
|
|
854
1010
|
const boundaryTarget = paragraphBoundaryTarget(view.state, event.key === "Backspace" ? "backward" : "forward");
|
|
855
1011
|
if (boundaryTarget !== null) return applyPPrDel(view, boundaryTarget, pluginState);
|
|
856
1012
|
const direction = event.key === "Backspace" ? "backward" : "forward";
|