// ==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);
});
}
})();