@kerebron/extension-basic-editor 0.4.28 → 0.4.29
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/esm/ExtensionBaseKeymap.js +1 -0
- package/esm/ExtensionBaseKeymap.js.map +1 -0
- package/esm/ExtensionBasicCodeEditor.js +1 -0
- package/esm/ExtensionBasicCodeEditor.js.map +1 -0
- package/esm/ExtensionBasicEditor.js +1 -0
- package/esm/ExtensionBasicEditor.js.map +1 -0
- package/esm/ExtensionDropcursor.js +1 -0
- package/esm/ExtensionDropcursor.js.map +1 -0
- package/esm/ExtensionGapcursor.js +1 -0
- package/esm/ExtensionGapcursor.js.map +1 -0
- package/esm/ExtensionHistory.js +1 -0
- package/esm/ExtensionHistory.js.map +1 -0
- package/esm/ExtensionHtml.js +1 -0
- package/esm/ExtensionHtml.js.map +1 -0
- package/esm/ExtensionMediaUpload.js +1 -0
- package/esm/ExtensionMediaUpload.js.map +1 -0
- package/esm/ExtensionSelection.js +1 -0
- package/esm/ExtensionSelection.js.map +1 -0
- package/esm/ExtensionTextAlign.js +1 -0
- package/esm/ExtensionTextAlign.js.map +1 -0
- package/esm/MarkBookmark.js +1 -0
- package/esm/MarkBookmark.js.map +1 -0
- package/esm/MarkChange.js +1 -0
- package/esm/MarkChange.js.map +1 -0
- package/esm/MarkCode.js +1 -0
- package/esm/MarkCode.js.map +1 -0
- package/esm/MarkHighlight.js +1 -0
- package/esm/MarkHighlight.js.map +1 -0
- package/esm/MarkItalic.js +1 -0
- package/esm/MarkItalic.js.map +1 -0
- package/esm/MarkLink.js +1 -0
- package/esm/MarkLink.js.map +1 -0
- package/esm/MarkStrike.js +1 -0
- package/esm/MarkStrike.js.map +1 -0
- package/esm/MarkStrong.js +1 -0
- package/esm/MarkStrong.js.map +1 -0
- package/esm/MarkSubscript.js +1 -0
- package/esm/MarkSubscript.js.map +1 -0
- package/esm/MarkSuperscript.js +1 -0
- package/esm/MarkSuperscript.js.map +1 -0
- package/esm/MarkTextColor.js +1 -0
- package/esm/MarkTextColor.js.map +1 -0
- package/esm/MarkUnderline.js +1 -0
- package/esm/MarkUnderline.js.map +1 -0
- package/esm/NodeAside.js +1 -0
- package/esm/NodeAside.js.map +1 -0
- package/esm/NodeBlockquote.js +1 -0
- package/esm/NodeBlockquote.js.map +1 -0
- package/esm/NodeBookmark.js +1 -0
- package/esm/NodeBookmark.js.map +1 -0
- package/esm/NodeBulletList.js +1 -0
- package/esm/NodeBulletList.js.map +1 -0
- package/esm/NodeCodeBlock.js +1 -0
- package/esm/NodeCodeBlock.js.map +1 -0
- package/esm/NodeDefinitionDesc.js +1 -0
- package/esm/NodeDefinitionDesc.js.map +1 -0
- package/esm/NodeDefinitionList.js +1 -0
- package/esm/NodeDefinitionList.js.map +1 -0
- package/esm/NodeDefinitionTerm.js +1 -0
- package/esm/NodeDefinitionTerm.js.map +1 -0
- package/esm/NodeDocument.js +1 -0
- package/esm/NodeDocument.js.map +1 -0
- package/esm/NodeDocumentCode.js +1 -0
- package/esm/NodeDocumentCode.js.map +1 -0
- package/esm/NodeFrontmatter.js +1 -0
- package/esm/NodeFrontmatter.js.map +1 -0
- package/esm/NodeHardBreak.js +1 -0
- package/esm/NodeHardBreak.js.map +1 -0
- package/esm/NodeHeading.js +1 -0
- package/esm/NodeHeading.js.map +1 -0
- package/esm/NodeHorizontalRule.js +1 -0
- package/esm/NodeHorizontalRule.js.map +1 -0
- package/esm/NodeImage.js +1 -0
- package/esm/NodeImage.js.map +1 -0
- package/esm/NodeInlineShortCode.js +1 -0
- package/esm/NodeInlineShortCode.js.map +1 -0
- package/esm/NodeListItem.js +1 -0
- package/esm/NodeListItem.js.map +1 -0
- package/esm/NodeMath.js +1 -0
- package/esm/NodeMath.js.map +1 -0
- package/esm/NodeOrderedList.js +1 -0
- package/esm/NodeOrderedList.js.map +1 -0
- package/esm/NodeParagraph.js +1 -0
- package/esm/NodeParagraph.js.map +1 -0
- package/esm/NodeTaskItem.js +1 -0
- package/esm/NodeTaskItem.js.map +1 -0
- package/esm/NodeTaskList.js +1 -0
- package/esm/NodeTaskList.js.map +1 -0
- package/esm/NodeText.js +1 -0
- package/esm/NodeText.js.map +1 -0
- package/esm/NodeVideo.js +1 -0
- package/esm/NodeVideo.js.map +1 -0
- package/esm/remote-selection/ExtensionRemoteSelection.js +1 -0
- package/esm/remote-selection/ExtensionRemoteSelection.js.map +1 -0
- package/esm/remote-selection/remoteSelectionPlugin.js +1 -0
- package/esm/remote-selection/remoteSelectionPlugin.js.map +1 -0
- package/package.json +6 -2
- package/src/ExtensionBaseKeymap.ts +64 -0
- package/src/ExtensionBasicCodeEditor.ts +82 -0
- package/src/ExtensionBasicEditor.ts +97 -0
- package/src/ExtensionDropcursor.ts +221 -0
- package/src/ExtensionGapcursor.ts +278 -0
- package/src/ExtensionHistory.ts +48 -0
- package/src/ExtensionHtml.ts +158 -0
- package/src/ExtensionMediaUpload.ts +258 -0
- package/src/ExtensionSelection.ts +379 -0
- package/src/ExtensionTextAlign.ts +50 -0
- package/src/MarkBookmark.ts +20 -0
- package/src/MarkChange.ts +17 -0
- package/src/MarkCode.ts +35 -0
- package/src/MarkHighlight.ts +38 -0
- package/src/MarkItalic.ts +41 -0
- package/src/MarkLink.ts +32 -0
- package/src/MarkStrike.ts +38 -0
- package/src/MarkStrong.ts +52 -0
- package/src/MarkSubscript.ts +42 -0
- package/src/MarkSuperscript.ts +42 -0
- package/src/MarkTextColor.ts +29 -0
- package/src/MarkUnderline.ts +47 -0
- package/src/NodeAside.ts +19 -0
- package/src/NodeBlockquote.ts +51 -0
- package/src/NodeBookmark.ts +23 -0
- package/src/NodeBulletList.ts +51 -0
- package/src/NodeCodeBlock.ts +60 -0
- package/src/NodeDefinitionDesc.ts +19 -0
- package/src/NodeDefinitionList.ts +46 -0
- package/src/NodeDefinitionTerm.ts +19 -0
- package/src/NodeDocument.ts +22 -0
- package/src/NodeDocumentCode.ts +33 -0
- package/src/NodeFrontmatter.ts +19 -0
- package/src/NodeHardBreak.ts +92 -0
- package/src/NodeHeading.ts +76 -0
- package/src/NodeHorizontalRule.ts +43 -0
- package/src/NodeImage.ts +36 -0
- package/src/NodeInlineShortCode.ts +55 -0
- package/src/NodeListItem.ts +320 -0
- package/src/NodeMath.ts +109 -0
- package/src/NodeOrderedList.ts +79 -0
- package/src/NodeParagraph.ts +60 -0
- package/src/NodeTaskItem.ts +190 -0
- package/src/NodeTaskList.ts +38 -0
- package/src/NodeText.ts +12 -0
- package/src/NodeVideo.ts +44 -0
- package/src/remote-selection/ExtensionRemoteSelection.ts +45 -0
- package/src/remote-selection/remoteSelectionPlugin.ts +157 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Attrs,
|
|
3
|
+
Fragment,
|
|
4
|
+
NodeRange,
|
|
5
|
+
NodeSpec,
|
|
6
|
+
type NodeType,
|
|
7
|
+
Slice,
|
|
8
|
+
} from 'prosemirror-model';
|
|
9
|
+
import type {
|
|
10
|
+
EditorState,
|
|
11
|
+
NodeSelection,
|
|
12
|
+
Transaction,
|
|
13
|
+
} from 'prosemirror-state';
|
|
14
|
+
import { Selection } from 'prosemirror-state';
|
|
15
|
+
import { type CoreEditor, Node } from '@kerebron/editor';
|
|
16
|
+
import {
|
|
17
|
+
type Command,
|
|
18
|
+
type CommandFactories,
|
|
19
|
+
type CommandShortcuts,
|
|
20
|
+
} from '@kerebron/editor/commands';
|
|
21
|
+
import {
|
|
22
|
+
canJoin,
|
|
23
|
+
canSplit,
|
|
24
|
+
liftTarget,
|
|
25
|
+
ReplaceAroundStep,
|
|
26
|
+
} from 'prosemirror-transform';
|
|
27
|
+
|
|
28
|
+
/// Build a command that splits a non-empty textblock at the top level
|
|
29
|
+
/// of a list item by also splitting that list item.
|
|
30
|
+
export function splitListItem(itemType: NodeType, itemAttrs?: Attrs): Command {
|
|
31
|
+
const cmd: Command = function (
|
|
32
|
+
state: EditorState,
|
|
33
|
+
dispatch?: (tr: Transaction) => void,
|
|
34
|
+
) {
|
|
35
|
+
const { $from, $to, node } = state.selection as NodeSelection;
|
|
36
|
+
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const grandParent = $from.node(-1);
|
|
40
|
+
if (grandParent.type != itemType) return false;
|
|
41
|
+
if (
|
|
42
|
+
$from.parent.content.size == 0 &&
|
|
43
|
+
$from.node(-1).childCount == $from.indexAfter(-1)
|
|
44
|
+
) {
|
|
45
|
+
// In an empty block. If this is a nested list, the wrapping
|
|
46
|
+
// list item should be split. Otherwise, bail out and let next
|
|
47
|
+
// command handle lifting.
|
|
48
|
+
if (
|
|
49
|
+
$from.depth == 3 || $from.node(-3).type != itemType ||
|
|
50
|
+
$from.index(-2) != $from.node(-2).childCount - 1
|
|
51
|
+
) return false;
|
|
52
|
+
if (dispatch) {
|
|
53
|
+
let wrap = Fragment.empty;
|
|
54
|
+
const depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3;
|
|
55
|
+
// Build a fragment containing empty versions of the structure
|
|
56
|
+
// from the outer list item to the parent node of the cursor
|
|
57
|
+
for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--) {
|
|
58
|
+
wrap = Fragment.from($from.node(d).copy(wrap));
|
|
59
|
+
}
|
|
60
|
+
let depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount
|
|
61
|
+
? 1
|
|
62
|
+
: $from.indexAfter(-2) < $from.node(-3).childCount
|
|
63
|
+
? 2
|
|
64
|
+
: 3;
|
|
65
|
+
// Add a second list item with an empty default start node
|
|
66
|
+
wrap = wrap.append(Fragment.from(itemType.createAndFill()));
|
|
67
|
+
let start = $from.before($from.depth - (depthBefore - 1));
|
|
68
|
+
let tr = state.tr.replace(
|
|
69
|
+
start,
|
|
70
|
+
$from.after(-depthAfter),
|
|
71
|
+
new Slice(wrap, 4 - depthBefore, 0),
|
|
72
|
+
);
|
|
73
|
+
let sel = -1;
|
|
74
|
+
tr.doc.nodesBetween(start, tr.doc.content.size, (node, pos) => {
|
|
75
|
+
if (sel > -1) return false;
|
|
76
|
+
if (node.isTextblock && node.content.size == 0) sel = pos + 1;
|
|
77
|
+
});
|
|
78
|
+
if (sel > -1) tr.setSelection(Selection.near(tr.doc.resolve(sel)));
|
|
79
|
+
dispatch(tr.scrollIntoView());
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
let nextType = $to.pos == $from.end()
|
|
84
|
+
? grandParent.contentMatchAt(0).defaultType
|
|
85
|
+
: null;
|
|
86
|
+
let tr = state.tr.delete($from.pos, $to.pos);
|
|
87
|
+
let types = nextType
|
|
88
|
+
? [itemAttrs ? { type: itemType, attrs: itemAttrs } : null, {
|
|
89
|
+
type: nextType,
|
|
90
|
+
}]
|
|
91
|
+
: undefined;
|
|
92
|
+
if (!canSplit(tr.doc, $from.pos, 2, types)) return false;
|
|
93
|
+
if (dispatch) dispatch(tr.split($from.pos, 2, types).scrollIntoView());
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
cmd.displayName = `splitListItem(${itemType.name})`;
|
|
98
|
+
|
|
99
|
+
return cmd;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Acts like [`splitListItem`](#schema-list.splitListItem), but
|
|
103
|
+
/// without resetting the set of active marks at the cursor.
|
|
104
|
+
function splitListItemKeepMarks(
|
|
105
|
+
itemType: NodeType,
|
|
106
|
+
itemAttrs?: Attrs,
|
|
107
|
+
): Command {
|
|
108
|
+
let split = splitListItem(itemType, itemAttrs);
|
|
109
|
+
return (state, dispatch) => {
|
|
110
|
+
return split(
|
|
111
|
+
state,
|
|
112
|
+
dispatch && ((tr) => {
|
|
113
|
+
let marks = state.storedMarks ||
|
|
114
|
+
(state.selection.$to.parentOffset && state.selection.$from.marks());
|
|
115
|
+
if (marks) tr.ensureMarks(marks);
|
|
116
|
+
dispatch(tr);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Create a command to lift the list item around the selection up into
|
|
123
|
+
/// a wrapping list.
|
|
124
|
+
export function liftListItem(itemType: NodeType): Command {
|
|
125
|
+
return function (state: EditorState, dispatch?: (tr: Transaction) => void) {
|
|
126
|
+
let { $from, $to } = state.selection;
|
|
127
|
+
let range = $from.blockRange(
|
|
128
|
+
$to,
|
|
129
|
+
(node) => node.childCount > 0 && node.firstChild!.type == itemType,
|
|
130
|
+
);
|
|
131
|
+
if (!range) return false;
|
|
132
|
+
if (!dispatch) return true;
|
|
133
|
+
if ($from.node(range.depth - 1).type == itemType) { // Inside a parent list
|
|
134
|
+
return liftToOuterList(state, dispatch, itemType, range);
|
|
135
|
+
} // Outer list node
|
|
136
|
+
else {
|
|
137
|
+
return liftOutOfList(state, dispatch, range);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function liftToOuterList(
|
|
143
|
+
state: EditorState,
|
|
144
|
+
dispatch: (tr: Transaction) => void,
|
|
145
|
+
itemType: NodeType,
|
|
146
|
+
range: NodeRange,
|
|
147
|
+
) {
|
|
148
|
+
let tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth);
|
|
149
|
+
if (end < endOfList) {
|
|
150
|
+
// There are siblings after the lifted items, which must become
|
|
151
|
+
// children of the last item
|
|
152
|
+
tr.step(
|
|
153
|
+
new ReplaceAroundStep(
|
|
154
|
+
end - 1,
|
|
155
|
+
endOfList,
|
|
156
|
+
end,
|
|
157
|
+
endOfList,
|
|
158
|
+
new Slice(
|
|
159
|
+
Fragment.from(itemType.create(null, range.parent.copy())),
|
|
160
|
+
1,
|
|
161
|
+
0,
|
|
162
|
+
),
|
|
163
|
+
1,
|
|
164
|
+
true,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
range = new NodeRange(
|
|
168
|
+
tr.doc.resolve(range.$from.pos),
|
|
169
|
+
tr.doc.resolve(endOfList),
|
|
170
|
+
range.depth,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const target = liftTarget(range);
|
|
174
|
+
if (target == null) return false;
|
|
175
|
+
tr.lift(range, target);
|
|
176
|
+
let $after = tr.doc.resolve(tr.mapping.map(end, -1) - 1);
|
|
177
|
+
if (
|
|
178
|
+
canJoin(tr.doc, $after.pos) &&
|
|
179
|
+
$after.nodeBefore!.type == $after.nodeAfter!.type
|
|
180
|
+
) tr.join($after.pos);
|
|
181
|
+
dispatch(tr.scrollIntoView());
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function liftOutOfList(
|
|
186
|
+
state: EditorState,
|
|
187
|
+
dispatch: (tr: Transaction) => void,
|
|
188
|
+
range: NodeRange,
|
|
189
|
+
) {
|
|
190
|
+
let tr = state.tr, list = range.parent;
|
|
191
|
+
// Merge the list items into a single big item
|
|
192
|
+
for (
|
|
193
|
+
let pos = range.end, i = range.endIndex - 1, e = range.startIndex;
|
|
194
|
+
i > e;
|
|
195
|
+
i--
|
|
196
|
+
) {
|
|
197
|
+
pos -= list.child(i).nodeSize;
|
|
198
|
+
tr.delete(pos - 1, pos + 1);
|
|
199
|
+
}
|
|
200
|
+
let $start = tr.doc.resolve(range.start), item = $start.nodeAfter!;
|
|
201
|
+
if (tr.mapping.map(range.end) != range.start + $start.nodeAfter!.nodeSize) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
let atStart = range.startIndex == 0,
|
|
205
|
+
atEnd = range.endIndex == list.childCount;
|
|
206
|
+
let parent = $start.node(-1), indexBefore = $start.index(-1);
|
|
207
|
+
if (
|
|
208
|
+
!parent.canReplace(
|
|
209
|
+
indexBefore + (atStart ? 0 : 1),
|
|
210
|
+
indexBefore + 1,
|
|
211
|
+
item.content.append(atEnd ? Fragment.empty : Fragment.from(list)),
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
let start = $start.pos, end = start + item.nodeSize;
|
|
217
|
+
// Strip off the surrounding list. At the sides where we're not at
|
|
218
|
+
// the end of the list, the existing list is closed. At sides where
|
|
219
|
+
// this is the end, it is overwritten to its end.
|
|
220
|
+
tr.step(
|
|
221
|
+
new ReplaceAroundStep(
|
|
222
|
+
start - (atStart ? 1 : 0),
|
|
223
|
+
end + (atEnd ? 1 : 0),
|
|
224
|
+
start + 1,
|
|
225
|
+
end - 1,
|
|
226
|
+
new Slice(
|
|
227
|
+
(atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
|
|
228
|
+
.append(
|
|
229
|
+
atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)),
|
|
230
|
+
),
|
|
231
|
+
atStart ? 0 : 1,
|
|
232
|
+
atEnd ? 0 : 1,
|
|
233
|
+
),
|
|
234
|
+
atStart ? 0 : 1,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
dispatch(tr.scrollIntoView());
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Create a command to sink the list item around the selection down
|
|
242
|
+
/// into an inner list.
|
|
243
|
+
export function sinkListItem(itemType: NodeType): Command {
|
|
244
|
+
return function (state, dispatch) {
|
|
245
|
+
const { $from, $to } = state.selection;
|
|
246
|
+
const range = $from.blockRange(
|
|
247
|
+
$to,
|
|
248
|
+
(node) => node.childCount > 0 && node.firstChild!.type == itemType,
|
|
249
|
+
);
|
|
250
|
+
if (!range) return false;
|
|
251
|
+
const startIndex = range.startIndex;
|
|
252
|
+
if (startIndex == 0) return false;
|
|
253
|
+
const parent = range.parent, nodeBefore = parent.child(startIndex - 1);
|
|
254
|
+
if (nodeBefore.type != itemType) return false;
|
|
255
|
+
|
|
256
|
+
if (dispatch) {
|
|
257
|
+
const nestedBefore = nodeBefore.lastChild &&
|
|
258
|
+
nodeBefore.lastChild.type == parent.type;
|
|
259
|
+
const inner = Fragment.from(nestedBefore ? itemType.create() : null);
|
|
260
|
+
const slice = new Slice(
|
|
261
|
+
Fragment.from(
|
|
262
|
+
itemType.create(null, Fragment.from(parent.type.create(null, inner))),
|
|
263
|
+
),
|
|
264
|
+
nestedBefore ? 3 : 1,
|
|
265
|
+
0,
|
|
266
|
+
);
|
|
267
|
+
const before = range.start, after = range.end;
|
|
268
|
+
dispatch(
|
|
269
|
+
state.tr.step(
|
|
270
|
+
new ReplaceAroundStep(
|
|
271
|
+
before - (nestedBefore ? 3 : 1),
|
|
272
|
+
after,
|
|
273
|
+
before,
|
|
274
|
+
after,
|
|
275
|
+
slice,
|
|
276
|
+
1,
|
|
277
|
+
true,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
.scrollIntoView(),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class NodeListItem extends Node {
|
|
288
|
+
override name = 'list_item';
|
|
289
|
+
requires = ['doc'];
|
|
290
|
+
|
|
291
|
+
override getNodeSpec(): NodeSpec {
|
|
292
|
+
return {
|
|
293
|
+
content: 'paragraph block*',
|
|
294
|
+
parseDOM: [{ tag: 'li' }],
|
|
295
|
+
defining: true,
|
|
296
|
+
toDOM() {
|
|
297
|
+
return ['li', 0];
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override getCommandFactories(
|
|
303
|
+
editor: CoreEditor,
|
|
304
|
+
type: NodeType,
|
|
305
|
+
): Partial<CommandFactories> {
|
|
306
|
+
return {
|
|
307
|
+
'splitListItem': () => splitListItem(type),
|
|
308
|
+
'liftListItem': () => liftListItem(type),
|
|
309
|
+
'sinkListItem': () => sinkListItem(type),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override getKeyboardShortcuts(): Partial<CommandShortcuts> {
|
|
314
|
+
return {
|
|
315
|
+
'Enter': 'splitListItem',
|
|
316
|
+
'Tab': 'sinkListItem',
|
|
317
|
+
'Shift-Tab': 'liftListItem',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
package/src/NodeMath.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type NodeSpec } from 'prosemirror-model';
|
|
2
|
+
import { Node } from '@kerebron/editor';
|
|
3
|
+
|
|
4
|
+
// import { MathMLToLaTeX } from 'mathml-to-latex';
|
|
5
|
+
// const latex = MathMLToLaTeX.convert(mathMl);
|
|
6
|
+
// https://mathlive.io/mathfield/
|
|
7
|
+
|
|
8
|
+
export class NodeMath extends Node {
|
|
9
|
+
override name = 'math';
|
|
10
|
+
requires = ['doc'];
|
|
11
|
+
|
|
12
|
+
override getNodeSpec(): NodeSpec {
|
|
13
|
+
return {
|
|
14
|
+
inline: true,
|
|
15
|
+
attrs: {
|
|
16
|
+
lang: { default: 'mathml' },
|
|
17
|
+
content: {},
|
|
18
|
+
},
|
|
19
|
+
group: 'inline',
|
|
20
|
+
draggable: true,
|
|
21
|
+
// parseDOM: [
|
|
22
|
+
// {
|
|
23
|
+
// tag: 'math',
|
|
24
|
+
// getAttrs(dom: HTMLElement) {
|
|
25
|
+
// return {
|
|
26
|
+
// content: dom.outerHTML,
|
|
27
|
+
// };
|
|
28
|
+
// },
|
|
29
|
+
// },
|
|
30
|
+
// ],
|
|
31
|
+
parseDOM: [{
|
|
32
|
+
tag: 'math',
|
|
33
|
+
getAttrs: (dom) => ({
|
|
34
|
+
lang: 'mathml',
|
|
35
|
+
content: new XMLSerializer().serializeToString(dom),
|
|
36
|
+
}),
|
|
37
|
+
}],
|
|
38
|
+
toDOM(node) {
|
|
39
|
+
const parser = new DOMParser();
|
|
40
|
+
const parsed = parser.parseFromString(
|
|
41
|
+
node.attrs.content,
|
|
42
|
+
'application/xml',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Check for parsing errors (e.g., invalid XML)
|
|
46
|
+
const errorNode = parsed.getElementsByTagName('parsererror');
|
|
47
|
+
if (errorNode.length > 0) {
|
|
48
|
+
return ['span', { class: 'mathml-error' }, 'Invalid MathML'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Import and return the parsed MathML element
|
|
52
|
+
return document.importNode(parsed.documentElement, true);
|
|
53
|
+
// const { xml } = node.attrs;
|
|
54
|
+
// return ['math', {}, []];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
MathML vs. LaTeX: Which is Better for Web Math Equations?
|
|
62
|
+
Neither is universally "better"—it depends on your goals (authoring, rendering, accessibility, or storage). MathML and LaTeX serve different primary purposes but can complement each other on the web (e.g., via converters like MathJax). Here's a comparison focused on web use:
|
|
63
|
+
Key Differences
|
|
64
|
+
|
|
65
|
+
Purpose:
|
|
66
|
+
|
|
67
|
+
LaTeX: A human-readable markup language for typesetting math (e.g., $E=mc^2$). It's concise and widely used for authoring documents, papers, and web input. Not native to HTML; requires a processor (e.g., MathJax, KaTeX) to render as HTML/CSS/SVG.
|
|
68
|
+
MathML: An XML-based standard (part of HTML5) for describing math structure and presentation (Presentation MathML for visuals; Content MathML for semantics). Designed for direct embedding in web pages (e.g., <math><mi>E</mi>=<mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></math>). Browsers like Firefox and Safari support it natively; others need polyfills.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Ease of Authoring:
|
|
72
|
+
|
|
73
|
+
LaTeX: Wins for humans—shorter, intuitive syntax familiar to mathematicians. Easier to write/edit manually or in editors like Overleaf. Verbose MathML is painful for hand-coding complex equations.
|
|
74
|
+
MathML: Better for programmatic generation (e.g., from tools or APIs) due to its structured XML. Use WYSIWYG editors (like those above) to avoid writing tags.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Rendering on Web:
|
|
78
|
+
|
|
79
|
+
LaTeX: Requires a JS library (MathJax ~300KB, KaTeX lighter ~100KB) for cross-browser display. Renders beautifully everywhere but adds load time and JS dependency. No native browser support.
|
|
80
|
+
MathML: Native in Firefox/Safari (fast, no JS). Improving in Chrome/Edge (as of 2023+), but still needs MathJax fallback for full support. Outputs scalable HTML/CSS without images.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Accessibility:
|
|
84
|
+
|
|
85
|
+
MathML: Superior—semantic structure allows screen readers (e.g., NVDA with MathCAT, JAWS) to read equations meaningfully (e.g., "E equals m times c squared"). Essential for WCAG/ADA compliance.
|
|
86
|
+
LaTeX: Poor native accessibility; screen readers treat it as plain text. MathJax can convert to MathML internally for better support, but raw LaTeX fails.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Performance & Compatibility:
|
|
90
|
+
|
|
91
|
+
LaTeX: Faster authoring, but rendering depends on the library. Universal via JS, but increases page weight.
|
|
92
|
+
MathML: Lightweight native rendering where supported; more robust for search engines/parsing (e.g., Google indexes MathML). Less portable outside web (LaTeX dominates PDFs/print).
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
Storage/Interoperability:
|
|
96
|
+
|
|
97
|
+
LaTeX: Compact for databases/files. Easy to convert to MathML (via tools like texmath or Pandoc).
|
|
98
|
+
MathML: Better for web standards (XML parsable), but bulkier. Ideal for exchange between math tools.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Recommendation
|
|
103
|
+
|
|
104
|
+
Use LaTeX if your focus is easy input/authoring (e.g., users typing equations) and you're okay with a renderer like MathJax. It's more practical for most web projects today due to familiarity and tools. Store as LaTeX, render via JS, and convert to MathML for accessibility if needed.
|
|
105
|
+
Use MathML if prioritizing native web standards, accessibility, or programmatic output (e.g., from editors). It's "better" for semantic web math and future-proofing (browser support is growing), but pair with LaTeX input for usability.
|
|
106
|
+
Hybrid Approach: Best for web—author in LaTeX, convert/output MathML for display/storage. Libraries like MathJax handle both seamlessly (input LaTeX, output rendered MathML). This gives LaTeX's simplicity with MathML's benefits.
|
|
107
|
+
|
|
108
|
+
If accessibility is key, always generate MathML (even from LaTeX source). Test in browsers: LaTeX+MathJax works everywhere; pure MathML shines in Firefox but needs fallbacks elsewhere.
|
|
109
|
+
*/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NodeSpec, NodeType } from 'prosemirror-model';
|
|
2
|
+
|
|
3
|
+
import { type CoreEditor, Node } from '@kerebron/editor';
|
|
4
|
+
import {
|
|
5
|
+
getHtmlAttributes,
|
|
6
|
+
setHtmlAttributes,
|
|
7
|
+
} from '@kerebron/editor/utilities';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type InputRule,
|
|
11
|
+
wrappingInputRule,
|
|
12
|
+
} from '@kerebron/editor/plugins/input-rules';
|
|
13
|
+
import {
|
|
14
|
+
type CommandFactories,
|
|
15
|
+
type CommandShortcuts,
|
|
16
|
+
} from '@kerebron/editor/commands';
|
|
17
|
+
|
|
18
|
+
export class NodeOrderedList extends Node {
|
|
19
|
+
override name = 'ordered_list';
|
|
20
|
+
requires = ['doc'];
|
|
21
|
+
|
|
22
|
+
override attributes = {
|
|
23
|
+
type: {
|
|
24
|
+
default: '1',
|
|
25
|
+
fromDom(element: HTMLElement) {
|
|
26
|
+
return element.hasAttribute('type')
|
|
27
|
+
? element.getAttribute('type')
|
|
28
|
+
: '1';
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
start: {
|
|
32
|
+
default: 1,
|
|
33
|
+
fromDom(element: HTMLElement) {
|
|
34
|
+
return element.hasAttribute('start')
|
|
35
|
+
? +element.getAttribute('start')!
|
|
36
|
+
: 1;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
override getNodeSpec(): NodeSpec {
|
|
42
|
+
return {
|
|
43
|
+
group: 'block',
|
|
44
|
+
content: 'list_item+',
|
|
45
|
+
parseDOM: [
|
|
46
|
+
{ tag: 'ol', getAttrs: (element) => setHtmlAttributes(this, element) },
|
|
47
|
+
],
|
|
48
|
+
toDOM: (node) => ['ol', getHtmlAttributes(this, node), 0],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override getInputRules(type: NodeType): InputRule[] {
|
|
53
|
+
return [
|
|
54
|
+
/// Given a list node type, returns an input rule that turns a number
|
|
55
|
+
/// followed by a dot at the start of a textblock into an ordered list.
|
|
56
|
+
wrappingInputRule(
|
|
57
|
+
/^(\d+)\.\s$/,
|
|
58
|
+
type,
|
|
59
|
+
(match) => ({ order: +match[1] }),
|
|
60
|
+
(match, node) => node.childCount + node.attrs.order == +match[1],
|
|
61
|
+
),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override getCommandFactories(
|
|
66
|
+
editor: CoreEditor,
|
|
67
|
+
type: NodeType,
|
|
68
|
+
): Partial<CommandFactories> {
|
|
69
|
+
return {
|
|
70
|
+
'toggleOrderedList': () => editor.commandFactories.wrapInList(type),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override getKeyboardShortcuts(): Partial<CommandShortcuts> {
|
|
75
|
+
return {
|
|
76
|
+
'Shift-Ctrl-7': 'toggleOrderedList',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type NodeSpec, type NodeType } from 'prosemirror-model';
|
|
2
|
+
import { type CoreEditor, Node } from '@kerebron/editor';
|
|
3
|
+
import {
|
|
4
|
+
type CommandFactories,
|
|
5
|
+
type CommandShortcuts,
|
|
6
|
+
} from '@kerebron/editor/commands';
|
|
7
|
+
|
|
8
|
+
type TextAlign = 'left' | 'center' | 'right' | 'justify';
|
|
9
|
+
|
|
10
|
+
export class NodeParagraph extends Node {
|
|
11
|
+
override name = 'paragraph';
|
|
12
|
+
requires = ['doc'];
|
|
13
|
+
|
|
14
|
+
override getNodeSpec(): NodeSpec {
|
|
15
|
+
return {
|
|
16
|
+
content: 'inline*',
|
|
17
|
+
group: 'block',
|
|
18
|
+
attrs: {
|
|
19
|
+
textAlign: { default: null },
|
|
20
|
+
},
|
|
21
|
+
parseDOM: [
|
|
22
|
+
{
|
|
23
|
+
tag: 'p',
|
|
24
|
+
getAttrs(dom) {
|
|
25
|
+
const element = dom as HTMLElement;
|
|
26
|
+
const style = element.style.textAlign;
|
|
27
|
+
if (
|
|
28
|
+
style === 'center' || style === 'right' || style === 'justify'
|
|
29
|
+
) {
|
|
30
|
+
return { textAlign: style };
|
|
31
|
+
}
|
|
32
|
+
return { textAlign: null };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
toDOM(node) {
|
|
37
|
+
const align = node.attrs.textAlign as TextAlign | null;
|
|
38
|
+
if (align && align !== 'left') {
|
|
39
|
+
return ['p', { style: `text-align: ${align}` }, 0];
|
|
40
|
+
}
|
|
41
|
+
return ['p', 0];
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override getCommandFactories(
|
|
47
|
+
editor: CoreEditor,
|
|
48
|
+
type: NodeType,
|
|
49
|
+
): Partial<CommandFactories> {
|
|
50
|
+
return {
|
|
51
|
+
'setParagraph': () => editor.commandFactories.setBlockType(type),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override getKeyboardShortcuts(): Partial<CommandShortcuts> {
|
|
56
|
+
return {
|
|
57
|
+
'Shift-Ctrl-0': 'setParagraph',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|