Tricks modern web editors (React, Notion, WordPress, etc) into accepting rendered HTML.
// ==UserScript==
// @name Paste Markdown as Rendered HTML
// @namespace http://tampermonkey.net/
// @version 1.1.1
// @description Tricks modern web editors (React, Notion, WordPress, etc) into accepting rendered HTML.
// @author Tertium
// @match *://*/*
// @grant none
// @run-at document-start
// @require https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Fast check to ensure the text contains valid Markdown syntax
const markdownRegex = /(?:^#{1,6}\s|^>|^\s*[-*+]\s|^\s*\d+\.\s|\*\*|__|\*|_|`|\[[^\]]+\]\([^)]+\))/m;
document.addEventListener('paste', function(e) {
// Prevent infinite loops from our own synthetic event
if (e.isSyntheticMarkdownPaste) return;
const clipboardData = e.clipboardData;
if (!clipboardData) return;
// 1. If you copied rich text (HTML) from another page, let it paste normally.
if (clipboardData.types.includes('text/html')) {
const htmlContent = clipboardData.getData('text/html');
if (htmlContent && htmlContent.trim() !== '') return;
}
// 2. Get the plain text from the clipboard and verify it contains Markdown.
const textData = clipboardData.getData('text/plain');
if (!textData || !markdownRegex.test(textData)) return;
// 3. Ensure we are pasting into a rich text editor.
const activeEl = document.activeElement;
const isRichText = activeEl && (
activeEl.isContentEditable ||
(activeEl.tagName === 'IFRAME' && activeEl.contentDocument && activeEl.contentDocument.designMode === 'on') ||
(typeof activeEl.closest === 'function' && activeEl.closest('[contenteditable="true"]'))
);
if (!isRichText || activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'INPUT') {
return;
}
// 4. Stop the browser's native pasting dead in its tracks
e.preventDefault();
e.stopPropagation();
// 5. Clean text & Parse Markdown
if (typeof marked === 'undefined') {
console.error("Markdown parser not loaded.");
return;
}
// This ensures 3+ empty lines are reduced to standard double lines (paragraphs)
// while letting marked.js handle normal spaces naturally.
const cleanText = textData.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n');
// Parse the markdown into HTML
const renderedHtml = marked.parse(cleanText, { gfm: true });
// 6. Create a brand new, fully populated DataTransfer object
const fakeDt = new DataTransfer();
fakeDt.setData('text/plain', cleanText);
fakeDt.setData('text/html', renderedHtml);
// 7. Generate a fake paste event carrying our new DataTransfer
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: fakeDt,
bubbles: true,
cancelable: true,
composed: true
});
pasteEvent.isSyntheticMarkdownPaste = true;
let targetNode = e.target;
if (targetNode.nodeType === Node.TEXT_NODE) targetNode = targetNode.parentNode;
// 8. Dispatch it to trick JS Frameworks
targetNode.dispatchEvent(pasteEvent);
// 9. THE FALLBACK for basic content-editable boxes
if (!pasteEvent.defaultPrevented) {
if (activeEl.tagName === 'IFRAME') {
activeEl.contentDocument.execCommand('insertHTML', false, renderedHtml);
} else {
document.execCommand('insertHTML', false, renderedHtml);
}
}
}, { capture: true });
})();