neuphlo-editor 2.5.1 → 2.6.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/Markdown-BAW0cb-U.d.cts +153 -0
- package/dist/Markdown-BAW0cb-U.d.ts +153 -0
- package/dist/chunk-ABL4D65Z.js +1454 -0
- package/dist/chunk-ABL4D65Z.js.map +1 -0
- package/dist/chunk-Z2DR2FK3.js +1582 -0
- package/dist/chunk-Z2DR2FK3.js.map +1 -0
- package/dist/headless/components/editor-bubble-item.d.ts +9 -0
- package/dist/headless/components/editor-bubble-item.js +29 -0
- package/dist/headless/components/editor-bubble.d.ts +9 -0
- package/dist/headless/components/editor-bubble.js +9 -0
- package/dist/headless/components/editor-command-item.d.ts +3 -0
- package/dist/headless/components/editor-command-item.js +27 -0
- package/dist/headless/components/editor-command.d.ts +3 -0
- package/dist/headless/components/editor-command.js +136 -0
- package/dist/headless/components/editor.d.ts +24 -0
- package/dist/headless/components/editor.js +13 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.d.ts +82 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.js +244 -0
- package/dist/headless/extensions/AISuggestion/index.d.ts +2 -0
- package/dist/headless/extensions/AISuggestion/index.js +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.js +6 -0
- package/dist/headless/extensions/CodeBlock/index.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/index.js +1 -0
- package/dist/headless/extensions/DragHandle/DragHandle.d.ts +9 -0
- package/dist/headless/extensions/DragHandle/DragHandle.js +47 -0
- package/dist/headless/extensions/DragHandle/index.d.ts +2 -0
- package/dist/headless/extensions/DragHandle/index.js +1 -0
- package/dist/headless/extensions/Image/Image.d.ts +6 -0
- package/dist/headless/extensions/Image/Image.js +197 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.d.ts +23 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.js +242 -0
- package/dist/headless/extensions/ImageBlock/index.d.ts +2 -0
- package/dist/headless/extensions/ImageBlock/index.js +1 -0
- package/dist/headless/extensions/Link/Link.d.ts +2 -0
- package/dist/headless/extensions/Link/Link.js +44 -0
- package/dist/headless/extensions/Link/index.d.ts +1 -0
- package/dist/headless/extensions/Link/index.js +1 -0
- package/dist/headless/extensions/Markdown.d.ts +9 -0
- package/dist/headless/extensions/Markdown.js +138 -0
- package/dist/headless/extensions/MarkdownPaste.d.ts +3 -0
- package/dist/headless/extensions/MarkdownPaste.js +168 -0
- package/dist/headless/extensions/Mention/index.d.ts +4 -0
- package/dist/headless/extensions/Mention/index.js +3 -0
- package/dist/headless/extensions/Mention/mention-command.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-command.js +165 -0
- package/dist/headless/extensions/Mention/mention-list.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-list.js +150 -0
- package/dist/headless/extensions/Mention/mention-node-view.d.ts +2 -0
- package/dist/headless/extensions/Mention/mention-node-view.js +92 -0
- package/dist/headless/extensions/Mention/mention.d.ts +46 -0
- package/dist/headless/extensions/Mention/mention.js +218 -0
- package/dist/headless/extensions/Table/index.d.ts +2 -0
- package/dist/headless/extensions/Table/index.js +2 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.d.ts +17 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.js +73 -0
- package/dist/headless/extensions/VideoBlock/index.d.ts +2 -0
- package/dist/headless/extensions/VideoBlock/index.js +1 -0
- package/dist/headless/extensions/extension-kit.d.ts +29 -0
- package/dist/headless/extensions/extension-kit.js +132 -0
- package/dist/headless/extensions/index.d.ts +16 -0
- package/dist/headless/extensions/index.js +12 -0
- package/dist/headless/extensions/slash-command.d.ts +24 -0
- package/dist/headless/extensions/slash-command.js +99 -0
- package/dist/headless/index.cjs +143 -2
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +3 -2
- package/dist/headless/index.d.ts +3 -2
- package/dist/headless/index.js +5 -1
- package/dist/headless/utils/atoms.d.ts +13 -0
- package/dist/headless/utils/atoms.js +5 -0
- package/dist/headless/utils/store.d.ts +2 -0
- package/dist/headless/utils/store.js +4 -0
- package/dist/highlight.css +0 -6
- package/dist/react/Editor.d.ts +43 -0
- package/dist/react/Editor.js +119 -0
- package/dist/react/TableOfContents.d.ts +7 -0
- package/dist/react/TableOfContents.js +96 -0
- package/dist/react/index.cjs +156 -9
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +5 -2
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.js +14 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/menus/DragHandle/BlockActionMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/BlockActionMenu.js +111 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.js +97 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.d.ts +2 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.js +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.js +75 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.d.ts +10 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.js +60 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.d.ts +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.js +29 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.js +37 -0
- package/dist/react/menus/ImageBlock/ImageUploader.d.ts +7 -0
- package/dist/react/menus/ImageBlock/ImageUploader.js +73 -0
- package/dist/react/menus/ImageBlock/index.d.ts +4 -0
- package/dist/react/menus/ImageBlock/index.js +4 -0
- package/dist/react/menus/ImageMenu.d.ts +10 -0
- package/dist/react/menus/ImageMenu.js +77 -0
- package/dist/react/menus/ImagePopover.d.ts +8 -0
- package/dist/react/menus/ImagePopover.js +146 -0
- package/dist/react/menus/LinkMenu.d.ts +2 -0
- package/dist/react/menus/LinkMenu.js +69 -0
- package/dist/react/menus/MenuList.d.ts +10 -0
- package/dist/react/menus/MenuList.js +94 -0
- package/dist/react/menus/SlashMenu.d.ts +5 -0
- package/dist/react/menus/SlashMenu.js +156 -0
- package/dist/react/menus/TableMenu.d.ts +5 -0
- package/dist/react/menus/TableMenu.js +462 -0
- package/dist/react/menus/TaskItem/TaskItemView.d.ts +12 -0
- package/dist/react/menus/TaskItem/TaskItemView.js +11 -0
- package/dist/react/menus/TextMenu.d.ts +10 -0
- package/dist/react/menus/TextMenu.js +139 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.d.ts +7 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.js +72 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.d.ts +10 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.js +87 -0
- package/dist/react/menus/VideoBlock/index.d.ts +2 -0
- package/dist/react/menus/VideoBlock/index.js +2 -0
- package/dist/react/menus/index.d.ts +15 -0
- package/dist/react/menus/index.js +9 -0
- package/package.json +13 -9
- package/LICENSE +0 -21
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { Image as TiptapImage } from "@tiptap/extension-image";
|
|
3
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
4
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
5
|
+
export const ImageBlock = TiptapImage.extend({
|
|
6
|
+
name: "imageBlock",
|
|
7
|
+
group: "block",
|
|
8
|
+
defining: true,
|
|
9
|
+
isolating: true,
|
|
10
|
+
addOptions() {
|
|
11
|
+
return {
|
|
12
|
+
...this.parent?.(),
|
|
13
|
+
inline: false,
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
addAttributes() {
|
|
17
|
+
return {
|
|
18
|
+
src: {
|
|
19
|
+
default: "",
|
|
20
|
+
parseHTML: (element) => element.getAttribute("src"),
|
|
21
|
+
renderHTML: (attributes) => ({
|
|
22
|
+
src: attributes.src,
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
width: {
|
|
26
|
+
default: "100%",
|
|
27
|
+
parseHTML: (element) => element.getAttribute("data-width"),
|
|
28
|
+
renderHTML: (attributes) => ({
|
|
29
|
+
"data-width": attributes.width,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
align: {
|
|
33
|
+
default: "center",
|
|
34
|
+
parseHTML: (element) => element.getAttribute("data-align"),
|
|
35
|
+
renderHTML: (attributes) => ({
|
|
36
|
+
"data-align": attributes.align,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
alt: {
|
|
40
|
+
default: undefined,
|
|
41
|
+
parseHTML: (element) => element.getAttribute("alt"),
|
|
42
|
+
renderHTML: (attributes) => ({
|
|
43
|
+
alt: attributes.alt,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
loading: {
|
|
47
|
+
default: false,
|
|
48
|
+
parseHTML: () => false,
|
|
49
|
+
renderHTML: () => ({}),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
parseHTML() {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
tag: 'img[src]:not([src^="data:"])',
|
|
57
|
+
getAttrs: (element) => {
|
|
58
|
+
const el = element;
|
|
59
|
+
return {
|
|
60
|
+
src: el.getAttribute("src"),
|
|
61
|
+
alt: el.getAttribute("alt"),
|
|
62
|
+
width: el.getAttribute("data-width") || "100%",
|
|
63
|
+
align: el.getAttribute("data-align") || "center",
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
},
|
|
69
|
+
renderHTML({ HTMLAttributes }) {
|
|
70
|
+
return ["img", mergeAttributes(HTMLAttributes)];
|
|
71
|
+
},
|
|
72
|
+
addCommands() {
|
|
73
|
+
return {
|
|
74
|
+
setImageBlock: (attrs) => ({ commands }) => {
|
|
75
|
+
return commands.insertContent({
|
|
76
|
+
type: "imageBlock",
|
|
77
|
+
attrs: { src: attrs.src },
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
setImageBlockAt: (attrs) => ({ commands }) => {
|
|
81
|
+
return commands.insertContentAt(attrs.pos, {
|
|
82
|
+
type: "imageBlock",
|
|
83
|
+
attrs: { src: attrs.src },
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
setImageBlockAlign: (align) => ({ commands }) => commands.updateAttributes("imageBlock", { align }),
|
|
87
|
+
setImageBlockWidth: (width) => ({ commands }) => commands.updateAttributes("imageBlock", {
|
|
88
|
+
width: `${Math.max(0, Math.min(100, width))}%`,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
addNodeView() {
|
|
93
|
+
if (this.options.nodeView) {
|
|
94
|
+
return ReactNodeViewRenderer(this.options.nodeView);
|
|
95
|
+
}
|
|
96
|
+
// If no custom node view is provided, we fall back to default behavior
|
|
97
|
+
// but avoid the circular dependency require()
|
|
98
|
+
return null;
|
|
99
|
+
},
|
|
100
|
+
addProseMirrorPlugins() {
|
|
101
|
+
return [
|
|
102
|
+
new Plugin({
|
|
103
|
+
key: new PluginKey("imageBlockDrop"),
|
|
104
|
+
props: {
|
|
105
|
+
handleDOMEvents: {
|
|
106
|
+
drop: (view, event) => {
|
|
107
|
+
const hasFiles = event.dataTransfer &&
|
|
108
|
+
event.dataTransfer.files &&
|
|
109
|
+
event.dataTransfer.files.length;
|
|
110
|
+
if (!hasFiles) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const images = Array.from(event.dataTransfer.files).filter((file) => /image/i.test(file.type));
|
|
114
|
+
if (images.length === 0) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
const { schema } = view.state;
|
|
119
|
+
const coordinates = view.posAtCoords({
|
|
120
|
+
left: event.clientX,
|
|
121
|
+
top: event.clientY,
|
|
122
|
+
});
|
|
123
|
+
if (!coordinates)
|
|
124
|
+
return false;
|
|
125
|
+
images.forEach(async (image) => {
|
|
126
|
+
if (this.options.uploadImage) {
|
|
127
|
+
try {
|
|
128
|
+
// Insert placeholder first
|
|
129
|
+
const placeholderNode = schema.nodes.imageBlock.create({
|
|
130
|
+
src: "",
|
|
131
|
+
loading: true,
|
|
132
|
+
});
|
|
133
|
+
const placeholderTr = view.state.tr.insert(coordinates.pos, placeholderNode);
|
|
134
|
+
view.dispatch(placeholderTr);
|
|
135
|
+
// Upload and replace
|
|
136
|
+
const url = await this.options.uploadImage(image);
|
|
137
|
+
const node = schema.nodes.imageBlock.create({ src: url });
|
|
138
|
+
// Find and replace the placeholder
|
|
139
|
+
const currentState = view.state;
|
|
140
|
+
let foundPos = -1;
|
|
141
|
+
currentState.doc.descendants((node, pos) => {
|
|
142
|
+
if (node.type.name === "imageBlock" &&
|
|
143
|
+
node.attrs.loading) {
|
|
144
|
+
foundPos = pos;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (foundPos !== -1) {
|
|
149
|
+
const transaction = view.state.tr.replaceWith(foundPos, foundPos + 1, node);
|
|
150
|
+
view.dispatch(transaction);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error("Failed to upload image:", error);
|
|
155
|
+
// Remove placeholder on error
|
|
156
|
+
const currentState = view.state;
|
|
157
|
+
let foundPos = -1;
|
|
158
|
+
currentState.doc.descendants((node, pos) => {
|
|
159
|
+
if (node.type.name === "imageBlock" &&
|
|
160
|
+
node.attrs.loading) {
|
|
161
|
+
foundPos = pos;
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
if (foundPos !== -1) {
|
|
166
|
+
const transaction = view.state.tr.delete(foundPos, foundPos + 1);
|
|
167
|
+
view.dispatch(transaction);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
174
|
+
paste: (view, event) => {
|
|
175
|
+
const hasFiles = event.clipboardData &&
|
|
176
|
+
event.clipboardData.files &&
|
|
177
|
+
event.clipboardData.files.length;
|
|
178
|
+
if (!hasFiles) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const images = Array.from(event.clipboardData.files).filter((file) => /image/i.test(file.type));
|
|
182
|
+
if (images.length === 0) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
images.forEach(async (image) => {
|
|
187
|
+
if (this.options.uploadImage) {
|
|
188
|
+
try {
|
|
189
|
+
// Insert placeholder first
|
|
190
|
+
const placeholderNode = view.state.schema.nodes.imageBlock.create({
|
|
191
|
+
src: "",
|
|
192
|
+
loading: true,
|
|
193
|
+
});
|
|
194
|
+
view.dispatch(view.state.tr.replaceSelectionWith(placeholderNode));
|
|
195
|
+
// Upload and replace
|
|
196
|
+
const url = await this.options.uploadImage(image);
|
|
197
|
+
const node = view.state.schema.nodes.imageBlock.create({
|
|
198
|
+
src: url,
|
|
199
|
+
});
|
|
200
|
+
// Find and replace the placeholder
|
|
201
|
+
const currentState = view.state;
|
|
202
|
+
let foundPos = -1;
|
|
203
|
+
currentState.doc.descendants((node, pos) => {
|
|
204
|
+
if (node.type.name === "imageBlock" &&
|
|
205
|
+
node.attrs.loading) {
|
|
206
|
+
foundPos = pos;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (foundPos !== -1) {
|
|
211
|
+
const transaction = view.state.tr.replaceWith(foundPos, foundPos + 1, node);
|
|
212
|
+
view.dispatch(transaction);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error("Failed to upload image:", error);
|
|
217
|
+
// Remove placeholder on error
|
|
218
|
+
const currentState = view.state;
|
|
219
|
+
let foundPos = -1;
|
|
220
|
+
currentState.doc.descendants((node, pos) => {
|
|
221
|
+
if (node.type.name === "imageBlock" &&
|
|
222
|
+
node.attrs.loading) {
|
|
223
|
+
foundPos = pos;
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (foundPos !== -1) {
|
|
228
|
+
const transaction = view.state.tr.delete(foundPos, foundPos + 1);
|
|
229
|
+
view.dispatch(transaction);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
];
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
export default ImageBlock;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ImageBlock } from "./ImageBlock";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import TiptapLink from "@tiptap/extension-link";
|
|
3
|
+
import { Plugin } from "@tiptap/pm/state";
|
|
4
|
+
export const Link = TiptapLink.extend({
|
|
5
|
+
inclusive: false,
|
|
6
|
+
parseHTML() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])',
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
},
|
|
13
|
+
renderHTML({ HTMLAttributes }) {
|
|
14
|
+
return [
|
|
15
|
+
"a",
|
|
16
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
17
|
+
class: "link",
|
|
18
|
+
}),
|
|
19
|
+
0,
|
|
20
|
+
];
|
|
21
|
+
},
|
|
22
|
+
addProseMirrorPlugins() {
|
|
23
|
+
const { editor } = this;
|
|
24
|
+
return [
|
|
25
|
+
...(this.parent?.() || []),
|
|
26
|
+
new Plugin({
|
|
27
|
+
props: {
|
|
28
|
+
handleKeyDown: (view, event) => {
|
|
29
|
+
const { selection } = editor.state;
|
|
30
|
+
if (event.key === "Escape" && selection.empty !== true) {
|
|
31
|
+
editor.commands.focus(selection.to, { scrollIntoView: false });
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
];
|
|
38
|
+
},
|
|
39
|
+
}).configure({
|
|
40
|
+
openOnClick: false,
|
|
41
|
+
autolink: true,
|
|
42
|
+
enableClickSelection: true,
|
|
43
|
+
});
|
|
44
|
+
export default Link;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Link";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Link";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { MarkdownSerializer } from "@tiptap/pm/markdown";
|
|
3
|
+
export declare function markdownToHtml(input: string | null | undefined): string;
|
|
4
|
+
export interface MarkdownStorage {
|
|
5
|
+
serializer: MarkdownSerializer | null;
|
|
6
|
+
getMarkdown: () => string;
|
|
7
|
+
}
|
|
8
|
+
export declare const Markdown: Extension<any, MarkdownStorage>;
|
|
9
|
+
export default Markdown;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { MarkdownSerializer } from "@tiptap/pm/markdown";
|
|
3
|
+
import { DOMSerializer } from "@tiptap/pm/model";
|
|
4
|
+
import MarkdownIt from "markdown-it";
|
|
5
|
+
const markdownIt = new MarkdownIt({ html: true, linkify: false, breaks: false });
|
|
6
|
+
export function markdownToHtml(input) {
|
|
7
|
+
if (!input)
|
|
8
|
+
return "";
|
|
9
|
+
return markdownIt.render(input);
|
|
10
|
+
}
|
|
11
|
+
function nodeToHtml(node, schema) {
|
|
12
|
+
if (typeof document === "undefined")
|
|
13
|
+
return "";
|
|
14
|
+
const dom = DOMSerializer.fromSchema(schema).serializeNode(node);
|
|
15
|
+
const container = document.createElement("div");
|
|
16
|
+
container.appendChild(dom);
|
|
17
|
+
return container.innerHTML;
|
|
18
|
+
}
|
|
19
|
+
const specificNodes = {
|
|
20
|
+
text: (state, node) => {
|
|
21
|
+
state.text(node.text ?? "");
|
|
22
|
+
},
|
|
23
|
+
paragraph: (state, node) => {
|
|
24
|
+
state.renderInline(node);
|
|
25
|
+
state.closeBlock(node);
|
|
26
|
+
},
|
|
27
|
+
heading: (state, node) => {
|
|
28
|
+
state.write(`${state.repeat("#", node.attrs.level)} `);
|
|
29
|
+
state.renderInline(node);
|
|
30
|
+
state.closeBlock(node);
|
|
31
|
+
},
|
|
32
|
+
blockquote: (state, node) => {
|
|
33
|
+
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
|
34
|
+
},
|
|
35
|
+
horizontalRule: (state, node) => {
|
|
36
|
+
state.write(node.attrs.markup || "---");
|
|
37
|
+
state.closeBlock(node);
|
|
38
|
+
},
|
|
39
|
+
bulletList: (state, node) => {
|
|
40
|
+
state.renderList(node, " ", () => `${node.attrs.bullet || "-"} `);
|
|
41
|
+
},
|
|
42
|
+
orderedList: (state, node) => {
|
|
43
|
+
const start = node.attrs.start || 1;
|
|
44
|
+
const maxWidth = String(start + node.childCount - 1).length;
|
|
45
|
+
const space = state.repeat(" ", maxWidth + 2);
|
|
46
|
+
state.renderList(node, space, (index) => {
|
|
47
|
+
const label = String(start + index);
|
|
48
|
+
return `${state.repeat(" ", maxWidth - label.length)}${label}. `;
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
listItem: (state, node) => {
|
|
52
|
+
state.renderContent(node);
|
|
53
|
+
},
|
|
54
|
+
codeBlock: (state, node) => {
|
|
55
|
+
state.write(`\`\`\`${node.attrs.language || ""}\n`);
|
|
56
|
+
state.text(node.textContent, false);
|
|
57
|
+
state.ensureNewLine();
|
|
58
|
+
state.write("```");
|
|
59
|
+
state.closeBlock(node);
|
|
60
|
+
},
|
|
61
|
+
hardBreak: (state, node, parent, index) => {
|
|
62
|
+
for (let i = index + 1; i < parent.childCount; i += 1) {
|
|
63
|
+
if (parent.child(i).type !== node.type) {
|
|
64
|
+
state.write("\\\n");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const inlineHtmlNodes = new Set(["mention", "reference"]);
|
|
71
|
+
const specificMarks = {
|
|
72
|
+
bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
|
|
73
|
+
italic: { open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true },
|
|
74
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
75
|
+
underline: { open: "<u>", close: "</u>", mixable: true },
|
|
76
|
+
code: { open: "`", close: "`", escape: false },
|
|
77
|
+
link: {
|
|
78
|
+
open: "[",
|
|
79
|
+
close: (_state, mark) => {
|
|
80
|
+
const href = mark.attrs.href || "";
|
|
81
|
+
const title = mark.attrs.title ? ` "${mark.attrs.title}"` : "";
|
|
82
|
+
return `](${href}${title})`;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function buildSerializer(schema) {
|
|
87
|
+
const nodes = {};
|
|
88
|
+
for (const name of Object.keys(schema.nodes)) {
|
|
89
|
+
if (specificNodes[name]) {
|
|
90
|
+
nodes[name] = specificNodes[name];
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const type = schema.nodes[name];
|
|
94
|
+
if (type.isText) {
|
|
95
|
+
nodes[name] = specificNodes.text;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (type.isInline || inlineHtmlNodes.has(name)) {
|
|
99
|
+
nodes[name] = (state, node) => {
|
|
100
|
+
state.text(nodeToHtml(node, schema), false);
|
|
101
|
+
};
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
nodes[name] = (state, node) => {
|
|
105
|
+
state.write(nodeToHtml(node, schema));
|
|
106
|
+
state.closeBlock(node);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const marks = {};
|
|
110
|
+
for (const name of Object.keys(schema.marks)) {
|
|
111
|
+
marks[name] = specificMarks[name] || { open: "", close: "", mixable: true };
|
|
112
|
+
}
|
|
113
|
+
return new MarkdownSerializer(nodes, marks);
|
|
114
|
+
}
|
|
115
|
+
export const Markdown = Extension.create({
|
|
116
|
+
name: "markdown",
|
|
117
|
+
addStorage() {
|
|
118
|
+
return {
|
|
119
|
+
serializer: null,
|
|
120
|
+
getMarkdown: () => "",
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
onBeforeCreate() {
|
|
124
|
+
const storage = this.storage;
|
|
125
|
+
storage.serializer = buildSerializer(this.editor.schema);
|
|
126
|
+
storage.getMarkdown = () => {
|
|
127
|
+
try {
|
|
128
|
+
return storage.serializer.serialize(this.editor.state.doc, {
|
|
129
|
+
tightLists: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return this.editor.getHTML();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
export default Markdown;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { MarkdownParser, defaultMarkdownParser } from "@tiptap/pm/markdown";
|
|
4
|
+
const markdownPastePluginKey = new PluginKey("markdownPaste");
|
|
5
|
+
/**
|
|
6
|
+
* Heuristic: does the plain text look like it contains markdown syntax?
|
|
7
|
+
*/
|
|
8
|
+
function looksLikeMarkdown(text) {
|
|
9
|
+
const patterns = [
|
|
10
|
+
/^#{1,6}\s/m, // headings
|
|
11
|
+
/^\s*[-*+]\s/m, // unordered list
|
|
12
|
+
/^\s*\d+\.\s/m, // ordered list
|
|
13
|
+
/^\s*>\s/m, // blockquote
|
|
14
|
+
/\|.+\|/m, // table
|
|
15
|
+
/^```/m, // fenced code block
|
|
16
|
+
/\*\*.+\*\*/, // bold
|
|
17
|
+
/\*.+\*/, // italic
|
|
18
|
+
/~~.+~~/, // strikethrough
|
|
19
|
+
/`[^`]+`/, // inline code
|
|
20
|
+
/^\s*---\s*$/m, // horizontal rule
|
|
21
|
+
/^\s*\*\*\*\s*$/m, // horizontal rule alt
|
|
22
|
+
/\[.+\]\(.+\)/, // links
|
|
23
|
+
/!\[.*\]\(.+\)/, // images
|
|
24
|
+
];
|
|
25
|
+
// Need at least one markdown pattern match and it shouldn't be HTML
|
|
26
|
+
const hasMarkdown = patterns.some((p) => p.test(text));
|
|
27
|
+
const isHtml = /^<[a-z][\s\S]*>/i.test(text.trim());
|
|
28
|
+
return hasMarkdown && !isHtml;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a MarkdownParser configured for TipTap's schema node/mark names.
|
|
32
|
+
* Re-uses the markdown-it tokenizer from prosemirror-markdown's default parser.
|
|
33
|
+
*/
|
|
34
|
+
function buildParser(schema) {
|
|
35
|
+
// Grab the markdown-it tokenizer instance from prosemirror-markdown's default parser
|
|
36
|
+
const md = defaultMarkdownParser.tokenizer;
|
|
37
|
+
const tokens = {};
|
|
38
|
+
// Block nodes
|
|
39
|
+
if (schema.nodes.paragraph)
|
|
40
|
+
tokens.paragraph = { block: "paragraph" };
|
|
41
|
+
if (schema.nodes.heading) {
|
|
42
|
+
tokens.heading = {
|
|
43
|
+
block: "heading",
|
|
44
|
+
getAttrs: (tok) => ({ level: Number(tok.tag.slice(1)) }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (schema.nodes.blockquote)
|
|
48
|
+
tokens.blockquote = { block: "blockquote" };
|
|
49
|
+
if (schema.nodes.bulletList)
|
|
50
|
+
tokens.bullet_list = { block: "bulletList" };
|
|
51
|
+
if (schema.nodes.orderedList) {
|
|
52
|
+
tokens.ordered_list = {
|
|
53
|
+
block: "orderedList",
|
|
54
|
+
getAttrs: (tok) => ({ start: Number(tok.attrGet("start") || 1) }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (schema.nodes.listItem)
|
|
58
|
+
tokens.list_item = { block: "listItem" };
|
|
59
|
+
if (schema.nodes.codeBlock) {
|
|
60
|
+
tokens.code_block = { block: "codeBlock", noCloseToken: true };
|
|
61
|
+
tokens.fence = {
|
|
62
|
+
block: "codeBlock",
|
|
63
|
+
getAttrs: (tok) => ({ language: tok.info || "" }),
|
|
64
|
+
noCloseToken: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (schema.nodes.horizontalRule) {
|
|
68
|
+
tokens.hr = { node: "horizontalRule" };
|
|
69
|
+
}
|
|
70
|
+
if (schema.nodes.hardBreak) {
|
|
71
|
+
tokens.hardbreak = { node: "hardBreak" };
|
|
72
|
+
}
|
|
73
|
+
if (schema.nodes.image) {
|
|
74
|
+
tokens.image = {
|
|
75
|
+
node: "image",
|
|
76
|
+
getAttrs: (tok) => ({
|
|
77
|
+
src: tok.attrGet("src"),
|
|
78
|
+
title: tok.attrGet("title") || null,
|
|
79
|
+
alt: tok.children?.[0]?.content || null,
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Table support — markdown-it needs the table plugin enabled
|
|
84
|
+
if (schema.nodes.table) {
|
|
85
|
+
tokens.table = { block: "table" };
|
|
86
|
+
tokens.thead = { ignore: true };
|
|
87
|
+
tokens.tbody = { ignore: true };
|
|
88
|
+
tokens.tr = { block: "tableRow" };
|
|
89
|
+
tokens.th = { block: "tableHeader" };
|
|
90
|
+
tokens.td = { block: "tableCell" };
|
|
91
|
+
}
|
|
92
|
+
// Marks
|
|
93
|
+
if (schema.marks.bold || schema.marks.strong) {
|
|
94
|
+
tokens.strong = { mark: schema.marks.bold ? "bold" : "strong" };
|
|
95
|
+
}
|
|
96
|
+
if (schema.marks.italic || schema.marks.em) {
|
|
97
|
+
tokens.em = { mark: schema.marks.italic ? "italic" : "em" };
|
|
98
|
+
}
|
|
99
|
+
if (schema.marks.code) {
|
|
100
|
+
tokens.code_inline = { mark: "code", noCloseToken: true };
|
|
101
|
+
}
|
|
102
|
+
if (schema.marks.link) {
|
|
103
|
+
tokens.link = {
|
|
104
|
+
mark: "link",
|
|
105
|
+
getAttrs: (tok) => ({
|
|
106
|
+
href: tok.attrGet("href"),
|
|
107
|
+
title: tok.attrGet("title") || null,
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (schema.marks.strike || schema.marks.strikethrough) {
|
|
112
|
+
tokens.s = { mark: schema.marks.strike ? "strike" : "strikethrough" };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return new MarkdownParser(schema, md, tokens);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export const MarkdownPaste = Extension.create({
|
|
122
|
+
name: "markdownPaste",
|
|
123
|
+
addProseMirrorPlugins() {
|
|
124
|
+
const schema = this.editor.schema;
|
|
125
|
+
let parser = null;
|
|
126
|
+
return [
|
|
127
|
+
new Plugin({
|
|
128
|
+
key: markdownPastePluginKey,
|
|
129
|
+
props: {
|
|
130
|
+
handlePaste(view, event) {
|
|
131
|
+
const clipboardData = event.clipboardData;
|
|
132
|
+
if (!clipboardData)
|
|
133
|
+
return false;
|
|
134
|
+
// If there's HTML content, let ProseMirror handle it normally
|
|
135
|
+
// (the browser/OS already converted rich content)
|
|
136
|
+
const html = clipboardData.getData("text/html");
|
|
137
|
+
if (html && html.trim().length > 0)
|
|
138
|
+
return false;
|
|
139
|
+
const text = clipboardData.getData("text/plain");
|
|
140
|
+
if (!text || !looksLikeMarkdown(text))
|
|
141
|
+
return false;
|
|
142
|
+
// Lazily build parser on first use
|
|
143
|
+
if (!parser) {
|
|
144
|
+
parser = buildParser(schema);
|
|
145
|
+
}
|
|
146
|
+
if (!parser)
|
|
147
|
+
return false;
|
|
148
|
+
try {
|
|
149
|
+
const doc = parser.parse(text);
|
|
150
|
+
if (!doc || doc.content.size === 0)
|
|
151
|
+
return false;
|
|
152
|
+
const { tr } = view.state;
|
|
153
|
+
const slice = doc.slice(0, doc.content.size);
|
|
154
|
+
tr.replaceSelection(slice);
|
|
155
|
+
view.dispatch(tr);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Fall back to default paste behavior
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
];
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
export default MarkdownPaste;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MentionItem } from "./mention";
|
|
2
|
+
interface MentionCommandProps {
|
|
3
|
+
items: MentionItem[];
|
|
4
|
+
command: (item: MentionItem) => void;
|
|
5
|
+
query: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const MentionCommand: import("react").ForwardRefExoticComponent<MentionCommandProps & import("react").RefAttributes<any>>;
|
|
8
|
+
export {};
|