fix: Decrease sensitivity of markdown detection, closes #7873

This commit is contained in:
Tom Moor
2024-10-31 22:14:45 -04:00
parent d78eeaba84
commit d372ccf5b6
2 changed files with 134 additions and 125 deletions

View File

@@ -90,14 +90,8 @@ export default class PasteHandler extends Extension {
},
},
handlePaste: (view, event: ClipboardEvent) => {
// Do nothing if the document isn't currently editable
if (!view.editable) {
return false;
}
// Default behavior if there is nothing on the clipboard or were
// special pasting with no formatting (Shift held)
if (!event.clipboardData || this.shiftKey) {
// Do nothing if the document isn't currently editable or there is no clipboard data
if (!view.editable || !event.clipboardData) {
return false;
}
@@ -137,76 +131,6 @@ export default class PasteHandler extends Extension {
return true;
}
// Check if the clipboard contents can be parsed as a single url
if (isUrl(text)) {
// If there is selected text then we want to wrap it in a link to the url
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 &&
this.editor.commands.embed &&
!isInCode(state) &&
!isInList(state)
) {
for (const embed of embeds) {
if (!embed.matchOnInput) {
continue;
}
const matches = embed.matcher(text);
if (matches) {
this.editor.commands.embed({
href: text,
});
return true;
}
}
}
// Is the link a link to a document? If so, we can grab the title and insert it.
if (isDocumentUrl(text)) {
const slug = parseDocumentSlug(text);
if (slug) {
void stores.documents
.fetch(slug)
.then((document) => {
if (view.isDestroyed) {
return;
}
if (document) {
const { hash } = new URL(text);
const hasEmoji =
determineIconType(document.icon) === IconType.Emoji;
const title = `${hasEmoji ? document.icon + " " : ""}${
document.titleWithDefault
}`;
insertLink(`${document.path}${hash}`, title);
}
})
.catch(() => {
if (view.isDestroyed) {
return;
}
insertLink(text);
});
}
} else {
insertLink(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.
@@ -215,52 +139,129 @@ export default class PasteHandler extends Extension {
const supportsCodeBlock = !!state.schema.nodes.code_block;
const supportsCodeMark = !!state.schema.marks.code_inline;
if (pasteCodeLanguage && pasteCodeLanguage !== "markdown") {
if (text.includes("\n") && supportsCodeBlock) {
event.preventDefault();
view.dispatch(
state.tr
.replaceSelectionWith(
state.schema.nodes.code_block.create({
language: Object.keys(LANGUAGES).includes(
vscodeMeta.mode
)
? vscodeMeta.mode
: null,
if (!this.shiftKey) {
// Check if the clipboard contents can be parsed as a single url
if (isUrl(text)) {
// If there is selected text then we want to wrap it in a link to the url
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 &&
this.editor.commands.embed &&
!isInCode(state) &&
!isInList(state)
) {
for (const embed of embeds) {
if (!embed.matchOnInput) {
continue;
}
const matches = embed.matcher(text);
if (matches) {
this.editor.commands.embed({
href: text,
});
return true;
}
}
}
// Is the link a link to a document? If so, we can grab the title and insert it.
if (isDocumentUrl(text)) {
const slug = parseDocumentSlug(text);
if (slug) {
void stores.documents
.fetch(slug)
.then((document) => {
if (view.isDestroyed) {
return;
}
if (document) {
const { hash } = new URL(text);
const hasEmoji =
determineIconType(document.icon) === IconType.Emoji;
const title = `${
hasEmoji ? document.icon + " " : ""
}${document.titleWithDefault}`;
insertLink(`${document.path}${hash}`, title);
}
})
)
.insertText(text)
);
.catch(() => {
if (view.isDestroyed) {
return;
}
insertLink(text);
});
}
} else {
insertLink(text);
}
return true;
}
if (supportsCodeMark) {
event.preventDefault();
view.dispatch(
state.tr
.insertText(text, state.selection.from, state.selection.to)
.addMark(
state.selection.from,
state.selection.to + text.length,
state.schema.marks.code_inline.create()
)
);
return true;
}
}
if (pasteCodeLanguage && pasteCodeLanguage !== "markdown") {
if (text.includes("\n") && supportsCodeBlock) {
event.preventDefault();
view.dispatch(
state.tr
.replaceSelectionWith(
state.schema.nodes.code_block.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 (supportsCodeMark) {
event.preventDefault();
view.dispatch(
state.tr
.insertText(
text,
state.selection.from,
state.selection.to
)
.addMark(
state.selection.from,
state.selection.to + text.length,
state.schema.marks.code_inline.create()
)
);
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)) ||
pasteCodeLanguage === "markdown"
pasteCodeLanguage === "markdown" ||
this.shiftKey
) {
event.preventDefault();

View File

@@ -1,34 +1,42 @@
export default function isMarkdown(text: string): boolean {
let signals = 0;
const lines = text.split("\n").length;
const minConfidence = Math.min(3, Math.floor(lines / 5));
// code-ish
const fences = text.match(/^```/gm);
if (fences && fences.length > 1) {
return true;
signals += fences.length;
}
// link-ish
if (text.match(/\[[^]+\]\(https?:\/\/\S+\)/gm)) {
return true;
const links = text.match(/\[[^]+\]\(https?:\/\/\S+\)/gm);
if (links) {
signals += links.length * 2;
}
if (text.match(/\[[^]+\]\(\/\S+\)/gm)) {
return true;
const relativeLinks = text.match(/\[[^]+\]\(\/\S+\)/gm);
if (relativeLinks) {
signals += relativeLinks.length * 2;
}
// heading-ish
if (text.match(/^#{1,6}\s+\S+/gm)) {
return true;
const headings = text.match(/^#{1,6}\s+\S+/gm);
if (headings) {
signals += headings.length;
}
// list-ish
const listItems = text.match(/^([-*]|\d+.)\s\S+/gm);
if (listItems && listItems.length > 1) {
return true;
if (listItems) {
signals += listItems.length;
}
// table header-ish
const tables = text.match(/\|\s?[:-]+\s?\|/gm);
if (tables && tables.length > 1) {
return true;
if (tables) {
signals += tables.length;
}
return false;
return signals > minConfidence;
}