Delete selected text in CKEditor 5 and retype it char-by-char to trigger live markdown behavior.
// ==UserScript==
// @name CKEditor Retype Selection (Markdown trigger)
// @namespace retype-ckeditor
// @version 1.0.0
// @description Delete selected text in CKEditor 5 and retype it char-by-char to trigger live markdown behavior.
// @match *://*/*
// @run-at document-idle
// @allFrames true
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(() => {
"use strict";
const HOTKEY = {
key: "Q",
shiftKey: true,
requireCtrlOrMeta: true
};
const BASE_DELAY_MS = 12;
const JITTER_MS = 8;
const CK_EDITABLE_SEL = ".ck-editor__editable[contenteditable='true']";
let running = false;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const delay = () => BASE_DELAY_MS + Math.floor(Math.random() * (JITTER_MS + 1));
function toast(msg) {
try {
const el = document.createElement("div");
el.textContent = msg;
el.style.cssText = `
position:fixed; z-index:2147483647; left:16px; bottom:16px;
padding:10px 12px; background:rgba(0,0,0,.85); color:#fff;
font:13px/1.3 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
border-radius:10px; box-shadow:0 6px 18px rgba(0,0,0,.25);
`;
document.documentElement.appendChild(el);
setTimeout(() => el.remove(), 1400);
} catch {}
}
function matchesHotkey(e) {
const keyMatches = e.key.toUpperCase() === HOTKEY.key.toUpperCase();
const modsMatch = (!!e.altKey === !!HOTKEY.altKey) && (!!e.shiftKey === !!HOTKEY.shiftKey);
const ctrlOrMetaOk = HOTKEY.requireCtrlOrMeta ? (e.ctrlKey || e.metaKey) : true;
return keyMatches && modsMatch && ctrlOrMetaOk;
}
function getSelectionRange() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return null;
const range = sel.getRangeAt(0);
if (range.collapsed) return null;
return range;
}
function getClosestCKEditableFromRange(range) {
const n = range.commonAncestorContainer;
const el = n.nodeType === Node.ELEMENT_NODE ? n : n.parentElement;
if (!el) return null;
return el.closest(CK_EDITABLE_SEL);
}
function dispatchBeforeInput(target, data, inputType) {
const ev = new InputEvent("beforeinput", { bubbles: true, cancelable: true, data, inputType });
return target.dispatchEvent(ev);
}
function dispatchInput(target, data, inputType) {
target.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: false, data, inputType }));
}
function deleteSelection(editable, range) {
editable.focus();
if (!dispatchBeforeInput(editable, null, "deleteByCut")) return false;
const ok = document.execCommand("delete");
if (!ok) {
try { range.deleteContents(); } catch { return false; }
}
dispatchInput(editable, null, "deleteByCut");
return true;
}
function insertText(editable, text) {
editable.focus();
if (!dispatchBeforeInput(editable, text, "insertText")) return false;
const ok = document.execCommand("insertText", false, text);
if (!ok) {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return false;
const r = sel.getRangeAt(0);
r.deleteContents();
r.insertNode(document.createTextNode(text));
r.collapse(false);
sel.removeAllRanges();
sel.addRange(r);
}
dispatchInput(editable, text, "insertText");
return true;
}
function insertParagraph(editable) {
editable.focus();
if (!dispatchBeforeInput(editable, null, "insertParagraph")) return false;
const ok = document.execCommand("insertParagraph");
if (!ok) document.execCommand("insertLineBreak");
dispatchInput(editable, null, "insertParagraph");
return true;
}
async function retype(editable, text) {
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (ch === "\r") continue;
if (ch === "\n") insertParagraph(editable);
else insertText(editable, ch);
await sleep(delay());
}
}
async function run() {
if (running) return;
const range = getSelectionRange();
if (!range) { toast("Retype: no selection"); return; }
const editable = getClosestCKEditableFromRange(range);
if (!editable) { toast("Retype: not in CKEditor"); return; }
const selectedText = window.getSelection().toString();
if (!selectedText) { toast("Retype: empty selection"); return; }
running = true;
toast("Retype: triggered");
try {
const ok = deleteSelection(editable, range);
if (!ok) { toast("Retype: delete blocked"); return; }
await sleep(30);
await retype(editable, selectedText);
} finally {
running = false;
}
}
document.addEventListener("keydown", (e) => {
if (!matchesHotkey(e)) return;
e.preventDefault();
e.stopPropagation();
run();
}, true);
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("CKEditor: Retype selected text", run);
}
})();