Greasy Fork is available in English.
Copy Dropbox Slate editor content with preserved line breaks, render .md files as formatted HTML or hybrid-styled Markdown, with segmented toolbar controls and persistent mode preference.
// ==UserScript== // @name Dropbox | Slate Editor — Copy & Markdown Render // @namespace https://greasyfork.org/en/users/1462137-piknockyou // @version 4.1 // @author Piknockyou (vibe-coded) // @license AGPL-3.0 // @description Copy Dropbox Slate editor content with preserved line breaks, render .md files as formatted HTML or hybrid-styled Markdown, with segmented toolbar controls and persistent mode preference. // @match *://*.dropbox.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-idle // @icon https://www.google.com/s2/favicons?sz=64&domain=dropbox.com // ==/UserScript== (function () { 'use strict'; // ── Config ─────────────────────────────────────────────────────────────── const CONFIG = { TOOLTIP_DELAY_MS: 0, // delay before tooltip appears (0 = instant) TOOLTIP_MAX_WIDTH: '220px', TOOLTIP_FONT_SIZE: '12px', TOOLTIP_PADDING: '7px 11px', TOOLTIP_BORDER_RAD: '6px', TOOLTIP_GAP: 10, // px between anchor element and tooltip TOOLTIP_MARGIN: 8, // min px from viewport edges }; // ── Tooltip ────────────────────────────────────────────────────────────── const Tooltip = (() => { const STYLES = { position: 'fixed', background: 'rgba(30,30,30,.96)', border: '1px solid rgba(128,128,128,.35)', borderRadius: CONFIG.TOOLTIP_BORDER_RAD, padding: CONFIG.TOOLTIP_PADDING, fontFamily: 'system-ui, sans-serif', fontSize: CONFIG.TOOLTIP_FONT_SIZE, fontWeight: '500', color: '#f0f0f0', boxShadow: '0 4px 12px rgba(0,0,0,0.4)', zIndex: '2147483647', pointerEvents: 'none', opacity: '0', visibility: 'hidden', transition: 'opacity 0.15s ease, visibility 0.15s ease', whiteSpace: 'pre-line', textAlign: 'center', lineHeight: '1.5', maxWidth: CONFIG.TOOLTIP_MAX_WIDTH, }; let _el = null, _timer = null; function _init() { if (_el) return; _el = document.createElement('div'); _el.setAttribute('role', 'tooltip'); Object.assign(_el.style, STYLES); document.body.appendChild(_el); } function _calculatePosition(r) { const MARGIN = CONFIG.TOOLTIP_MARGIN, GAP = CONFIG.TOOLTIP_GAP; _el.style.left = '-9999px'; _el.style.top = '-9999px'; _el.style.visibility = 'hidden'; _el.style.opacity = '0'; const tt = _el.getBoundingClientRect(); const tw = tt.width, th = tt.height; const vw = window.innerWidth, vh = window.innerHeight; let left, top; if (r.top - MARGIN >= th + GAP) { top = r.top - th - GAP; left = r.left + r.width / 2 - tw / 2; } else if (vh - r.bottom - MARGIN >= th + GAP) { top = r.bottom + GAP; left = r.left + r.width / 2 - tw / 2; } else if (vw - r.right - MARGIN >= tw + GAP) { left = r.right + GAP; top = r.top + r.height / 2 - th / 2; } else if (r.left - MARGIN >= tw + GAP) { left = r.left - tw - GAP; top = r.top + r.height / 2 - th / 2; } else { left = (vw - tw) / 2; top = Math.max(MARGIN, r.top - th - GAP); } left = Math.max(MARGIN, Math.min(left, vw - tw - MARGIN)); top = Math.max(MARGIN, Math.min(top, vh - th - MARGIN)); return { left, top }; } return { show(target, text) { _init(); _el.textContent = text; const pos = _calculatePosition(target.getBoundingClientRect()); _el.style.left = `${pos.left}px`; _el.style.top = `${pos.top}px`; _el.style.visibility = 'visible'; _el.style.opacity = '1'; }, scheduleShow(target, text, delay = CONFIG.TOOLTIP_DELAY_MS) { this.hide(); _timer = setTimeout(() => this.show(target, text), delay); }, hide() { if (_timer) { clearTimeout(_timer); _timer = null; } if (_el) { _el.style.opacity = '0'; _el.style.visibility = 'hidden'; } }, destroy() { this.hide(); if (_el) { _el.remove(); _el = null; } }, }; })(); const EDITOR_SEL = 'div[data-slate-editor="true"]'; const OVERLAY_CLS = 'dbx-md-overlay'; const HYBRID_CLS = 'dbx-md-hybrid'; const TOOLBAR_ID = 'dbx-md-toolbar'; const BTN_SIZE = 28; const FEEDBACK_MS = 1500; if (typeof markdownit === 'undefined') { console.error('[Slate-MD] markdown-it failed to load'); return; } const mdi = markdownit({ html: false, linkify: true, typographer: false, breaks: true }); let activeEditor = null; let currentMode = 'raw'; let toolbar = null; let feedbackTimer = null; let updateTimer = null; let editorObserver = null; let editorRetagTimer = null; const getStoredMode = () => GM_getValue('renderMode', 'raw'); const storeMode = m => GM_setValue('renderMode', m); // Text helpers function getLineText(li) { const spans = li.querySelectorAll('span[data-slate-string="true"]'); return spans.length ? Array.from(spans, s => s.textContent).join('').replace(/\uFEFF/g, '') : ''; } function extractText(editor) { if (!editor) return ''; const lines = []; for (const child of editor.children) { if (child.nodeName !== 'LI' || child.getAttribute('data-slate-node') !== 'element') continue; lines.push(getLineText(child)); } return lines.join('\n'); } // Copy function copyContent(showAlert = false) { if (!activeEditor) { if (showAlert) alert('No Slate editor content found.'); return; } GM_setClipboard(extractText(activeEditor)); if (showAlert) return; const btn = toolbar?.querySelector('[data-action="copy"]'); if (!btn) return; btn.textContent = '✓'; btn.disabled = true; clearTimeout(feedbackTimer); feedbackTimer = setTimeout(() => { btn.textContent = '📋'; btn.disabled = false; }, FEEDBACK_MS); } // Full render function getOverlay() { return activeEditor?.parentNode?.querySelector(`:scope > .${OVERLAY_CLS}`); } function applyFullRender() { if (!activeEditor) return; let overlay = getOverlay(); if (!overlay) { overlay = document.createElement('div'); overlay.className = OVERLAY_CLS; activeEditor.parentNode.insertBefore(overlay, activeEditor.nextSibling); } overlay.innerHTML = mdi.render(extractText(activeEditor)); overlay.style.display = ''; activeEditor.style.display = 'none'; } function removeFullRender() { if (!activeEditor) return; const overlay = getOverlay(); if (overlay) overlay.style.display = 'none'; activeEditor.style.display = ''; } // Hybrid const MD_ATTRS = [ 'data-md-level', 'data-md-bold', 'data-md-italic', 'data-md-code', 'data-md-delim', 'data-md-link', 'data-md-fence', ]; function applyHybrid() { if (!activeEditor) return; activeEditor.classList.add(HYBRID_CLS); tagAllElements(activeEditor); startEditorObserver(); } function removeHybrid() { if (!activeEditor) return; stopEditorObserver(); activeEditor.classList.remove(HYBRID_CLS); const sel = MD_ATTRS.map(a => `[${a}]`).join(','); for (const el of activeEditor.querySelectorAll(sel)) { for (const a of MD_ATTRS) el.removeAttribute(a); } for (const el of activeEditor.querySelectorAll('[data-md-link-url]')) { el.removeAttribute('data-md-link-url'); el.style.cursor = ''; } } function startEditorObserver() { stopEditorObserver(); if (!activeEditor) return; editorObserver = new MutationObserver(() => { clearTimeout(editorRetagTimer); editorRetagTimer = setTimeout(() => { if (currentMode !== 'hybrid' || !activeEditor) return; const sel = MD_ATTRS.map(a => `[${a}]`).join(','); for (const el of activeEditor.querySelectorAll(sel)) { for (const a of MD_ATTRS) el.removeAttribute(a); } tagAllElements(activeEditor); }, 120); }); editorObserver.observe(activeEditor, { childList: true, subtree: true, characterData: true }); } function stopEditorObserver() { editorObserver?.disconnect(); editorObserver = null; clearTimeout(editorRetagTimer); } function tagAllElements(editor) { let inFence = false; for (const li of editor.children) { if (li.nodeName !== 'LI' || li.getAttribute('data-slate-node') !== 'element') continue; const text = getLineText(li); // Fence lines: ``` or ```language if (/^`{3,}/.test(text)) { li.setAttribute('data-md-fence', ''); inFence = !inFence; continue; } // Skip inline formatting inside fenced code blocks if (inFence) continue; const hMatch = text.match(/^(#{1,6})\s/); if (hMatch) li.setAttribute('data-md-level', hMatch[1].length); tagInlineFormatting(li); } } function tagInlineFormatting(li) { const leaves = Array.from(li.querySelectorAll('span[data-slate-leaf="true"]')); const isHeading = li.hasAttribute('data-md-level'); let skippedFirst = false; // First pass: handle gray/green leaves (bold, italic, links) let bold = false, italic = false; for (let i = 0; i < leaves.length; i++) { const leaf = leaves[i]; const isGray = /\b_gray_/.test(leaf.className); const isGreen = /\b_green_/.test(leaf.className); const strSpan = leaf.querySelector('span[data-slate-string="true"]'); if (!strSpan) continue; const t = strSpan.textContent.replace(/\uFEFF/g, ''); if (isGray && isHeading && !skippedFirst) { skippedFirst = true; leaf.setAttribute('data-md-delim', ''); continue; } if (isGreen) { leaf.setAttribute('data-md-link', ''); const url = extractLinkUrl(leaves, i); if (url) { leaf.setAttribute('data-md-link-url', url); leaf.style.cursor = 'pointer'; leaf.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); window.open(url, '_blank', 'noopener'); }); } continue; } if (isGray) { if (t === '**' || t === '__') { bold = !bold; leaf.setAttribute('data-md-delim', ''); leaf.setAttribute('data-md-bold', ''); } else if (t === '***' || t === '___') { bold = !bold; italic = !italic; leaf.setAttribute('data-md-delim', ''); leaf.setAttribute('data-md-bold', ''); leaf.setAttribute('data-md-italic', ''); } else if (t === '*' || t === '_') { italic = !italic; leaf.setAttribute('data-md-delim', ''); leaf.setAttribute('data-md-italic', ''); } else { leaf.setAttribute('data-md-delim', ''); } } else { if (bold) leaf.setAttribute('data-md-bold', ''); if (italic) leaf.setAttribute('data-md-italic', ''); } } // Second pass: detect code spans // Dropbox renders `code` as a single leaf containing backticks + content, // or occasionally as separate leaves. Handle both cases. let inCode = false; for (const leaf of leaves) { if (leaf.hasAttribute('data-md-link') || leaf.hasAttribute('data-md-delim')) continue; const strSpan = leaf.querySelector('span[data-slate-string="true"]'); if (!strSpan) continue; const t = strSpan.textContent.replace(/\uFEFF/g, ''); // Case 1: entire leaf is `content` (backticks inside one leaf) if (/^`.+`$/.test(t)) { leaf.setAttribute('data-md-code', ''); continue; } // Case 2: leaf is just a lone backtick (separate leaf delimiter) if (t === '`') { inCode = !inCode; leaf.setAttribute('data-md-code', ''); leaf.setAttribute('data-md-delim', ''); continue; } // Case 3: we're between two backtick delimiters if (inCode) { leaf.setAttribute('data-md-code', ''); } } } function extractLinkUrl(leaves, greenIndex) { for (let j = greenIndex + 1; j < leaves.length && j <= greenIndex + 3; j++) { const leaf = leaves[j]; if (!/\b_gray_/.test(leaf.className)) continue; const strSpan = leaf.querySelector('span[data-slate-string="true"]'); if (!strSpan) continue; const m = strSpan.textContent.replace(/\uFEFF/g, '').match(/\]\((.+?)\)/); if (m) return m[1]; } return null; } // Mode switching function setMode(mode) { if (currentMode === 'full') removeFullRender(); if (currentMode === 'hybrid') removeHybrid(); currentMode = mode; storeMode(mode); if (mode === 'full') applyFullRender(); if (mode === 'hybrid') applyHybrid(); syncToolbar(); } // Toolbar function syncToolbar() { if (!toolbar) return; for (const btn of toolbar.querySelectorAll('[data-mode]')) { btn.classList.toggle('dbx-md-seg--active', btn.dataset.mode === currentMode); } } function createToolbar() { if (toolbar && document.body.contains(toolbar)) { syncToolbar(); return; } toolbar = null; toolbar = document.createElement('div'); toolbar.id = TOOLBAR_ID; const seg = document.createElement('div'); seg.className = 'dbx-md-seg-group'; for (const { mode, icon, title, pos } of [ { mode: 'raw', icon: '📝', title: 'Raw text', pos: 'left' }, { mode: 'hybrid', icon: '✨', title: 'Hybrid Markdown', pos: 'mid' }, { mode: 'full', icon: '👁', title: 'Rendered Markdown', pos: 'right' }, ]) { const btn = document.createElement('button'); btn.className = `dbx-md-seg dbx-md-seg--${pos}`; btn.dataset.mode = mode; btn.textContent = icon; btn.title = ''; btn.addEventListener('click', e => { e.stopPropagation(); setMode(mode); }); btn.addEventListener('mouseenter', () => Tooltip.scheduleShow(btn, title)); btn.addEventListener('mouseleave', () => Tooltip.hide()); seg.appendChild(btn); } toolbar.appendChild(seg); const copy = document.createElement('button'); copy.className = 'dbx-md-copy'; copy.dataset.action = 'copy'; copy.textContent = '📋'; copy.title = ''; copy.addEventListener('click', e => { e.stopPropagation(); copyContent(); }); copy.addEventListener('mouseenter', () => Tooltip.scheduleShow(copy, 'Copy to clipboard')); copy.addEventListener('mouseleave', () => Tooltip.hide()); toolbar.appendChild(copy); toolbar.classList.add('dbx-md-toolbar--fixed'); document.body.appendChild(toolbar); syncToolbar(); } function removeToolbar() { Tooltip.destroy(); toolbar?.remove(); toolbar = null; } // Lifecycle function cleanup() { if (currentMode === 'full') removeFullRender(); if (currentMode === 'hybrid') removeHybrid(); if (activeEditor) { getOverlay()?.remove(); activeEditor.style.display = ''; } activeEditor = null; currentMode = 'raw'; } function updateState() { const editor = document.querySelector(EDITOR_SEL); if (editor && editor !== activeEditor) { cleanup(); activeEditor = editor; createToolbar(); const stored = getStoredMode(); if (stored !== 'raw') setMode(stored); else syncToolbar(); } else if (editor && editor === activeEditor) { if (!toolbar || !document.body.contains(toolbar)) { toolbar = null; createToolbar(); syncToolbar(); } } else if (!editor && activeEditor) { cleanup(); removeToolbar(); } } function startObserving() { new MutationObserver(() => { clearTimeout(updateTimer); updateTimer = setTimeout(updateState, 150); }).observe(document.documentElement, { childList: true, subtree: true }); } // Styles function injectStyles() { const S = BTN_SIZE; GM_addStyle(` #${TOOLBAR_ID} { display: inline-flex; align-items: center; gap: 6px; position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; background: rgba(30,30,30,.92); backdrop-filter: blur(8px); padding: 6px 8px; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,.3); } .dbx-md-seg-group { display: inline-flex; border: 1px solid rgba(128,128,128,.35); border-radius: 6px; overflow: hidden; } .dbx-md-seg { width: ${S}px; height: ${S}px; min-width: ${S}px; min-height: ${S}px; padding: 0; margin: 0; border: none; border-right: 1px solid rgba(128,128,128,.15); background: transparent; color: inherit; cursor: pointer; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; transition: background .15s; box-sizing: border-box; } .dbx-md-seg:last-child { border-right: none; } .dbx-md-seg:hover { background: rgba(0,97,254,.12); } .dbx-md-seg--active { background: #0061FE !important; color: #fff !important; } .dbx-md-seg--active:hover { background: #0052D9 !important; } .dbx-md-copy { width: ${S}px; height: ${S}px; min-width: ${S}px; min-height: ${S}px; padding: 0; margin: 0; border: 1px solid rgba(128,128,128,.35); border-radius: 6px; background: transparent; color: inherit; cursor: pointer; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; transition: background .15s; box-sizing: border-box; overflow: hidden; } .dbx-md-copy:hover { background: rgba(0,97,254,.12); } .dbx-md-copy:active { background: rgba(0,97,254,.2); } .dbx-md-copy:disabled { opacity: .5; cursor: default; } /* Full render overlay */ .${OVERLAY_CLS} { color: inherit; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 15px; line-height: 1.7; word-wrap: break-word; overflow-wrap: break-word; padding: 8px 0; } .${OVERLAY_CLS} h1, .${OVERLAY_CLS} h2 { border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .3em; margin-top: 1.5em; } .${OVERLAY_CLS} h1 { font-size: 1.8em; } .${OVERLAY_CLS} h2 { font-size: 1.4em; } .${OVERLAY_CLS} h3 { font-size: 1.2em; margin-top: 1.2em; } .${OVERLAY_CLS} h4 { font-size: 1.05em; margin-top: 1em; } .${OVERLAY_CLS} code { background: rgba(128,128,128,.12); padding: 2px 5px; border-radius: 4px; font-size: .9em; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; } .${OVERLAY_CLS} pre { background: rgba(128,128,128,.1); padding: 14px; border-radius: 6px; overflow-x: auto; } .${OVERLAY_CLS} pre code { background: none; padding: 0; } .${OVERLAY_CLS} blockquote { border-left: 4px solid #0061FE; margin: 0 0 16px; padding-left: 16px; opacity: .8; } .${OVERLAY_CLS} table { border-collapse: collapse; width: 100%; margin: 16px 0; } .${OVERLAY_CLS} td, .${OVERLAY_CLS} th { border: 1px solid rgba(128,128,128,.25); padding: 6px 12px; } .${OVERLAY_CLS} th { font-weight: bold; background: rgba(128,128,128,.08); } .${OVERLAY_CLS} a { color: #0061FE; } .${OVERLAY_CLS} img { max-width: 100%; } .${OVERLAY_CLS} hr { border: none; border-top: 1px solid rgba(128,128,128,.2); margin: 24px 0; } .${OVERLAY_CLS} ul, .${OVERLAY_CLS} ol { padding-left: 2em; } .${OVERLAY_CLS} li { margin: 4px 0; } .${OVERLAY_CLS} input[type="checkbox"] { margin-right: 6px; } /* Hybrid mode */ .${HYBRID_CLS} li[data-md-level] span[data-slate-string], .${HYBRID_CLS} li[data-md-level] [data-md-delim] span[data-slate-string] { color: #569CD6 !important; font-weight: 700 !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-bold] span[data-slate-string], .${HYBRID_CLS} [data-md-bold][data-md-delim] span[data-slate-string] { font-weight: 700 !important; color: #DCDCAA !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-italic] span[data-slate-string], .${HYBRID_CLS} [data-md-italic][data-md-delim] span[data-slate-string] { font-style: italic !important; color: #C586C0 !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-bold][data-md-italic] span[data-slate-string] { font-weight: 700 !important; font-style: italic !important; color: #C586C0 !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-code] { background: rgba(206,145,120,.15) !important; border-radius: 3px !important; padding: 0 3px !important; } .${HYBRID_CLS} [data-md-code] span[data-slate-string], .${HYBRID_CLS} [data-md-code][data-md-delim] span[data-slate-string] { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace !important; color: #CE9178 !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-link] span[data-slate-string] { color: #4EC9B0 !important; text-decoration: underline !important; text-underline-offset: 2px !important; opacity: 1 !important; font-size: 1em !important; } .${HYBRID_CLS} [data-md-link]:hover span[data-slate-string] { color: #6EDDD1 !important; } .${HYBRID_CLS} li[data-md-fence] span[data-slate-string] { color: #608B4E !important; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace !important; opacity: .7 !important; } `); } // Init function init() { injectStyles(); startObserving(); updateState(); GM_registerMenuCommand('Copy Slate Content', () => copyContent(true), 'C'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();