-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathStrictMarksPlugin.ts
More file actions
134 lines (119 loc) · 4.01 KB
/
Copy pathStrictMarksPlugin.ts
File metadata and controls
134 lines (119 loc) · 4.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { Extension } from '@tiptap/core';
import { Mark, type ResolvedPos, type Schema } from '@tiptap/pm/model';
import { Plugin, PluginKey, type EditorState } from '@tiptap/pm/state';
const NONINCLUSIVE_MARKS = ['link', 'mention'] as const;
function stripNonInclusiveMarksFromSet(
schema: Schema,
marks: readonly Mark[]
): readonly Mark[] {
let out = marks.slice();
for (const name of NONINCLUSIVE_MARKS) {
const markType = schema.marks[name];
if (!markType?.isInSet(out)) continue;
out = out.filter((m) => m.type !== markType);
}
return out;
}
function stripNonInclusiveMarksIfAfterIsOut(
schema: Schema,
$from: ResolvedPos
): readonly Mark[] {
const nodeBefore = $from.nodeBefore;
if (!nodeBefore) {
return [];
}
let out = nodeBefore.marks;
for (const name of NONINCLUSIVE_MARKS) {
const markType = schema.marks[name];
if (!markType?.isInSet(out)) continue;
const afterHasMark =
$from.nodeAfter != null && markType.isInSet($from.nodeAfter.marks);
if (!afterHasMark) {
out = out.filter((m) => m.type !== markType);
}
}
return out;
}
function resolveStrictMarks(
$from: ResolvedPos,
oldState: EditorState,
newState: EditorState,
docChanged: boolean,
textLengthChanged: boolean
): readonly Mark[] {
// Editor completely empty
if (newState.doc.textContent.length === 0) {
return textLengthChanged
? []
: (oldState.storedMarks ?? newState.storedMarks ?? []);
}
// Character to the left: strictly inherit from it
if ($from.nodeBefore) {
// Non-inclusive marks: do not carry onto text typed after the last marked
// character (e.g. space exits link/mention, matching native).
return stripNonInclusiveMarksIfAfterIsOut(newState.schema, $from);
}
// Start of line with text to the right
if ($from.nodeAfter) {
if (!docChanged) {
return []; // Pure cursor movement: kill RTL mark bleeding
}
const old$ = oldState.selection.$from;
const { nodeBefore: oldBefore, nodeAfter: oldAfter } = old$;
// Styled|plain boundary after Enter/paste/etc.: inherit the left inline's marks so
// bold, code, and other marks behave the same on a new paragraph.
if (
oldBefore &&
oldAfter &&
!Mark.sameSet(oldBefore.marks, oldAfter.marks)
) {
return stripNonInclusiveMarksFromSet(newState.schema, oldBefore.marks);
}
return stripNonInclusiveMarksFromSet(
newState.schema,
$from.nodeAfter.marks
);
}
// Completely empty line
if (textLengthChanged) {
const prevEndPos = $from.before() - 1;
return prevEndPos > 0 ? newState.doc.resolve(prevEndPos).marks() : [];
}
if (oldState.storedMarks) {
return oldState.storedMarks; // Structural nav (Enter/Backspace): keep explicit marks
}
return newState.storedMarks ?? $from.marks();
}
export const StrictMarksPlugin = Extension.create({
name: 'strictMarksPlugin',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('strictMarks'),
appendTransaction(transactions, oldState, newState) {
const { selection } = newState;
if (!selection.empty) return null;
const docChanged = !oldState.doc.eq(newState.doc);
const selChanged = !oldState.selection.eq(newState.selection);
if (!docChanged && !selChanged) return null;
const isExplicitToggle =
!docChanged && transactions.some((tr) => tr.storedMarks !== null);
if (isExplicitToggle) return null;
const { $from } = selection;
const textLengthChanged =
oldState.doc.textContent.length !== newState.doc.textContent.length;
const strictMarks = resolveStrictMarks(
$from,
oldState,
newState,
docChanged,
textLengthChanged
);
const activeMarks = newState.storedMarks ?? $from.marks();
if (Mark.sameSet(strictMarks, activeMarks)) return null;
return newState.tr.setStoredMarks(strictMarks);
},
}),
];
},
});