您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
One-click (or hotkey) CodePen→Markdown: HTML/CSS/JS fences with optional attribution; raw or compiled output (SCSS→CSS, TS/Babel→JS); customizable shortcut; persistent preferences.
// ==UserScript== // @name CodePen.md - Copy as Markdown // @namespace https://github.com/AstroMash/userscripts // @version 2.3.1 // @description One-click (or hotkey) CodePen→Markdown: HTML/CSS/JS fences with optional attribution; raw or compiled output (SCSS→CSS, TS/Babel→JS); customizable shortcut; persistent preferences. // @author AstroMash // @match https://codepen.io/*/pen/* // @match https://cdpn.io/* // @run-at document-idle // @grant GM_setClipboard // @grant GM_notification // @grant GM_addStyle // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license MIT // @icon https://raw.githubusercontent.com/astromash/userscripts/main/scripts/codepen-md/icon.png // ==/UserScript== (function () { 'use strict'; const APP_TITLE = 'CodePen.md'; const NOTIFY_TAG = 'codepen-md-status'; const ORIGIN_PARENT = 'https://codepen.io'; const ORIGIN_CHILD = 'https://cdpn.io'; const IS_PREVIEW = location.hostname.endsWith('cdpn.io'); const IS_PARENT = location.hostname.endsWith('codepen.io'); // Prefs const PREF = { processed: 'cpmd_processed', // '1'|'0' includeHeader: 'cpmd_include_header', // '1'|'0' shortcutEnabled: 'cpmd_shortcut_enabled', // '1'|'0' shortcutCombo: 'cpmd_shortcut_combo', // JSON string: {ctrl,alt,shift,meta,key,code} }; // Default shortcut combo const DEFAULT_SHORTCUT = { alt: true, shift: true, ctrl: false, meta: false, key: 'x', code: 'KeyX', }; // Initialize prefs if (localStorage.getItem(PREF.processed) == null) localStorage.setItem(PREF.processed, '0'); if (localStorage.getItem(PREF.includeHeader) == null) localStorage.setItem(PREF.includeHeader, '1'); if (localStorage.getItem(PREF.shortcutEnabled) == null) localStorage.setItem(PREF.shortcutEnabled, '1'); if (localStorage.getItem(PREF.shortcutCombo) == null) localStorage.setItem( PREF.shortcutCombo, JSON.stringify(DEFAULT_SHORTCUT) ); const getPref = (k) => localStorage.getItem(k); const setPref = (k, v) => localStorage.setItem(k, v); const isTrue = (k) => getPref(k) === '1'; const getShortcutCombo = () => { try { return JSON.parse(getPref(PREF.shortcutCombo)); } catch { return DEFAULT_SHORTCUT; } }; // ---- Pref bus + menu refresh const CAN_UNREGISTER = typeof GM_unregisterMenuCommand === 'function' || (typeof GM === 'object' && typeof GM?.unregisterMenuCommand === 'function'); const Menu = { // Commands are preferences and actions that can be toggled or executed // from the userscript menu in the browser extension. The `id` is set to // the return value of GM_registerMenuCommand, which can be used to // unregister the command later if needed (and if supported). Unregistering // then re-registering is useful for toggling checkmarks for preference commands. commands: { setProcessed: { id: null, pref: PREF.processed, caption: 'Use processed output (SCSS→CSS, etc)', commandFn: () => togglePref(PREF.processed, { toastLabel: 'Compiled code' }), accessKey: 'P', }, setHeader: { id: null, pref: PREF.includeHeader, caption: 'Include source header', commandFn: () => togglePref(PREF.includeHeader, { toastLabel: 'Attribution header', }), accessKey: 'H', }, setShortcut: { id: null, pref: PREF.shortcutEnabled, caption: 'Enable keyboard shortcut', commandFn: () => togglePref(PREF.shortcutEnabled, { toastLabel: 'Keyboard shortcut', }), accessKey: 'S', }, execCopy: { id: null, pref: null, // not a pref, just a command caption: 'Copy CodePen as Markdown', commandFn: () => extractAndCopy({ userGesture: true }), accessKey: 'C', }, }, registered: false, // whether the menu commands are registered }; const registerMenuCommands = (menu) => { if (typeof GM_registerMenuCommand !== 'function') return; if (menu.registered) return; // already registered if (!menu.commands || typeof menu.commands !== 'object') return; // Helper to add checkmark to preference captions const setCheckmark = (pref, label) => `${isTrue(pref) ? '✓ ' : ' '}${label}`; Object.entries(menu.commands).forEach(([key, config]) => { let { caption, pref, commandFn, accessKey } = config; if (!caption || typeof commandFn !== 'function') return; if (pref) caption = setCheckmark(pref, caption); // only preferences need checkmarks if (!accessKey) { // default to first letter of key after stripping 'set' or 'exec' accessKey = key.startsWith('set') || key.startsWith('exec') ? key.slice(3) : key; } if (accessKey.length > 1) { // if accessKey is more than one character, use first character accessKey = accessKey.charAt(0); } menu.commands[key].id = GM_registerMenuCommand( `${caption} [${accessKey.toUpperCase()}]`, commandFn, accessKey.toUpperCase() ); }); menu.registered = true; }; const _unreg = (id) => { try { if (!id) return; if (typeof GM_unregisterMenuCommand === 'function') GM_unregisterMenuCommand(id); else if ( typeof GM === 'object' && typeof GM.unregisterMenuCommand === 'function' ) GM.unregisterMenuCommand(id); } catch {} }; function refreshMenu({ force = false } = {}) { if (!CAN_UNREGISTER && Menu.registered && !force) return; if (CAN_UNREGISTER) { Object.entries(Menu.commands).forEach(([key, cmd]) => { const { id } = cmd; if (id) { _unreg(id); cmd.id = null; } }); } Menu.registered = false; // reset state registerMenuCommands(Menu); } function emitPref(key, value) { try { window.dispatchEvent( new CustomEvent('cpmd:prefs', { detail: { key, value } }) ); } catch {} } function togglePref(key, { toastLabel } = {}) { const next = isTrue(key) ? '0' : '1'; setPref(key, next); emitPref(key, next); refreshMenu(); // re-label menu broadcastPrefs(); // sync to preview if (toastLabel) { toast(`${toastLabel} ${next === '1' ? 'enabled' : 'disabled'}.`, { type: 'info', }); } } // ---------- Styles (parent only) ---------- if (IS_PARENT && typeof GM_addStyle === 'function') { GM_addStyle(` /* Main button wrapper */ .cpmd-wrap { position: fixed; right: 24px; bottom: 32px; z-index: 2147483647; display: flex; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,.4), 0 2px 10px rgba(0,0,0,.2); backdrop-filter: blur(20px) saturate(180%); } /* Main copy button */ .cpmd-btn-main { padding: 14px 18px; border: 0; background: rgba(17, 24, 39, 0.95); color: #e5e7eb; font: 500 14px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; cursor: pointer; transition: all 0.2s ease; position: relative; overflow: hidden; border: 1px solid rgba(255,255,255,.08); border-right: 0; } .cpmd-btn-main::before { content: ''; position: absolute; top: 0; left: -100%; right: 100%; bottom: 0; background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.15), transparent); transition: left 0.5s ease, right 0.5s ease; } .cpmd-btn-main:hover::before { left: 100%; right: -100%; } .cpmd-btn-main:hover { background: rgba(17, 24, 39, 0.98); color: #f3f4f6; } .cpmd-btn-main span { position: relative; z-index: 1; letter-spacing: -0.01em; } .cpmd-btn-main.is-busy { opacity: .7; cursor: wait; } .cpmd-btn-main.is-busy span::after { content: ''; display: inline-block; width: 8px;height: 8px;margin-left: 8px; border: 2px solid rgba(102, 126, 234, 0.3); border-top-color: #667eea; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Gear button */ .cpmd-btn-gear { width: 44px; min-width: 44px; border: 0; border-left: 1px solid rgba(255,255,255,.08); background: rgba(17, 24, 39, 0.95); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; position: relative; border: 1px solid rgba(255,255,255,.08); border-left: 0; } .cpmd-btn-gear:hover { background: rgba(30, 41, 59, 0.95); } .cpmd-btn-gear[aria-expanded="true"] { background: rgba(30, 41, 59, 0.98); border-color: rgba(102, 126, 234, 0.3); } .cpmd-gear-ic { width: 18px; height: 18px; display: block; transition: transform .3s cubic-bezier(.4,0,.2,1); opacity: .8; } .cpmd-btn-gear:hover .cpmd-gear-ic { opacity: 1; } .cpmd-btn-gear[aria-expanded="true"] .cpmd-gear-ic { transform: rotate(60deg); opacity: 1; } /* Toast notifications */ .cpxt-toast-wrap { position: fixed; z-index: 2147483647; right: 24px; top: 24px; display: flex; flex-direction: column; gap: 10px; font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; } .cpxt-toast { min-width: 280px; max-width: 420px; padding: 14px 16px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,.3), 0 2px 10px rgba(0,0,0,.2); background: rgba(17, 24, 39, 0.98); backdrop-filter: blur(20px) saturate(180%); border: 1px solid rgba(255,255,255,.08); color: #e5e7eb; opacity: 0; transform: translateY(-10px) scale(0.95); transition: all .3s cubic-bezier(.4,0,.2,1); } .cpxt-toast.show { opacity: 1; transform: translateY(0) scale(1); } .cpxt-toast .cpxt-title { font-weight: 600; margin-bottom: 4px; color: #f3f4f6; letter-spacing: -0.01em; } .cpxt-toast.info { border-left: 3px solid #60a5fa; background: linear-gradient(to right, rgba(59,130,246,.08), rgba(17,24,39,.98)); } .cpxt-toast.warn { border-left: 3px solid #fbbf24; background: linear-gradient(to right, rgba(245,158,11,.08), rgba(17,24,39,.98)); } .cpxt-toast.error{ border-left: 3px solid #f87171; background: linear-gradient(to right, rgba(239,68,68,.08), rgba(17,24,39,.98)); } .cpxt-toast.success{border-left: 3px solid #34d399; background: linear-gradient(to right, rgba(16,185,129,.08), rgba(17,24,39,.98));} .cpxt-toast details{margin-top:8px;} .cpxt-toast summary{cursor:pointer;user-select:none;font-size:13px;color:#9ca3af;transition:color .2s ease;} .cpxt-toast summary:hover{color:#e5e7eb;} /* Options panel */ .cpmd-panel { position: fixed; right: 24px; bottom: 84px; z-index: 2147483646; width: 380px; background: rgba(17, 24, 39, 0.98); backdrop-filter: blur(20px) saturate(180%); color: #e5e7eb; border: 1px solid rgba(255,255,255,.1); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.3); padding: 0; display: none; overflow: hidden; animation: panelSlideUp .3s cubic-bezier(.4,0,.2,1); } @keyframes panelSlideUp { from{opacity:0;transform:translateY(10px) scale(.95);} to{opacity:1;transform:translateY(0) scale(1);} } .cpmd-panel.show { display: block; } .cpmd-panel h3 { margin:0; padding:18px 20px; font:600 16px/1.2 -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; background: linear-gradient(135deg, rgba(102,126,234,.1), rgba(118,75,162,.1)); border-bottom:1px solid rgba(255,255,255,.08); letter-spacing:-.01em; } .cpmd-panel-body{ padding:16px 20px 20px; } .cpmd-opt{ display:flex; gap:12px; align-items:flex-start; margin:0; padding:12px; border-radius:8px; font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; transition:background .2s ease; cursor:pointer; } .cpmd-opt:hover{ background: rgba(255,255,255,.05); } .cpmd-opt + .cpmd-opt { margin-top:8px; } .cpmd-opt input[type="checkbox"]{ margin-top:2px; width:18px; height:18px; accent-color:#667eea; cursor:pointer; } .cpmd-opt-label{ flex:1; cursor:pointer; } .cpmd-opt-title{ font-weight:600; color:#f3f4f6; margin-bottom:2px; } .cpmd-opt-desc{ font-size:13px; color:#9ca3af; line-height:1.4; } .cpmd-separator{ height:1px; background:rgba(255,255,255,.08); margin:16px -20px; } /* Shortcut section wrapper */ .cpmd-shortcut-wrapper { margin-top: 8px; } /* Collapsed shortcut row */ .cpmd-shortcut-row { display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; transition: background 0.2s ease; } .cpmd-shortcut-row:hover { background: rgba(255,255,255,.05); } .cpmd-shortcut-row input[type="checkbox"] { margin: 0; width: 18px; height: 18px; accent-color: #667eea; cursor: pointer; pointer-events: auto; } .cpmd-shortcut-info { flex: 1; display: flex; align-items: center; gap: 10px; } .cpmd-shortcut-label { font-weight: 600; color: #f3f4f6; font-size: 14px; pointer-events: auto; } .cpmd-shortcut-badge { padding: 4px 10px; background: rgba(102, 126, 234, 0.12); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: #93c5fd; font-weight: 500; } .cpmd-shortcut-row.disabled .cpmd-shortcut-badge { opacity: 0.5; } .cpmd-edit-btn { padding: 6px 12px; background: transparent; border: 1px solid rgba(255,255,255,.1); border-radius: 6px; color: #9ca3af; font: 12px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; cursor: pointer; transition: all 0.2s ease; } .cpmd-edit-btn:hover { background: rgba(255,255,255,.05); color: #e5e7eb; border-color: rgba(102, 126, 234, 0.3); } .cpmd-edit-btn.is-editing { color: #60a5fa; border-color: rgba(102, 126, 234, 0.4); background: rgba(102, 126, 234, 0.08); } .cpmd-shortcut-row.disabled .cpmd-edit-btn { opacity: 0.4; pointer-events: none; } /* Expandable config section */ .cpmd-shortcut-expand { overflow: hidden; max-height: 0; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .cpmd-shortcut-expand.show { max-height: 300px; } .cpmd-shortcut-config { padding: 12px; margin: 0 12px 12px; border: 1px solid rgba(255,255,255,.06); border-radius: 10px; background: rgba(255,255,255,.02); animation: fadeInConfig 0.2s ease; } @keyframes fadeInConfig { from { opacity: 0; } to { opacity: 1; } } /* Modifier keys row */ .cpmd-modifier-row { display: flex; gap: 6px; margin-bottom: 10px; } .cpmd-mod-key { flex: 1; display: flex; align-items: center; justify-content: center; gap: 4px; padding: 8px 4px; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.2s ease; } .cpmd-mod-key:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.12); } .cpmd-mod-key.active { background: rgba(102, 126, 234, 0.12); border-color: rgba(102, 126, 234, 0.35); } .cpmd-mod-key input { margin: 0; width: 14px; height: 14px; accent-color: #667eea; } .cpmd-mod-key span { user-select: none; font-weight: 500; } /* Main key capture section */ .cpmd-key-section { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; } .cpmd-key-capture { flex: 1; padding: 10px 14px; border: 1px solid rgba(255,255,255,.08); border-radius: 6px; background: rgba(255,255,255,.03); color: #e5e7eb; cursor: pointer; font: 13px/1.2 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; transition: all 0.2s ease; display: flex; align-items: center; justify-content: space-between; } .cpmd-key-capture:hover { background: rgba(255,255,255,.06); border-color: rgba(102, 126, 234, 0.3); } .cpmd-key-capture.is-capturing { background: rgba(102, 126, 234, 0.12); border-color: rgba(102, 126, 234, 0.5); box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15); } .cpmd-key-capture-label { color: #9ca3af; font-size: 12px; } .cpmd-key-value { padding: 3px 8px; background: rgba(102, 126, 234, 0.15); border: 1px solid rgba(102, 126, 234, 0.25); border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; font-weight: 500; color: #93c5fd; } .cpmd-key-capture.is-capturing .cpmd-key-value { animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } /* Bottom row with reset and done */ .cpmd-shortcut-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .cpmd-reset-btn { padding: 6px 12px; background: transparent; border: 1px solid rgba(255,255,255,.08); border-radius: 6px; color: #9ca3af; font: 12px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; cursor: pointer; transition: all 0.2s ease; } .cpmd-reset-btn:hover { background: rgba(255,255,255,.05); color: #e5e7eb; border-color: rgba(255,255,255,.15); } .cpmd-done-btn { padding: 6px 14px; background: rgba(102, 126, 234, 0.15); border: 1px solid rgba(102, 126, 234, 0.25); border-radius: 6px; color: #93c5fd; font: 12px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; cursor: pointer; transition: all 0.2s ease; } .cpmd-done-btn:hover { background: rgba(102, 126, 234, 0.2); border-color: rgba(102, 126, 234, 0.35); } /* Overlay for copy dialog */ .cpmd-overlay { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; animation: fadeIn .2s ease; } @keyframes fadeIn { from{opacity:0;} to{opacity:1;} } .cpmd-card { background: rgba(17, 24, 39, 0.98); backdrop-filter: blur(20px) saturate(180%); color: #e5e7eb; min-width: 380px; max-width: 480px; padding: 32px; border-radius: 16px; border: 1px solid rgba(255,255,255,.1); box-shadow: 0 30px 80px rgba(0,0,0,.5), 0 10px 40px rgba(0,0,0,.3); text-align: center; animation: cardSlideUp .3s cubic-bezier(.4,0,.2,1); } @keyframes cardSlideUp { from{opacity:0;transform:translateY(20px) scale(.9);} to{opacity:1;transform:translateY(0) scale(1);} } .cpmd-card h2 { margin: 0 0 12px; font: 600 22px/1.2 -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; color: #f3f4f6; letter-spacing: -0.02em; } .cpmd-card p { margin: 0 0 24px; font: 15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; color: #9ca3af; } .cpmd-cta { display:inline-flex; align-items:center; gap:10px; font:500 15px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; padding:12px 20px; border-radius:8px; border:1px solid rgba(102,126,234,.3); background:rgba(102,126,234,.15); color:#e5e7eb; cursor:pointer; transition:all .2s ease; letter-spacing:-.01em; } .cpmd-cta:hover{ background: rgba(102,126,234,.25); border-color: rgba(102,126,234,.5); transform: translateY(-1px); } .cpmd-cta:active{ transform: translateY(0); } .cpmd-ghost{ margin-left:12px; background:transparent; color:#9ca3af; border:1px solid rgba(229,231,235,.15); } .cpmd-ghost:hover{ background:rgba(255,255,255,.05); color:#e5e7eb; border-color:rgba(229,231,235,.25); } `); } // ---------- Toasts (parent only) ---------- function ensureToastWrap() { if (!IS_PARENT) return; if (!document.querySelector('.cpxt-toast-wrap')) { const wrap = document.createElement('div'); wrap.className = 'cpxt-toast-wrap'; document.body.appendChild(wrap); } } function toast(msg, { type = 'info', title = APP_TITLE, details } = {}) { if (!IS_PARENT) return; ensureToastWrap(); const wrap = document.querySelector('.cpxt-toast-wrap'); const el = document.createElement('div'); el.className = `cpxt-toast ${type}`; el.innerHTML = ` ${title ? `<div class="cpxt-title">${title}</div>` : ''} <div>${msg}</div> ${ details?.length ? `<details><summary>Details</summary><ul style="margin:6px 0 0 18px">${details .map((d) => `<li>${d}</li>`) .join('')}</ul></details>` : '' } `; wrap.appendChild(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 200); }, 4000); } // ---------- Desktop notifications (parent only) ---------- function notifyDesktop({ text, title = APP_TITLE, timeout = 5000, tag = NOTIFY_TAG, highlight = false, url, onclick, ondone, image, silent, } = {}) { if (!IS_PARENT) return; const api = (typeof GM_notification === 'function' && ((o) => GM_notification(o))) || (typeof GM === 'object' && (GM.notification || GM.notify)); if (!api) return; try { api({ text, title, timeout, tag, highlight, url, onclick, ondone, image, silent, }); } catch {} } // ---------- Shared helpers ---------- const rootWin = (() => { try { const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; return uw.top || uw; } catch { return window; } })(); function hasFocus() { try { return document.hasFocus(); } catch { return true; } } function onReady(fn) { if ( document.readyState === 'complete' || document.readyState === 'interactive' ) { fn(); } else window.addEventListener('DOMContentLoaded', fn, { once: true }); } async function waitFor( predicate, { timeout = 15000, interval = 120 } = {} ) { const start = performance.now(); return new Promise((resolve) => { (function tick() { try { const v = predicate(); if (v) return resolve(v); } catch {} if (performance.now() - start >= timeout) return resolve(null); setTimeout(tick, interval); })(); }); } function formatShortcutDisplay(combo = getShortcutCombo()) { const parts = []; if (combo.ctrl) parts.push('Ctrl'); if (combo.alt) parts.push('Alt'); if (combo.shift) parts.push('Shift'); if (combo.meta) parts.push( /Mac|iPhone|iPad/.test(navigator.platform || '') ? '⌘' : '⊞' ); const last = (() => { if (combo.code && !/^Key[A-Z]$/.test(combo.code)) return combo.code.toUpperCase(); return (combo.key || '').toString().toUpperCase(); })(); parts.push(last); return parts.join('+'); } function bindShortcut(handler) { if (!isTrue(PREF.shortcutEnabled)) return; const combo = getShortcutCombo(); let cooldown = false; const match = (e) => { if (!!combo.alt !== !!e.altKey) return false; if (!!combo.shift !== !!e.shiftKey) return false; if (!!combo.ctrl !== !!e.ctrlKey) return false; if (!!combo.meta !== !!e.metaKey) return false; const k = (e.key || '').toLowerCase(); const c = e.code || ''; return k === (combo.key || '').toLowerCase() || c === combo.code; }; const listener = (e) => { if (cooldown || e.repeat) return; if (match(e)) { e.preventDefault(); e.stopPropagation(); cooldown = true; setTimeout(() => (cooldown = false), 600); handler(e); } }; window.addEventListener('keydown', listener, true); document.addEventListener('keydown', listener, true); return () => { window.removeEventListener('keydown', listener, true); document.removeEventListener('keydown', listener, true); }; } // ---------- CP-first extraction (parent only) ---------- function usernameFromProfiled(pen, prof) { if (prof?.id && pen?.user_id && prof.id === pen.user_id) { return ( (prof.base_url || '').replace(/^\/|\/$/g, '') || prof.name || null ); } return null; } function usernameFromURL() { return location.pathname.split('/')[1] || null || null; } // NOTE: if processed, always return normal languages. function fenceLang(kind, pen = rootWin.CP?.pen || {}, processed = false) { if (processed) return kind === 'js' ? 'javascript' : kind; if ( kind === 'css' && pen.css_pre_processor && pen.css_pre_processor !== 'none' ) return pen.css_pre_processor; if ( kind === 'js' && pen.js_pre_processor && pen.js_pre_processor !== 'none' ) return pen.js_pre_processor === 'babel' ? 'javascript' : pen.js_pre_processor; return kind === 'js' ? 'javascript' : kind; } async function getCodeFromCP({ preferProcessed = false } = {}) { const CP = rootWin.CP; if (!CP) return null; const pen = CP.pen || {}; let html = '', css = '', js = ''; let source = 'raw'; if ( preferProcessed && typeof CP.getProcessedBodyByType === 'function' ) { try { if (CP.ensureProcessingRunOnce) { try { await CP.ensureProcessingRunOnce(); } catch {} } [html, css, js] = await Promise.all( ['html', 'css', 'js'].map((t) => CP.getProcessedBodyByType(t).catch(() => '') ) ); source = 'processed'; } catch {} } if (!html && !css && !js) { html = pen.html || ''; css = pen.css || ''; js = pen.js || ''; source = 'raw'; } const profUser = usernameFromProfiled(pen, CP.profiled); const username = profUser || usernameFromURL(); const id = pen.hashid || pen.slug_hash || location.pathname.split('/')[3] || null; const url = username && id ? `https://codepen.io/${username}/pen/${id}` : location.href; return { html, css, js, source, meta: { title: pen.title || document.title.replace(/\s*-\s*CodePen\s*$/i, '') || 'Untitled Pen', username, displayName: CP.profiled?.name || username || '', id, url, pen, processed: source === 'processed', }, }; } function safeGetEditor(util, type) { try { return util?.getEditorByType?.(type) || null; } catch { return null; } } function readEditorText(ed) { try { if (!ed) return ''; if (typeof ed.value === 'string') return ed.value; if (typeof ed.getValue === 'function') return ed.getValue(); if (ed.getDoc && typeof ed.getDoc === 'function') { const doc = ed.getDoc(); if (doc?.getValue) return doc.getValue(); } const s = ed.view?.state?.doc ?? ed.state?.doc ?? ed._state?.doc; if (s?.toString) return s.toString(); } catch {} return ''; } async function getViaUtil({ timeout = 7000 } = {}) { const util = await waitFor(() => rootWin.CodeEditorsUtil, { timeout }); if (!util) return null; return { html: readEditorText(safeGetEditor(util, 'html')), css: readEditorText(safeGetEditor(util, 'css')), js: readEditorText(safeGetEditor(util, 'js')), source: 'util', meta: { title: document.querySelector('meta[property="og:title"]') ?.content || document.title.replace(/\s*-\s*CodePen\s*$/i, '') || 'Untitled Pen', username: usernameFromURL(), displayName: usernameFromURL() || '', id: location.pathname.split('/')[3] || null, url: location.href, pen: {}, processed: false, }, }; } function toMarkdown({ html, css, js, meta }) { const includeHeader = isTrue(PREF.includeHeader); const processed = !!meta?.processed; const blocks = []; if (html) blocks.push( `\`\`\`${fenceLang( 'html', meta?.pen, processed )}\n${html}\n\`\`\`` ); if (css) blocks.push( `\`\`\`${fenceLang( 'css', meta?.pen, processed )}\n${css}\n\`\`\`` ); if (js) blocks.push( `\`\`\`${fenceLang('js', meta?.pen, processed)}\n${js}\n\`\`\`` ); const header = includeHeader && meta?.url ? `> Source: [${meta.title} by ${meta.displayName}](${meta.url})\n\n` : ''; return header + blocks.join('\n\n') + (blocks.length ? '\n' : ''); } async function generateMarkdownForPage() { const params = new URLSearchParams(location.search); const preferProcessed = params.get('processed') === '1' || isTrue(PREF.processed); let data = await getCodeFromCP({ preferProcessed }); if (!data || (!data.html && !data.css && !data.js)) { toast('Falling back to editor API.', { type: 'warn' }); data = (await getViaUtil({ timeout: 15000 })) || { html: '', css: '', js: '', meta: null, }; } else { toast( `Using ${ data.source === 'processed' ? 'compiled' : 'raw' } code via CP.`, { type: 'info' } ); } return { md: toMarkdown(data), meta: data.meta }; } // ---------- Copy helpers (parent) ---------- function focusAnyEditor() { try { const util = rootWin.CodeEditorsUtil; util?.getEditorByType?.('html')?.focus?.() || util?.getEditorByType?.('css')?.focus?.() || util?.getEditorByType?.('js')?.focus?.(); } catch {} } async function copyMarkdown(md, { userGesture = false } = {}) { if (!md || !md.trim()) throw new Error('Nothing to copy'); if (!userGesture && typeof GM_setClipboard !== 'undefined') { GM_setClipboard(md); return 'GM_setClipboard'; } if (userGesture && hasFocus() && navigator.clipboard?.writeText) { focusAnyEditor(); await navigator.clipboard.writeText(md); return 'native API'; } if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(md); return 'GM_setClipboard'; } throw new Error('No clipboard method available'); } // Big center overlay for "needs click" function showCopyOverlay(md) { closePanel(); const wrap = document.createElement('div'); wrap.className = 'cpmd-overlay'; wrap.innerHTML = ` <div class="cpmd-card" role="dialog" aria-modal="true"> <h2>Click to copy Markdown</h2> <p>Your browser blocked clipboard access. One click will finish copying your CodePen.</p> <div> <button class="cpmd-cta" id="cpmd-do-copy" type="button"><span>Copy to clipboard</span></button> <button class="cpmd-cta cpmd-ghost" id="cpmd-cancel" type="button"><span>Cancel</span></button> </div> </div>`; function cleanup() { document.removeEventListener('keydown', onKey, true); wrap.remove(); } function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); cleanup(); } } wrap.addEventListener( 'pointerdown', (e) => { if (e.target === wrap) cleanup(); }, true ); document.addEventListener('keydown', onKey, true); wrap.querySelector('#cpmd-cancel').onclick = cleanup; wrap.querySelector('#cpmd-do-copy').onclick = async () => { try { if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(md); else if (typeof GM_setClipboard !== 'undefined') GM_setClipboard(md); toast('Successfully copied Markdown to clipboard!', { type: 'success', }); } catch (e) { console.error(e); toast('Failed to copy. Check console for details.', { type: 'error', }); } finally { cleanup(); } }; document.body.appendChild(wrap); wrap.querySelector('#cpmd-do-copy').focus(); } // ---------- Panel control helpers (parent only) ---------- function isPanelOpen() { const panel = document.getElementById('cpmd-panel'); return !!panel && panel.classList.contains('show'); } function closePanel() { const panel = document.getElementById('cpmd-panel'); if (panel?.classList.contains('show')) { panel.classList.remove('show'); document .getElementById('cpmd-btn-gear') ?.setAttribute('aria-expanded', 'false'); } } // ---------- Parent UI & flow ---------- function setBusy(b) { const btn = document.getElementById('cpmd-btn-main'); if (!btn) return; btn.disabled = !!b; const span = btn.querySelector('span'); if (span) span.textContent = b ? 'Copying…' : 'Copy CodePen as Markdown'; btn.classList.toggle('is-busy', !!b); } async function extractAndCopy({ userGesture = false } = {}) { closePanel(); setBusy(true); const { md } = await generateMarkdownForPage(); if (!md.trim()) { toast('No content to copy.', { type: 'warn' }); notifyDesktop({ text: 'No content found.' }); setBusy(false); return; } try { const method = await copyMarkdown(md, { userGesture }); toast('Copied Markdown.', { type: 'success' }); notifyDesktop({ text: `Copied via ${method}.` }); } catch { showCopyOverlay(md); } finally { setBusy(false); } } // ----- Options panel (parent only) ----- function buildOptionsPanel() { if (!IS_PARENT) return; if (document.getElementById('cpmd-panel')) return; const panel = document.createElement('div'); panel.id = 'cpmd-panel'; panel.className = 'cpmd-panel'; const combo = getShortcutCombo(); const shortcutEnabled = isTrue(PREF.shortcutEnabled); const isMac = /Mac|iPhone|iPad/.test(navigator.platform || ''); panel.innerHTML = ` <h3>Options</h3> <div class="cpmd-panel-body"> <label class="cpmd-opt"> <input type="checkbox" id="cpmd-opt-processed"> <div class="cpmd-opt-label"> <div class="cpmd-opt-title">Copy compiled code</div> <div class="cpmd-opt-desc">Use processed output (e.g. SCSS→CSS, TypeScript→JS)</div> </div> </label> <label class="cpmd-opt"> <input type="checkbox" id="cpmd-opt-header" checked> <div class="cpmd-opt-label"> <div class="cpmd-opt-title">Add attribution header</div> <div class="cpmd-opt-desc">Include pen title, author name, and link to original</div> </div> </label> <div class="cpmd-separator"></div> <div class="cpmd-shortcut-wrapper"> <div class="cpmd-shortcut-row ${shortcutEnabled ? '' : 'disabled'}"> <input type="checkbox" id="cpmd-opt-shortcut-enabled" ${ shortcutEnabled ? 'checked' : '' }> <div class="cpmd-shortcut-info"> <label class="cpmd-shortcut-label" for="cpmd-opt-shortcut-enabled">Keyboard shortcut</label> <span class="cpmd-shortcut-badge" id="cpmd-shortcut-badge">${formatShortcutDisplay( combo )}</span> </div> <button type="button" class="cpmd-edit-btn" id="cpmd-edit-shortcut"> <span id="cpmd-edit-text">Edit</span> </button> </div> <div class="cpmd-shortcut-expand" id="cpmd-shortcut-expand"> <div class="cpmd-shortcut-config"> <div class="cpmd-modifier-row"> <label class="cpmd-mod-key ${ combo.ctrl ? 'active' : '' }" id="mod-ctrl"> <input type="checkbox" id="cpmd-key-ctrl" ${ combo.ctrl ? 'checked' : '' }> <span>Ctrl</span> </label> <label class="cpmd-mod-key ${ combo.alt ? 'active' : '' }" id="mod-alt"> <input type="checkbox" id="cpmd-key-alt" ${ combo.alt ? 'checked' : '' }> <span>Alt</span> </label> <label class="cpmd-mod-key ${ combo.shift ? 'active' : '' }" id="mod-shift"> <input type="checkbox" id="cpmd-key-shift" ${ combo.shift ? 'checked' : '' }> <span>Shift</span> </label> <label class="cpmd-mod-key ${ combo.meta ? 'active' : '' }" id="mod-meta"> <input type="checkbox" id="cpmd-key-meta" ${ combo.meta ? 'checked' : '' }> <span>${isMac ? '⌘' : '⊞'}</span> </label> </div> <div class="cpmd-key-section"> <button type="button" class="cpmd-key-capture" id="cpmd-key-capture"> <span class="cpmd-key-capture-label">Press to set key</span> <span class="cpmd-key-value" id="cpmd-key-label" data-code="${ combo.code || '' }">${(combo.key || 'X').toUpperCase()}</span> </button> </div> <div class="cpmd-shortcut-footer"> <button type="button" class="cpmd-reset-btn" id="cpmd-reset-shortcut">Reset</button> <button type="button" class="cpmd-done-btn" id="cpmd-done-editing">Done</button> </div> </div> </div> </div> </div> `; document.body.appendChild(panel); let editingShortcut = false; const syncPanel = () => { panel.querySelector('#cpmd-opt-processed').checked = isTrue( PREF.processed ); panel.querySelector('#cpmd-opt-header').checked = isTrue( PREF.includeHeader ); panel.querySelector('#cpmd-opt-shortcut-enabled').checked = isTrue( PREF.shortcutEnabled ); const combo = getShortcutCombo(); panel.querySelector('#cpmd-key-ctrl').checked = combo.ctrl; panel.querySelector('#cpmd-key-alt').checked = combo.alt; panel.querySelector('#cpmd-key-shift').checked = combo.shift; panel.querySelector('#cpmd-key-meta').checked = combo.meta; // Update active states panel .querySelector('#mod-ctrl') .classList.toggle('active', combo.ctrl); panel .querySelector('#mod-alt') .classList.toggle('active', combo.alt); panel .querySelector('#mod-shift') .classList.toggle('active', combo.shift); panel .querySelector('#mod-meta') .classList.toggle('active', combo.meta); const keyLabel = panel.querySelector('#cpmd-key-label'); keyLabel.textContent = (combo.key || 'X').toUpperCase(); keyLabel.dataset.code = combo.code || ''; const enabled = isTrue(PREF.shortcutEnabled); panel .querySelector('.cpmd-shortcut-row') .classList.toggle('disabled', !enabled); panel.querySelector('#cpmd-shortcut-badge').textContent = formatShortcutDisplay(combo); }; syncPanel(); // Toggle expand/collapse const toggleShortcutEdit = (show) => { editingShortcut = show; const expandEl = panel.querySelector('#cpmd-shortcut-expand'); const editBtn = panel.querySelector('#cpmd-edit-shortcut'); const editText = panel.querySelector('#cpmd-edit-text'); expandEl.classList.toggle('show', show); editBtn.classList.toggle('is-editing', show); editText.textContent = show ? 'Close' : 'Edit'; }; // Edit button click panel .querySelector('#cpmd-edit-shortcut') .addEventListener('click', () => { if (!isTrue(PREF.shortcutEnabled)) return; toggleShortcutEdit(!editingShortcut); }); // Done button panel .querySelector('#cpmd-done-editing') .addEventListener('click', () => { toggleShortcutEdit(false); }); // Option change handlers panel .querySelector('#cpmd-opt-processed') .addEventListener('change', () => { togglePref(PREF.processed, { toastLabel: 'Compiled code' }); }); panel .querySelector('#cpmd-opt-header') .addEventListener('change', () => { togglePref(PREF.includeHeader, { toastLabel: 'Attribution header', }); }); // Shortcut enabled toggle panel .querySelector('#cpmd-opt-shortcut-enabled') .addEventListener('change', (e) => { const enabled = e.target.checked; setPref(PREF.shortcutEnabled, enabled ? '1' : '0'); panel .querySelector('.cpmd-shortcut-row') .classList.toggle('disabled', !enabled); if (!enabled) { toggleShortcutEdit(false); } if (window.cpmdCleanupShortcut) { window.cpmdCleanupShortcut(); window.cpmdCleanupShortcut = null; } if (enabled) { window.cpmdCleanupShortcut = bindShortcut(() => extractAndCopy({ userGesture: true }) ); } broadcastPrefs(); toast( `Keyboard shortcut ${enabled ? 'enabled' : 'disabled'}.`, { type: 'info' } ); }); // --- Key capture + update --- function labelForKey(ev) { if (/^F\d{1,2}$/.test(ev.key)) return ev.key.toUpperCase(); if (ev.key === ' ') return 'SPACE'; if (ev.key.length === 1) return ev.key.toUpperCase(); return (ev.code || ev.key || '').toUpperCase().replace(/^KEY/, ''); } function readComboFromUI() { const ctrl = panel.querySelector('#cpmd-key-ctrl').checked; const alt = panel.querySelector('#cpmd-key-alt').checked; const shift = panel.querySelector('#cpmd-key-shift').checked; const meta = panel.querySelector('#cpmd-key-meta').checked; const keyEl = panel.querySelector('#cpmd-key-label'); const key = (keyEl.textContent || 'X').toUpperCase(); const code = keyEl.dataset.code || (/^[A-Z]$/.test(key) ? 'Key' + key : /^\d$/.test(key) ? 'Digit' + key : key); return { ctrl, alt, shift, meta, key: key.toLowerCase(), code }; } const keyBtn = panel.querySelector('#cpmd-key-capture'); const keyLabel = panel.querySelector('#cpmd-key-label'); const captureLabel = panel.querySelector('.cpmd-key-capture-label'); let capturing = false; function stopCapture() { capturing = false; keyBtn.classList.remove('is-capturing'); captureLabel.textContent = 'Press to set key'; document.removeEventListener('keydown', onCapture, true); } function onCapture(e) { e.preventDefault(); e.stopPropagation(); if (['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) return; keyLabel.textContent = labelForKey(e); keyLabel.dataset.code = e.code || ''; stopCapture(); updateShortcut(); } keyBtn.addEventListener('click', () => { if (capturing) { stopCapture(); return; } capturing = true; keyBtn.classList.add('is-capturing'); captureLabel.textContent = 'Press any key...'; document.addEventListener('keydown', onCapture, true); }); const updateShortcut = () => { const combo = readComboFromUI(); if (!combo.ctrl && !combo.alt && !combo.shift && !combo.meta) { toast('At least one modifier key required!', { type: 'warn' }); return; } setPref(PREF.shortcutCombo, JSON.stringify(combo)); panel.querySelector('#cpmd-shortcut-badge').textContent = formatShortcutDisplay(combo); if (isTrue(PREF.shortcutEnabled)) { if (window.cpmdCleanupShortcut) window.cpmdCleanupShortcut(); window.cpmdCleanupShortcut = bindShortcut(() => extractAndCopy({ userGesture: true }) ); } broadcastPrefs(); }; // Wire up modifier checkboxes ['ctrl', 'alt', 'shift', 'meta'].forEach((mod) => { const checkbox = panel.querySelector(`#cpmd-key-${mod}`); const label = panel.querySelector(`#mod-${mod}`); checkbox.addEventListener('change', () => { label.classList.toggle('active', checkbox.checked); updateShortcut(); }); }); // Reset button panel .querySelector('#cpmd-reset-shortcut') .addEventListener('click', () => { setPref(PREF.shortcutCombo, JSON.stringify(DEFAULT_SHORTCUT)); syncPanel(); if (isTrue(PREF.shortcutEnabled)) { if (window.cpmdCleanupShortcut) window.cpmdCleanupShortcut(); window.cpmdCleanupShortcut = bindShortcut(() => extractAndCopy({ userGesture: true }) ); } broadcastPrefs(); toast('Shortcut reset to Alt+Shift+X', { type: 'info' }); }); // Listen for external changes window.addEventListener('cpmd:prefs', () => { syncPanel(); }); // Click-away close const onAway = (ev) => { if (!isPanelOpen()) return; const wrap = document.getElementById('cpmd-wrap'); if (panel.contains(ev.target) || wrap?.contains(ev.target)) return; closePanel(); }; document.addEventListener('pointerdown', onAway, true); document.addEventListener( 'keydown', (e) => { if (e.key === 'Escape') { if (editingShortcut) { e.preventDefault(); toggleShortcutEdit(false); } else if (isPanelOpen()) { e.preventDefault(); closePanel(); } } }, true ); } function addSplitButton() { if (!IS_PARENT) return; if (document.getElementById('cpmd-wrap')) return; const wrap = document.createElement('div'); wrap.id = 'cpmd-wrap'; wrap.className = 'cpmd-wrap'; const main = document.createElement('button'); main.id = 'cpmd-btn-main'; main.type = 'button'; main.className = 'cpmd-btn-main'; main.innerHTML = '<span>Copy CodePen as Markdown</span>'; main.addEventListener('click', () => extractAndCopy({ userGesture: true }) ); const gear = document.createElement('button'); gear.id = 'cpmd-btn-gear'; gear.type = 'button'; gear.className = 'cpmd-btn-gear'; gear.setAttribute('aria-label', 'CodePen.md options'); gear.setAttribute('aria-haspopup', 'dialog'); gear.setAttribute('aria-expanded', 'false'); gear.innerHTML = ` <svg class="cpmd-gear-ic" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" fill="#ffffff"><g id="bgCarrier" stroke-width="0"></g><g id="tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="iconCarrier"> <path d="M0 0h48v48H0z" fill="none"></path> <g id="Shopicon"> <path d="M8.706,37.027c2.363-0.585,4.798-1.243,6.545-1.243c0.683,0,1.261,0.101,1.688,0.345c1.474,0.845,2.318,4.268,3.245,7.502 C21.421,43.866,22.694,44,24,44c1.306,0,2.579-0.134,3.816-0.368c0.926-3.234,1.771-6.657,3.244-7.501 c0.427-0.245,1.005-0.345,1.688-0.345c1.747,0,4.183,0.658,6.545,1.243c1.605-1.848,2.865-3.99,3.706-6.333 c-2.344-2.406-4.872-4.891-4.872-6.694c0-1.804,2.528-4.288,4.872-6.694c-0.841-2.343-2.101-4.485-3.706-6.333 c-2.363,0.585-4.798,1.243-6.545,1.243c-0.683,0-1.261-0.101-1.688-0.345c-1.474-0.845-2.318-4.268-3.245-7.502 C26.579,4.134,25.306,4,24,4c-1.306,0-2.579,0.134-3.816,0.368c-0.926,3.234-1.771,6.657-3.245,7.501 c-0.427-0.245-1.005-0.345-1.688-0.345c-1.747,0-4.183,0.658-6.545,1.243C7.101,12.821,5.841,14.962,5,17.306 C7.344,19.712,9.872,22.196,9.872,24c0,1.804-2.527,4.288-4.872,6.694C5.841,33.037,7.101,35.179,8.706,37.027z M18,24 c0-3.314,2.686-6,6-6s6,2.686,6,6s-2.686,6-6,6S18,27.314,18,24z"></path> </g> </g></svg> `; gear.addEventListener('click', () => { buildOptionsPanel(); const panel = document.getElementById('cpmd-panel'); const isOpen = panel.classList.toggle('show'); if (isOpen) { panel.querySelector('#cpmd-opt-processed').checked = isTrue( PREF.processed ); panel.querySelector('#cpmd-opt-header').checked = isTrue( PREF.includeHeader ); } gear.setAttribute('aria-expanded', String(isOpen)); }); wrap.appendChild(main); wrap.appendChild(gear); document.body.appendChild(wrap); } // ---------- Pref sync (parent <-> preview) ---------- function currentPrefs() { return { processed: isTrue(PREF.processed), includeHeader: isTrue(PREF.includeHeader), shortcutEnabled: isTrue(PREF.shortcutEnabled), shortcutCombo: getShortcutCombo(), }; } function broadcastPrefs() { document .querySelectorAll('iframe[src*="//cdpn.io/"]') .forEach((ifr) => { try { ifr.contentWindow?.postMessage( { type: 'CPMD_PREFS_STATE', prefs: currentPrefs() }, ORIGIN_CHILD ); } catch {} }); } // ---------- Preview agent (cdpn.io) ---------- if (IS_PREVIEW) { // apply pushed prefs and (re)bind shortcut function applyPrefsInChild(p) { try { localStorage.setItem(PREF.processed, p.processed ? '1' : '0'); localStorage.setItem( PREF.includeHeader, p.includeHeader ? '1' : '0' ); localStorage.setItem( PREF.shortcutEnabled, p.shortcutEnabled ? '1' : '0' ); localStorage.setItem( PREF.shortcutCombo, JSON.stringify(p.shortcutCombo || DEFAULT_SHORTCUT) ); if (window.cpmdCleanupShortcut) { window.cpmdCleanupShortcut(); window.cpmdCleanupShortcut = null; } if (p.shortcutEnabled) { window.cpmdCleanupShortcut = bindShortcut(async () => { try { const md = await requestMarkdownFromParent(); if (!md) return; if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(md); else if (typeof GM_setClipboard !== 'undefined') GM_setClipboard(md); window.parent.postMessage( { type: 'CPMD_NOTIFY', level: 'success', msg: 'Copied from Preview.', }, ORIGIN_PARENT ); } catch (e) { window.parent.postMessage( { type: 'CPMD_NOTIFY', level: 'error', msg: 'Preview copy failed.', }, ORIGIN_PARENT ); } }); } } catch {} } // ask parent for prefs at startup window.parent.postMessage( { type: 'CPMD_PREFS_REQUEST' }, ORIGIN_PARENT ); // listen for pushes window.addEventListener( 'message', (ev) => { const d = ev.data || {}; if ( ev.origin === ORIGIN_PARENT && d.type === 'CPMD_PREFS_STATE' ) applyPrefsInChild(d.prefs || {}); }, true ); if (isTrue(PREF.shortcutEnabled)) { // bind with whatever's currently in localStorage until parent replies window.cpmdCleanupShortcut = bindShortcut(async () => { try { const md = await requestMarkdownFromParent(); if (!md) return; if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(md); else if (typeof GM_setClipboard !== 'undefined') GM_setClipboard(md); window.parent.postMessage( { type: 'CPMD_NOTIFY', level: 'success', msg: 'Copied from Preview.', }, ORIGIN_PARENT ); } catch (e) { window.parent.postMessage( { type: 'CPMD_NOTIFY', level: 'error', msg: 'Preview copy failed.', }, ORIGIN_PARENT ); console.error('[CodePen.md] Preview copy failed', e); } }); } // Close panel from preview clicks/Esc let _lastSignal = 0; function signalParent(t) { const now = Date.now(); if (now - _lastSignal < 120) return; _lastSignal = now; window.parent.postMessage({ type: t }, ORIGIN_PARENT); } window.addEventListener( 'pointerdown', (e) => { if (e.isTrusted && e.button === 0) signalParent('CPMD_PREVIEW_POINTER'); }, true ); window.addEventListener( 'keydown', (e) => { if (e.key === 'Escape') signalParent('CPMD_PREVIEW_ESC'); }, true ); async function requestMarkdownFromParent() { const id = Math.random().toString(36).slice(2); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { window.removeEventListener('message', onMsg, true); reject(new Error('Timed out waiting for Markdown')); }, 7000); function onMsg(ev) { if (ev.origin !== ORIGIN_PARENT) return; const d = ev.data || {}; if (d.type === 'CPMD_COPY_PAYLOAD' && d.id === id) { clearTimeout(timeout); window.removeEventListener('message', onMsg, true); resolve(d.md || ''); } } window.addEventListener('message', onMsg, true); window.parent.postMessage( { type: 'CPMD_COPY_REQUEST', id }, ORIGIN_PARENT ); }); } return; // agent stops here } // ---------- Parent: message bridge for preview agent ---------- if (IS_PARENT) { window.addEventListener('message', async (ev) => { const d = ev.data || {}; if (ev.origin === ORIGIN_CHILD && d.type === 'CPMD_COPY_REQUEST') { try { const { md } = await generateMarkdownForPage(); ev.source?.postMessage( { type: 'CPMD_COPY_PAYLOAD', id: d.id, md }, ORIGIN_CHILD ); } catch { ev.source?.postMessage( { type: 'CPMD_COPY_PAYLOAD', id: d.id, md: '' }, ORIGIN_CHILD ); } } else if (ev.origin === ORIGIN_CHILD && d.type === 'CPMD_NOTIFY') { toast(d.msg || '', { type: d.level === 'success' ? 'success' : d.level === 'warn' ? 'warn' : 'error', }); } else if ( ev.origin === ORIGIN_CHILD && (d.type === 'CPMD_PREVIEW_POINTER' || d.type === 'CPMD_PREVIEW_ESC') ) { closePanel(); } else if ( ev.origin === ORIGIN_CHILD && d.type === 'CPMD_PREFS_REQUEST' ) { ev.source?.postMessage( { type: 'CPMD_PREFS_STATE', prefs: currentPrefs() }, ORIGIN_CHILD ); } }); // Menu (register once; labels auto-refresh when supported) refreshMenu({ force: true }); // UI + shortcut if (isTrue(PREF.shortcutEnabled)) { window.cpmdCleanupShortcut = bindShortcut(() => extractAndCopy({ userGesture: true }) ); } onReady(() => { setTimeout(addSplitButton, 2000); setTimeout(broadcastPrefs, 2500); // let preview load, then push prefs const params = new URLSearchParams(location.search); if (params.get('copy') === '1') setTimeout(() => extractAndCopy({ userGesture: false }), 3000); }); } })();