/
PasteHandler.ts
170 lines (149 loc) · 6.05 KB
/
PasteHandler.ts
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import { toggleMark } from "prosemirror-commands";
import { Plugin } from "prosemirror-state";
import { isInTable } from "prosemirror-tables";
import { isUrl } from "../../utils/urls";
import Extension from "../lib/Extension";
import isMarkdown from "../lib/isMarkdown";
import selectionIsInCode from "../queries/isInCode";
import { LANGUAGES } from "./Prism";
function isDropboxPaper(html: string): boolean {
// The best we have to detect if a paste is likely coming from Paper
// In this case it's actually better to use the text version
return html?.includes("usually-unique-id");
}
/**
* Add support for additional syntax that users paste even though it isn't
* supported by the markdown parser directly by massaging the text content.
*
* @param text The incoming pasted plain text
*/
function normalizePastedMarkdown(text: string): string {
const CHECKBOX_REGEX = /^\s?(\[(X|\s|_|-)\]\s(.*)?)/gim;
// find checkboxes not contained in a list and wrap them in list items
while (text.match(CHECKBOX_REGEX)) {
text = text.replace(CHECKBOX_REGEX, (match) => `- ${match.trim()}`);
}
// find multiple newlines and insert a hard break to ensure they are respected
text = text.replace(/\n{2,}/g, "\n\n\\\n");
return text;
}
export default class PasteHandler extends Extension {
get name() {
return "markdown-paste";
}
get plugins() {
return [
new Plugin({
props: {
transformPastedHTML(html: string) {
if (isDropboxPaper(html)) {
// Fixes double paragraphs when pasting from Dropbox Paper
html = html.replace(/<div><br><\/div>/gi, "<p></p>");
}
return html;
},
handlePaste: (view, event: ClipboardEvent) => {
if (view.props.editable && !view.props.editable(view.state)) {
return false;
}
if (!event.clipboardData) {
return false;
}
const text = event.clipboardData.getData("text/plain");
const html = event.clipboardData.getData("text/html");
const vscode = event.clipboardData.getData("vscode-editor-data");
const { state, dispatch } = view;
// first check if the clipboard contents can be parsed as a single
// url, this is mainly for allowing pasted urls to become embeds
if (isUrl(text)) {
// just paste the link mark directly onto the selected text
if (!state.selection.empty) {
toggleMark(this.editor.schema.marks.link, { href: text })(
state,
dispatch
);
return true;
}
// Is this link embeddable? Create an embed!
const { embeds } = this.editor.props;
if (embeds && !isInTable(state)) {
for (const embed of embeds) {
const matches = embed.matcher(text);
if (matches) {
this.editor.commands.embed({
href: text,
});
return true;
}
}
}
// well, it's not an embed and there is no text selected – so just
// go ahead and insert the link directly
const transaction = view.state.tr
.insertText(text, state.selection.from, state.selection.to)
.addMark(
state.selection.from,
state.selection.to + text.length,
state.schema.marks.link.create({ href: text })
);
view.dispatch(transaction);
return true;
}
// If the users selection is currently in a code block then paste
// as plain text, ignore all formatting and HTML content.
if (selectionIsInCode(view.state)) {
event.preventDefault();
view.dispatch(view.state.tr.insertText(text));
return true;
}
// Because VSCode is an especially popular editor that places metadata
// on the clipboard, we can parse it to find out what kind of content
// was pasted.
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
const pasteCodeLanguage = vscodeMeta?.mode;
if (pasteCodeLanguage && pasteCodeLanguage !== "markdown") {
event.preventDefault();
view.dispatch(
view.state.tr
.replaceSelectionWith(
view.state.schema.nodes.code_fence.create({
language: Object.keys(LANGUAGES).includes(vscodeMeta.mode)
? vscodeMeta.mode
: null,
})
)
.insertText(text)
);
return true;
}
// If the HTML on the clipboard is from Prosemirror then the best
// compatability is to just use the HTML parser, regardless of
// whether it "looks" like Markdown, see: outline/outline#2416
if (html?.includes("data-pm-slice")) {
return false;
}
// If the text on the clipboard looks like Markdown OR there is no
// html on the clipboard then try to parse content as Markdown
if (
(isMarkdown(text) && !isDropboxPaper(html)) ||
html.length === 0 ||
pasteCodeLanguage === "markdown"
) {
event.preventDefault();
const paste = this.editor.pasteParser.parse(
normalizePastedMarkdown(text)
);
const slice = paste.slice(0);
const transaction = view.state.tr.replaceSelection(slice);
view.dispatch(transaction);
return true;
}
// otherwise use the default HTML parser which will handle all paste
// "from the web" events
return false;
},
},
}),
];
}
}