Files

987 lines
71 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="description" content="clip2md — paste a Reddit thread, an article, any rich text and save it as clean Markdown in one click. Client-side, private, no upload." />
<meta name="theme-color" content="#EAEAE3" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="clip2md" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<title>clip2md — Paste to Markdown</title>
<script>
try { var lc = JSON.parse(localStorage.getItem('clip2md:langChoice')); var nl = (navigator.languages && navigator.languages[0]) || navigator.language || 'en'; document.documentElement.lang = (lc === 'de' || lc === 'en') ? lc : (/^de/i.test(nl) ? 'de' : 'en'); } catch (e) {}
try { var th = JSON.parse(localStorage.getItem('clip2md:theme')); if (th === 'dark' || (th === null && matchMedia('(prefers-color-scheme: dark)').matches)) document.documentElement.setAttribute('data-theme','dark'); } catch (e) {}
</script>
<style>
@font-face { font-family: "Space Grotesk"; src: url("vendor/fonts/space-grotesk-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Space Grotesk"; src: url("vendor/fonts/space-grotesk-latin-500-normal.woff2") format("woff2"); font-weight: 500; font-display: swap; }
@font-face { font-family: "Space Grotesk"; src: url("vendor/fonts/space-grotesk-latin-700-normal.woff2") format("woff2"); font-weight: 700; font-display: swap; }
@font-face { font-family: "Space Mono"; src: url("vendor/fonts/space-mono-latin-400-normal.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Space Mono"; src: url("vendor/fonts/space-mono-latin-700-normal.woff2") format("woff2"); font-weight: 700; font-display: swap; }
/* Klarbild-Familie: warmes Papier, Karte, kräftiges Blau, Passermarke, scharfe Ecken */
:root {
--paper: oklch(92.8% 0.005 95); /* #EAEAE3 */
--card: oklch(98.4% 0.004 95); /* #FBFBF7 */
--tint: oklch(95.6% 0.005 95);
--line: oklch(86.8% 0.007 95); /* #D7D6CC */
--line-2: oklch(78% 0.008 95);
--mark: oklch(70% 0.006 95);
--ink: oklch(21% 0.01 95); /* #16150F */
--ink-soft: oklch(49% 0.008 95); /* #6A685D */
--accent: oklch(45% 0.23 267); /* #1B3BE0 */
--accent-2: oklch(38% 0.21 267);
--accent-bg: oklch(45% 0.23 267 / 0.07);
--on-accent: #fff;
--ok: oklch(60% 0.15 150);
--err: oklch(55% 0.19 25);
--shadow: 0 1px 2px oklch(21% 0.01 95 / 0.05);
--shadow-lg: 0 1px 2px oklch(21% 0.01 95 / 0.06), 0 12px 32px oklch(21% 0.01 95 / 0.10);
--radius: 4px; --radius-sm: 3px;
--sans: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: "Space Mono", ui-monospace, "SF Mono", Menlo, monospace;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--gut: 22px;
}
[data-theme="dark"] {
--paper: oklch(17.5% 0.01 267);
--card: oklch(21% 0.012 267);
--tint: oklch(24% 0.014 267);
--line: oklch(31% 0.015 267);
--line-2: oklch(42% 0.018 267);
--mark: oklch(52% 0.012 267);
--ink: oklch(94% 0.005 95);
--ink-soft: oklch(72% 0.01 95);
--accent: oklch(70% 0.15 267);
--accent-2: oklch(79% 0.12 267);
--accent-bg: oklch(70% 0.15 267 / 0.14);
--on-accent: oklch(17% 0.03 267);
--ok: oklch(74% 0.14 150);
--err: oklch(70% 0.16 25);
--shadow: 0 1px 2px oklch(0% 0 0 / 0.3);
--shadow-lg: 0 1px 2px oklch(0% 0 0 / 0.3), 0 12px 32px oklch(0% 0 0 / 0.45);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; min-height: 100dvh; display: flex; flex-direction: column;
background: var(--paper); color: var(--ink);
font-family: var(--sans); font-size: 15px; line-height: 1.5;
-webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent;
padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right);
}
a { color: var(--accent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button { font: inherit; color: inherit; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.mono-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-soft); }
svg.i { width: 18px; height: 18px; flex: none; }
/* ---------- Top bar ---------- */
.topbar {
position: sticky; top: 0; z-index: 30;
display: flex; align-items: center; gap: 12px;
padding: calc(10px + env(safe-area-inset-top)) var(--gut) 10px;
background: color-mix(in oklab, var(--card) 88%, transparent);
backdrop-filter: saturate(1.2) blur(8px); -webkit-backdrop-filter: saturate(1.2) blur(8px);
border-bottom: 1px solid var(--line);
}
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); text-decoration: none;
font-weight: 700; font-size: 1.2rem; letter-spacing: -0.02em; }
.brand svg { width: 22px; height: 22px; color: var(--accent); }
.brand .two { color: var(--accent); }
.tagline { margin-left: 6px; }
.topbar .spacer { flex: 1; }
.tools { display: flex; align-items: center; gap: 6px; }
.pill {
min-height: 36px; padding: 0 14px; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
background: transparent; color: var(--ink-soft); border: 1px solid var(--line); border-radius: 999px;
font-size: 0.88rem; font-weight: 500; text-decoration: none; cursor: pointer; white-space: nowrap;
transition: background .15s, color .15s, border-color .15s, transform .06s;
}
.pill:hover { color: var(--ink); background: var(--tint); }
.pill:active { transform: scale(0.96); }
.pill.icon { width: 36px; padding: 0; }
.pill[aria-pressed="true"] { color: var(--accent); background: var(--accent-bg); border-color: transparent; font-weight: 700; }
.pill.lang { font-family: var(--mono); font-size: 0.72rem; letter-spacing: .06em; padding: 0 11px; }
.pill.donate { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); }
.pill.donate:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.pill.donate svg { width: 16px; height: 16px; }
.mini { min-height: 30px; padding: 0 11px; display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px;
background: transparent; color: var(--ink-soft); font-size: 0.8rem; font-weight: 500; cursor: pointer; white-space: nowrap; transition: color .15s, background .15s, border-color .15s; }
.mini:hover { color: var(--ink); background: var(--tint); }
.mini.accent { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 40%, var(--line)); }
.mini svg { width: 14px; height: 14px; }
.mini[hidden] { display: none; }
.btn.nudge { animation: nudge 1.4s var(--ease) 3; }
@keyframes nudge { 0%,100% { box-shadow: 0 0 0 0 var(--accent-bg); } 50% { box-shadow: 0 0 0 8px color-mix(in oklab, var(--accent) 22%, transparent); } }
/* ---------- Action bar ---------- */
.bar { display: flex; align-items: stretch; gap: 8px; padding: 16px var(--gut) 12px; flex-wrap: wrap; }
.fname {
flex: 1 1 280px; min-width: 0; display: flex; align-items: center; gap: 10px;
background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
padding: 0 8px 0 12px; min-height: 44px; box-shadow: var(--shadow);
transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.fname:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
.fname.flash { animation: flash .6s var(--ease); }
@keyframes flash { 0% { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); } 100% { border-color: var(--line); box-shadow: var(--shadow); } }
.fname input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink);
font-family: var(--mono); font-size: 14px; padding: 10px 0; }
.fname .ext { font-family: var(--mono); font-size: 14px; color: var(--ink-soft); }
.fname .tag { font-family: var(--mono); font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-soft); border: 1px solid var(--line); border-radius: 999px; padding: 3px 8px; }
.fname .tag.auto { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 35%, var(--line)); }
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn {
min-height: 44px; padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 999px;
font-size: 0.95rem; font-weight: 500; cursor: pointer; white-space: nowrap; box-shadow: var(--shadow);
transition: background .15s, border-color .15s, color .15s, transform .06s;
}
.btn:hover { border-color: var(--line-2); background: var(--tint); }
.btn:active { transform: scale(0.97); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 700; }
.btn.primary:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn.ghost { box-shadow: none; background: transparent; color: var(--ink-soft); }
.btn.ghost:hover { color: var(--err); border-color: color-mix(in oklab, var(--err) 40%, var(--line)); background: transparent; }
/* ---------- Workspace ---------- */
main { flex: 1; display: grid; grid-template-columns: 1fr; gap: 12px; padding: 0 var(--gut) 12px; min-height: 0; }
main.split { grid-template-columns: 1fr 1fr; }
.pane { position: relative; display: flex; flex-direction: column; min-height: 360px;
background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow);
transition: border-color .2s var(--ease), box-shadow .2s var(--ease); }
.pane-head { display: flex; align-items: center; gap: 8px; padding: 9px 14px; border-bottom: 1px solid var(--line); }
.pane-head .regmark { width: 11px; height: 11px; position: relative; flex: none; }
.pane-head .regmark::before, .pane-head .regmark::after { content: ''; position: absolute; background: var(--mark); }
.pane-head .regmark::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); }
.pane-head .regmark::after { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); }
.pane-head .spacer { flex: 1; }
.pane-head .badge { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
main:not(.split) .preview-pane { display: none; }
.editor-pane:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
main.drag .editor-pane { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
#editor { flex: 1; width: 100%; min-height: 0; resize: none; border: 0; outline: 0; background: transparent; color: var(--ink);
padding: 18px 20px 24px; font-family: var(--mono); font-size: 14px; line-height: 1.7; tab-size: 2; border-radius: 0 0 var(--radius) var(--radius); }
.badge { display: inline-flex; align-items: center; gap: 7px; opacity: 0; transition: opacity .2s var(--ease); }
.badge.show { opacity: 1; }
.badge .led { width: 7px; height: 7px; border-radius: 50%; background: var(--mark); }
.badge.ok .led { background: var(--ok); }
.badge.pulse .led { animation: pulse .7s var(--ease); }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--ok) 45%, transparent); } 100% { box-shadow: 0 0 0 10px transparent; } }
/* Empty state */
.empty { position: absolute; inset: 42px 0 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px; padding: 24px; text-align: center; pointer-events: none; transition: opacity .2s var(--ease); }
main.has-content .empty { opacity: 0; visibility: hidden; }
.empty .mark { width: 44px; height: 44px; color: var(--accent); }
.empty h2 { margin: 0; font-size: clamp(1.4rem, 1.1rem + 1vw, 1.9rem); font-weight: 700; letter-spacing: -0.02em; line-height: 1.15; }
.empty p { margin: 0; max-width: 46ch; color: var(--ink-soft); font-size: 0.95rem; }
.empty .btn { pointer-events: auto; min-height: 48px; padding: 0 22px; font-size: 1rem; }
.empty .keys { color: var(--ink-soft); }
kbd { font-family: var(--mono); font-size: 11px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: var(--radius-sm); padding: 1px 5px; background: var(--tint); color: var(--ink); }
/* Preview */
.preview { flex: 1; overflow: auto; padding: 18px 22px 26px; font-size: 15px; line-height: 1.65; }
.preview > :first-child { margin-top: 0; }
.preview .fm { margin: 0 0 1.2em; padding-bottom: 12px; border-bottom: 1px dashed var(--line); font-family: var(--mono); font-size: 12px; color: var(--ink-soft); display: grid; gap: 2px; }
.preview .fm b { color: var(--ink); font-weight: 400; }
.preview h1, .preview h2, .preview h3 { font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; margin: 1.3em 0 .45em; }
.preview h1 { font-size: 1.75em; } .preview h2 { font-size: 1.35em; } .preview h3 { font-size: 1.12em; }
.preview p, .preview ul, .preview ol, .preview blockquote, .preview pre, .preview table { margin: 0 0 .9em; }
.preview a { color: var(--accent); text-underline-offset: 2px; overflow-wrap: anywhere; }
.preview code { font-family: var(--mono); font-size: .86em; background: var(--tint); padding: 1px 4px; border-radius: var(--radius-sm); }
.preview pre { background: var(--tint); border: 1px solid var(--line); padding: 12px 14px; border-radius: var(--radius); overflow: auto; }
.preview pre code { background: none; padding: 0; }
.preview blockquote { border-left: 2px solid var(--accent); padding-left: 14px; color: var(--ink-soft); }
.preview img { max-width: 100%; height: auto; }
.preview table { border-collapse: collapse; display: block; overflow-x: auto; font-size: .92em; }
.preview th, .preview td { border: 1px solid var(--line); padding: 6px 10px; text-align: left; }
.preview hr { border: 0; border-top: 1px solid var(--line); margin: 1.4em 0; }
/* ---------- Footer ---------- */
footer { display: flex; align-items: center; gap: 8px 18px; flex-wrap: wrap; padding: 6px var(--gut) calc(18px + env(safe-area-inset-bottom)); color: var(--ink-soft); font-size: 0.85rem; }
footer .spacer { flex: 1; }
footer nav { display: inline-flex; gap: 4px 14px; flex-wrap: wrap; align-items: center; }
footer nav a { color: var(--ink-soft); text-decoration: none; padding: 6px 0; }
footer nav a:hover { color: var(--accent); }
footer nav a.coffee { color: var(--accent); font-weight: 500; }
footer nav button.linkish { background: none; border: 0; padding: 6px 0; color: var(--ink-soft); cursor: pointer; font-size: inherit; }
footer nav button.linkish:hover { color: var(--accent); }
[data-donate][hidden] { display: none !important; }
/* History + help sheets reuse .pop */
.pop.wide { width: 420px; }
.hist { list-style: none; margin: 0; padding: 0 6px 6px; }
.hist li { display: flex; align-items: center; gap: 8px; border-radius: var(--radius-sm); }
.hist li:hover { background: var(--tint); }
.hist .load { flex: 1; min-width: 0; text-align: left; background: none; border: 0; padding: 10px 8px; cursor: pointer; min-height: 48px; }
.hist .load b { display: block; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hist .load span { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); }
.hist .del { width: 40px; height: 40px; border: 0; background: none; color: var(--ink-soft); cursor: pointer; border-radius: 999px; }
.hist .del:hover { color: var(--err); }
.pop .empty-note { padding: 6px 12px 14px; color: var(--ink-soft); font-size: 0.9rem; margin: 0; }
.pop .foot { display: flex; justify-content: space-between; gap: 8px; padding: 6px 12px 10px; }
.help { padding: 0 12px 12px; font-size: 0.93rem; }
.help ol { padding-left: 1.2rem; margin: 6px 0 10px; }
.help li { margin-bottom: 6px; }
.help code { font-family: var(--mono); font-size: 0.82em; background: var(--tint); padding: 1px 4px; border-radius: 3px; overflow-wrap: anywhere; }
.help p { margin: 6px 0; color: var(--ink-soft); }
.sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 6px 0 12px; }
.sheet-head h2 { font-size: 1.05rem; margin: 6px 0; }
/* ---------- Settings sheet ---------- */
.scrim { position: fixed; inset: 0; z-index: 45; background: oklch(21% 0.01 95 / 0.25); opacity: 0; pointer-events: none; transition: opacity .2s var(--ease); }
.scrim.open { opacity: 1; pointer-events: auto; }
.pop { position: fixed; z-index: 50; top: calc(62px + env(safe-area-inset-top)); right: var(--gut); width: 340px; max-width: calc(100vw - 32px);
max-height: calc(100dvh - 90px); overflow: auto;
background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 6px;
display: none; }
.pop.open { display: block; animation: pop .18s var(--ease); }
@keyframes pop { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.pop h3 { margin: 0; padding: 12px 12px 6px; }
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; min-height: 48px; border-radius: var(--radius-sm); }
.row:hover { background: var(--tint); }
.row label { cursor: pointer; font-size: 0.95rem; }
.row .hint { display: block; font-size: 0.8rem; color: var(--ink-soft); }
.field { padding: 4px 12px 10px; }
.field.two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.field input, .field select { width: 100%; min-height: 40px; padding: 0 10px; background: var(--paper); color: var(--ink);
border: 1px solid var(--line); border-radius: var(--radius-sm); font-family: var(--mono); font-size: 13px; outline: 0; }
.field input:focus, .field select:focus { border-color: var(--accent); }
.sep { height: 1px; background: var(--line); margin: 4px 8px; }
.sw { position: relative; width: 42px; height: 24px; flex: none; }
.sw input { position: absolute; inset: 0; z-index: 2; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.sw .track { position: absolute; inset: 0; background: var(--line-2); border-radius: 999px; transition: background .15s; pointer-events: none; }
.sw .knob { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: var(--card); box-shadow: 0 1px 2px oklch(0% 0 0 / .25); transition: transform .18s var(--ease); pointer-events: none; }
.sw input:checked ~ .track { background: var(--accent); }
.sw input:checked ~ .knob { transform: translateX(18px); }
.sw input:focus-visible ~ .track { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ---------- Toast ---------- */
.toast { position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom)); z-index: 60; max-width: calc(100vw - 32px);
transform: translate(-50%, 14px); opacity: 0; pointer-events: none;
background: var(--ink); color: var(--paper); padding: 11px 18px; border-radius: 999px; font-size: 0.9rem; box-shadow: var(--shadow-lg);
transition: opacity .2s, transform .2s var(--ease); }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.err { background: var(--err); color: #fff; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (max-width: 1000px) {
.tagline { display: none; }
main.split { grid-template-columns: 1fr; }
main.split .editor-pane { display: none; }
}
@media (max-width: 767px) {
:root { --gut: 14px; }
.topbar { gap: 8px; }
.pill .lbl { display: none; }
.pill { min-height: 40px; }
.pill.icon, .pill.donate, #previewBtn { width: 40px; padding: 0; }
.bar { padding-top: 12px; }
.fname { flex-basis: 100%; }
.actions { width: 100%; display: grid; grid-template-columns: 1.5fr repeat(4, 1fr); gap: 6px; }
.actions .btn { flex-direction: column; gap: 2px; padding: 4px 2px; min-height: 54px; border-radius: 14px; font-size: 0.72rem; }
.actions .btn.primary { flex-direction: row; gap: 7px; font-size: 0.95rem; }
footer nav a { padding: 12px 0; }
main { padding-bottom: 8px; }
.pane { min-height: calc(100dvh - 250px); }
#editor, .fname input, .fname .ext, .field input, .field select { font-size: 16px; }
.pop { left: 0; right: 0; top: auto; bottom: 0; width: auto; max-width: none; max-height: 80dvh;
border-radius: 14px 14px 0 0; padding: 6px 6px calc(12px + env(safe-area-inset-bottom)); }
.pop.open { animation: sheet .24s var(--ease); }
@keyframes sheet { from { transform: translateY(40px); opacity: 0; } to { transform: none; opacity: 1; } }
footer { font-size: 0.8rem; }
.pop.wide { width: auto; }
.pane-head { flex-wrap: wrap; row-gap: 6px; }
#histBtn span, #obsBtn span { display: none; }
#histBtn, #obsBtn { width: 34px; padding: 0; justify-content: center; }
.mini { min-height: 34px; }
}
@media (max-width: 380px) {
.brand { font-size: 1.05rem; }
.pill.lang { padding: 0 8px; }
}
@media (hover: none) { .keys { display: none !important; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; } }
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="logo" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="17" rx="1.5"/><path d="M12 9.5v7.5M8.8 13.8 12 17l3.2-3.2"/></g><rect x="8.6" y="2.4" width="6.8" height="3.8" rx="1" fill="currentColor"/></symbol>
</svg>
<header class="topbar">
<a class="brand" href="/" aria-label="clip2md">
<svg aria-hidden="true"><use href="#logo"/></svg><span>clip<span class="two">2</span>md</span>
</a>
<span class="mono-label tagline" data-i18n="tagline">Paste → Markdown</span>
<span class="spacer"></span>
<div class="tools">
<button id="previewBtn" class="pill" aria-pressed="false" data-i18n-title="t_preview" title="Toggle preview">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
<span class="lbl" data-i18n="preview">Preview</span>
</button>
<button id="settingsBtn" class="pill icon" aria-haspopup="dialog" aria-expanded="false" data-i18n-title="t_settings" title="Settings">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h9M17 8h3M4 16h3M11 16h9"/><circle cx="15" cy="8" r="2.2"/><circle cx="9" cy="16" r="2.2"/></svg>
</button>
<button id="themeBtn" class="pill icon" data-i18n-title="t_theme" title="Toggle theme">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9Z"/></svg>
</button>
<button id="langBtn" class="pill lang" title="Deutsch">DE</button>
<a class="pill donate" id="donateTop" data-donate hidden href="#" target="_blank" rel="noopener nofollow" data-i18n-title="t_donate" title="Support clip2md">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20s-7.5-4.6-7.5-10.2A4.3 4.3 0 0 1 12 7.2a4.3 4.3 0 0 1 7.5 2.6C19.5 15.4 12 20 12 20Z"/></svg>
<span class="lbl" data-i18n="donate">Donate</span>
</a>
</div>
</header>
<div class="bar">
<label class="fname" id="fnameBox">
<span class="mono-label" data-i18n="file">File</span>
<input id="filename" type="text" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off" enterkeyhint="done" aria-label="Filename" />
<span class="ext">.md</span>
<span class="tag auto" id="fnameTag">auto</span>
</label>
<div class="actions">
<button id="pasteBtn" class="btn primary">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="8" height="4" rx="1"/><path d="M16 5h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2"/></svg>
<span class="lbl" data-i18n="paste">Paste</span>
</button>
<button id="save" class="btn">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>
<span class="lbl" data-i18n="save">Save</span>
</button>
<button id="copy" class="btn">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="1.5"/><path d="M5 15V5a1 1 0 0 1 1-1h10"/></svg>
<span class="lbl" data-i18n="copy">Copy</span>
</button>
<button id="openBtn" class="btn">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/></svg>
<span class="lbl" data-i18n="open">Open</span>
</button>
<button id="clear" class="btn ghost" data-i18n-title="clear" title="Clear">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14M10 7V5h4v2M7 7l1 13h8l1-13"/></svg>
<span class="lbl" data-i18n="clear">Clear</span>
</button>
<input id="fileInput" type="file" accept=".md,.markdown,.txt,.html,.htm,text/markdown,text/plain,text/html" hidden />
</div>
</div>
<main id="main">
<section class="pane editor-pane">
<div class="pane-head">
<span class="regmark" aria-hidden="true"></span><span class="mono-label">Markdown</span>
<span class="spacer"></span>
<span class="badge mono-label" id="srcBadge" aria-live="polite"><span class="led"></span><span id="srcText"></span></span>
<button class="mini" id="rawBtn" type="button" hidden data-i18n="raw_again">Undo clean-up</button>
<button class="mini" id="histBtn" type="button" hidden data-i18n-title="history" title="History">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.5-6"/><path d="M3.5 4.5V9H8"/><path d="M12 7.5V12l3 2"/></svg>
<span data-i18n="history">History</span>
</button>
<button class="mini accent" id="obsBtn" type="button" hidden data-i18n-title="t_obs" title="Open in Obsidian">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.8 19 7v10l-7 4.2L5 17V7Z"/><path d="M12 2.8v18.4"/></svg>
<span>Obsidian</span>
</button>
</div>
<textarea id="editor" spellcheck="false" autocapitalize="off" autocorrect="off" aria-label="Markdown editor"></textarea>
<div class="empty" id="empty">
<svg class="mark" aria-hidden="true"><use href="#logo"/></svg>
<h2 data-i18n="empty_h">Paste to start</h2>
<p data-i18n="empty_p">A long Reddit thread, an article, any formatted text. Rich HTML becomes clean Markdown, plain text stays as it is.</p>
<button class="btn primary" id="pasteBig" type="button">
<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="3" width="8" height="4" rx="1"/><path d="M16 5h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2"/></svg>
<span data-i18n="paste_big">Paste from clipboard</span>
</button>
<div class="keys mono-label"><kbd>⌘V</kbd> <span data-i18n="k_paste">paste</span> · <kbd>⌘S</kbd> <span data-i18n="k_save">save</span> · <span data-i18n="k_drop">or drop a file</span></div>
</div>
</section>
<section class="pane preview-pane">
<div class="pane-head"><span class="regmark" aria-hidden="true"></span><span class="mono-label" data-i18n="preview">Preview</span></div>
<div class="preview" id="preview"></div>
</section>
</main>
<footer>
<span class="mono-label"><span class="num" id="words">0</span> <span data-i18n="words">words</span> · <span class="num" id="chars">0</span> <span data-i18n="chars">chars</span> · <span class="num" id="rt">0</span> <span data-i18n="minread">min</span></span>
<span data-i18n="privacy">Runs in your browser. Nothing is uploaded.</span>
<span class="spacer"></span>
<nav aria-label="Footer">
<a class="coffee" data-donate hidden href="#" target="_blank" rel="noopener nofollow" data-i18n="coffee">Buy me a coffee</a>
<button class="linkish" id="helpBtn" type="button" data-i18n="iphone">iPhone shortcut</button>
<a href="https://github.com/tillheidrich/clip2md" target="_blank" rel="noopener">GitHub</a>
<a href="/impressum.html" id="lnkImp" data-i18n="l_imp">Impressum</a>
<a href="/datenschutz.html" id="lnkDs" data-i18n="l_ds">Datenschutz</a>
</nav>
</footer>
<div class="scrim" id="scrim"></div>
<div class="pop" id="settingsPop" role="dialog" aria-modal="false" aria-label="Settings">
<h3 class="mono-label" data-i18n="s_output">Output</h3>
<div class="row">
<label for="fmToggle"><span data-i18n="s_fm">YAML front matter</span><span class="hint" data-i18n="s_fm_hint">Prepend title, date, source, tags</span></label>
<span class="sw"><input type="checkbox" id="fmToggle" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="field two" id="fmFields" hidden>
<input id="fmSource" type="url" inputmode="url" data-i18n-ph="ph_source" placeholder="Source URL" />
<input id="fmTags" type="text" data-i18n-ph="ph_tags" placeholder="tags, comma-separated" />
</div>
<div class="sep"></div>
<h3 class="mono-label" data-i18n="s_conv">Conversion</h3>
<div class="row">
<label for="stripLinks" data-i18n="s_links">Links as plain text</label>
<span class="sw"><input type="checkbox" id="stripLinks" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="row">
<label for="dropImages" data-i18n="s_images">Remove images</label>
<span class="sw"><input type="checkbox" id="dropImages" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="field two">
<select id="bulletSel" aria-label="Bullet marker">
<option value="-">- bullets</option>
<option value="*">* bullets</option>
<option value="+">+ bullets</option>
</select>
<select id="headingSel" aria-label="Heading style">
<option value="atx"># headings</option>
<option value="setext">= headings</option>
</select>
</div>
<div class="sep"></div>
<h3 class="mono-label" data-i18n="s_clean">Clean-up</h3>
<div class="row">
<label for="cleanToggle"><span data-i18n="s_cleanup">Clean up web pages</span><span class="hint" data-i18n="s_cleanup_hint">Drop navigation, cookie banners, buttons</span></label>
<span class="sw"><input type="checkbox" id="cleanToggle" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="row">
<label for="redditToggle"><span data-i18n="s_reddit">Format Reddit threads</span><span class="hint" data-i18n="s_reddit_hint">No vote counts, comments as nested quotes</span></label>
<span class="sw"><input type="checkbox" id="redditToggle" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="sep"></div>
<h3 class="mono-label">Obsidian</h3>
<div class="row">
<label for="obsToggle"><span data-i18n="s_obs">“Open in Obsidian” button</span><span class="hint" data-i18n="s_obs_hint">Creates the note directly in your vault</span></label>
<span class="sw"><input type="checkbox" id="obsToggle" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="field two" id="obsFields" hidden>
<input id="obsVault" type="text" autocapitalize="off" autocorrect="off" spellcheck="false" data-i18n-ph="ph_vault" placeholder="Vault (optional)" />
<input id="obsFolder" type="text" autocapitalize="off" autocorrect="off" spellcheck="false" data-i18n-ph="ph_folder" placeholder="Folder (optional)" />
</div>
<div class="sep"></div>
<h3 class="mono-label" data-i18n="history">History</h3>
<div class="row">
<label for="histToggle"><span data-i18n="s_hist">Keep the last 10 clips</span><span class="hint" data-i18n="s_hist_hint">Stored only in this browser. Off by default.</span></label>
<span class="sw"><input type="checkbox" id="histToggle" /><span class="track"></span><span class="knob"></span></span>
</div>
<div class="sep"></div>
<h3 class="mono-label" data-i18n="s_file">Filename</h3>
<div class="field">
<select id="namePattern" aria-label="Filename pattern">
<option value="title" data-i18n="s_p_title">Title + date</option>
<option value="datetime" data-i18n="s_p_dt">Date + time</option>
</select>
</div>
</div>
<div class="pop wide" id="histPop" role="dialog" aria-labelledby="histTitle">
<div class="sheet-head"><h2 id="histTitle" data-i18n="history">History</h2><button class="pill icon" type="button" data-close aria-label="Close">✕</button></div>
<ul class="hist" id="histList"></ul>
<p class="empty-note" id="histEmpty" data-i18n="hist_empty">No clips saved yet.</p>
<div class="foot"><span class="mono-label" data-i18n="hist_local">Only in this browser</span><button class="mini" type="button" id="histClear" data-i18n="hist_clear">Delete all</button></div>
</div>
<div class="pop wide" id="helpPop" role="dialog" aria-labelledby="helpTitle">
<div class="sheet-head"><h2 id="helpTitle" data-i18n="iphone">iPhone shortcut</h2><button class="pill icon" type="button" data-close aria-label="Close">✕</button></div>
<div class="help" id="helpBody"></div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="vendor/turndown.js"></script>
<script src="vendor/turndown-plugin-gfm.js"></script>
<script src="vendor/marked.umd.js"></script>
<script src="vendor/purify.min.js"></script>
<script src="vendor/Readability.js"></script>
<script src="clean.js"></script>
<script>
(function () {
"use strict";
var T = {
en: {
tagline: 'Paste → Markdown', preview: 'Preview', file: 'File', save: 'Save', copy: 'Copy', clear: 'Clear', open: 'Open', paste: 'Paste',
donate: 'Donate', coffee: 'Buy me a coffee', t_donate: 'Support clip2md',
l_imp: 'Legal notice', l_ds: 'Privacy', raw_again: 'Undo clean-up', t_obs: 'Open in Obsidian', history: 'History', iphone: 'iPhone shortcut',
src_article: 'Article extracted', src_cleaned: 'Page cleaned up', src_reddit: 'Reddit thread',
s_clean: 'Clean-up', s_cleanup: 'Clean up web pages', s_cleanup_hint: 'Drop navigation, cookie banners, buttons',
s_reddit: 'Format Reddit threads', s_reddit_hint: 'No vote counts, comments as nested quotes',
s_obs: '“Open in Obsidian” button', s_obs_hint: 'Creates the note directly in your vault', ph_vault: 'Vault (optional)', ph_folder: 'Folder (optional)',
s_hist: 'Keep the last 10 clips', s_hist_hint: 'Stored only in this browser. Off by default.',
hist_empty: 'No clips saved yet.', hist_local: 'Only in this browser', hist_clear: 'Delete all', hist_clear_confirm: 'Delete the whole history?',
hist_off_confirm: 'Also delete the clips saved so far?', hist_loaded: 'Clip restored', comments: 'Comments',
obs_open: 'Opening Obsidian …', obs_fail: 'Could not hand over to Obsidian — copy the Markdown instead', raw_done: 'Inserted without clean-up',
ios_hint: 'Tap “Paste” to take over the page',
help_html: '<p>iOS does not let web apps into the share sheet. A shortcut does the job: it copies the page and opens clip2md.</p><ol><li>Open the <b>Shortcuts</b> app, tap <b>+</b>, name it <b>clip2md</b>.</li><li>Tap the <b>ⓘ</b> icon, turn on <b>Show in Share Sheet</b> and allow <b>Safari web pages</b>, <b>Rich text</b>, <b>Text</b>.</li><li>Add <b>Get Details of Safari Web Page</b>, set it to <b>Page Contents</b>.</li><li>Add <b>Copy to Clipboard</b>.</li><li>Add <b>Open URLs</b> with <code>https://clip2md.heidrich-digital.de/?from=ios</code></li></ol><p>In Safari: Share → <b>clip2md</b> → tap <b>Paste</b>. The first time, iOS asks whether clip2md may paste.</p><p>For a selection only: copy it, then use the Paste button — no shortcut needed.</p>',
empty_h: 'Paste to start',
empty_p: 'A long Reddit thread, an article, any formatted text. Rich HTML becomes clean Markdown, plain text stays as it is.',
paste_big: 'Paste from clipboard', k_paste: 'paste', k_save: 'save', k_drop: 'or drop a file',
words: 'words', chars: 'chars', minread: 'min read',
privacy: 'Runs in your browser. Nothing is uploaded.',
s_output: 'Output', s_fm: 'YAML front matter', s_fm_hint: 'Prepend title, date, source, tags',
s_conv: 'Conversion', s_links: 'Links as plain text', s_images: 'Remove images', s_file: 'Filename',
s_p_title: 'Title + date', s_p_dt: 'Date + time',
ph_source: 'Source URL', ph_tags: 'tags, comma-separated',
t_preview: 'Toggle preview', t_settings: 'Settings', t_theme: 'Toggle theme',
src_conv: 'HTML → Markdown', src_raw: 'Plain text', src_raw_forced: 'Plain text (forced)', src_fail: 'Plain text (conversion failed)',
saved: 'Saved', copied: 'Markdown copied', nosave: 'Nothing to save', nocopy: 'Nothing to copy',
loaded: 'File loaded', clear_confirm: 'Clear the editor?', copyfail: 'Copy failed',
paste_empty: 'The clipboard is empty', paste_denied: 'No clipboard access — press ⌘V / Ctrl+V or long-press the editor and choose Paste',
replace_confirm: 'Replace the editor content?', toobig: 'File too large (max 10 MB)', readfail: 'Could not read file'
},
de: {
tagline: 'Einfügen → Markdown', preview: 'Vorschau', file: 'Datei', save: 'Speichern', copy: 'Kopieren', clear: 'Leeren', open: 'Öffnen', paste: 'Einfügen',
donate: 'Spenden', coffee: 'Spendier mir einen Kaffee', t_donate: 'clip2md unterstützen',
l_imp: 'Impressum', l_ds: 'Datenschutz', raw_again: 'Ohne Aufräumen', t_obs: 'In Obsidian öffnen', history: 'Verlauf', iphone: 'iPhone-Kurzbefehl',
src_article: 'Artikel extrahiert', src_cleaned: 'Seite aufgeräumt', src_reddit: 'Reddit-Thread',
s_clean: 'Aufräumen', s_cleanup: 'Webseiten aufräumen', s_cleanup_hint: 'Navigation, Cookie-Banner, Knöpfe entfernen',
s_reddit: 'Reddit-Threads aufbereiten', s_reddit_hint: 'Keine Votes, Kommentare als verschachtelte Zitate',
s_obs: 'Knopf „In Obsidian öffnen"', s_obs_hint: 'Legt die Notiz direkt im Vault an', ph_vault: 'Vault (optional)', ph_folder: 'Ordner (optional)',
s_hist: 'Letzte 10 Clips behalten', s_hist_hint: 'Nur in diesem Browser gespeichert. Standardmäßig aus.',
hist_empty: 'Noch keine Clips gespeichert.', hist_local: 'Nur in diesem Browser', hist_clear: 'Alle löschen', hist_clear_confirm: 'Gesamten Verlauf löschen?',
hist_off_confirm: 'Bisher gespeicherte Clips auch löschen?', hist_loaded: 'Clip wiederhergestellt', comments: 'Kommentare',
obs_open: 'Obsidian wird geöffnet …', obs_fail: 'Übergabe an Obsidian fehlgeschlagen — Markdown stattdessen kopieren', raw_done: 'Ohne Aufräumen eingefügt',
ios_hint: 'Tippe auf „Einfügen", um die Seite zu übernehmen',
help_html: '<p>iOS lässt Web-Apps nicht ins Teilen-Menü. Ein Kurzbefehl übernimmt das: Er kopiert die Seite und öffnet clip2md.</p><ol><li>App <b>Kurzbefehle</b> öffnen, <b>+</b> tippen, Name <b>clip2md</b>.</li><li>Auf <b>ⓘ</b> tippen, <b>Im Share-Sheet anzeigen</b> einschalten und <b>Safari-Webseiten</b>, <b>Rich Text</b>, <b>Text</b> zulassen.</li><li>Aktion <b>Details von Safari-Webseite abrufen</b> hinzufügen, auf <b>Seiteninhalt</b> stellen.</li><li>Aktion <b>In Zwischenablage kopieren</b> hinzufügen.</li><li>Aktion <b>URLs öffnen</b> mit <code>https://clip2md.heidrich-digital.de/?from=ios</code></li></ol><p>In Safari dann: Teilen → <b>clip2md</b> → <b>Einfügen</b> tippen. Beim ersten Mal fragt iOS, ob clip2md einfügen darf.</p><p>Nur markierter Text: kopieren und den Einfügen-Knopf nutzen, dafür braucht es keinen Kurzbefehl.</p>',
empty_h: 'Einfügen und los',
empty_p: 'Ein langer Reddit-Thread, ein Artikel, beliebig formatierter Text. HTML wird zu sauberem Markdown, reiner Text bleibt, wie er ist.',
paste_big: 'Aus Zwischenablage einfügen', k_paste: 'einfügen', k_save: 'speichern', k_drop: 'oder Datei hineinziehen',
words: 'Wörter', chars: 'Zeichen', minread: 'Min. Lesezeit',
privacy: 'Läuft in deinem Browser. Nichts wird hochgeladen.',
s_output: 'Ausgabe', s_fm: 'YAML-Front-Matter', s_fm_hint: 'Titel, Datum, Quelle, Tags voranstellen',
s_conv: 'Konvertierung', s_links: 'Links als Klartext', s_images: 'Bilder entfernen', s_file: 'Dateiname',
s_p_title: 'Titel + Datum', s_p_dt: 'Datum + Uhrzeit',
ph_source: 'Quell-URL', ph_tags: 'Tags, kommagetrennt',
t_preview: 'Vorschau umschalten', t_settings: 'Einstellungen', t_theme: 'Hell/Dunkel',
src_conv: 'HTML → Markdown', src_raw: 'Reiner Text', src_raw_forced: 'Reiner Text (erzwungen)', src_fail: 'Reiner Text (Konvertierung fehlgeschlagen)',
saved: 'Gespeichert', copied: 'Markdown kopiert', nosave: 'Nichts zu speichern', nocopy: 'Nichts zu kopieren',
loaded: 'Datei geladen', clear_confirm: 'Editor wirklich leeren?', copyfail: 'Kopieren fehlgeschlagen',
paste_empty: 'Die Zwischenablage ist leer', paste_denied: 'Kein Zugriff auf die Zwischenablage — ⌘V / Strg+V drücken oder lange in den Editor tippen und „Einfügen" wählen',
replace_confirm: 'Editor-Inhalt ersetzen?', toobig: 'Datei zu groß (max. 10 MB)', readfail: 'Datei nicht lesbar'
}
};
var lang = 'en';
function t(k) { return (T[lang] && T[lang][k]) || T.en[k] || k; }
var LS = { get: function(k,d){ try { var v = localStorage.getItem('clip2md:'+k); return v===null?d:JSON.parse(v); } catch(e){ return d; } },
set: function(k,v){ try { localStorage.setItem('clip2md:'+k, JSON.stringify(v)); } catch(e){} } };
var $ = function(id){ return document.getElementById(id); };
var editor=$('editor'), main=$('main'), preview=$('preview'),
filename=$('filename'), fnameBox=$('fnameBox'), fnameTag=$('fnameTag'),
wordsEl=$('words'), charsEl=$('chars'), rtEl=$('rt'),
srcBadge=$('srcBadge'), srcText=$('srcText'), toastEl=$('toast'),
settingsPop=$('settingsPop'), settingsBtn=$('settingsBtn'), previewBtn=$('previewBtn'), themeBtn=$('themeBtn'), scrim=$('scrim');
var coarse = window.matchMedia && matchMedia('(pointer: coarse)').matches;
var settings = {
frontMatter: LS.get('frontMatter', false), source: LS.get('source', ''), tags: LS.get('tags', ''),
stripLinks: LS.get('stripLinks', false), dropImages: LS.get('dropImages', false),
bullet: LS.get('bullet', '-'), heading: LS.get('heading', 'atx'), pattern: LS.get('pattern', 'title'),
cleanup: LS.get('cleanup', true), reddit: LS.get('reddit', true), obs: LS.get('obs', false), obsVault: LS.get('obsVault', ''), obsFolder: LS.get('obsFolder', ''), hist: LS.get('hist', false)
};
var filenameEdited = false, forceRaw = false, forceRawTimer = null, toastTimer = null, previewOn = LS.get('preview', false);
// ---------- Turndown ----------
var td;
function buildTurndown() {
td = new TurndownService({ headingStyle: settings.heading, hr: '---', bulletListMarker: settings.bullet,
codeBlockStyle: 'fenced', emDelimiter: '*', strongDelimiter: '**', linkStyle: 'inlined' });
try { if (window.turndownPluginGfm && window.turndownPluginGfm.gfm) td.use(window.turndownPluginGfm.gfm); } catch(e){}
td.remove(['script','style','noscript','iframe','svg','meta','link']);
// One space after the list marker instead of Turndown's three.
td.addRule('tightListItem', { filter: 'li', replacement: function(content, node, opt){
content = content.replace(/^\n+/, '').replace(/\n+$/, '\n').replace(/\n/gm, '\n ');
var prefix = opt.bulletListMarker + ' ', parent = node.parentNode;
if (parent && parent.nodeName === 'OL') { var start = parent.getAttribute('start'), idx = Array.prototype.indexOf.call(parent.children, node);
prefix = (start ? Number(start) + idx : idx + 1) + '. '; }
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : ''); } });
if (settings.dropImages) td.addRule('noimg', { filter: 'img', replacement: function(){ return ''; } });
if (settings.stripLinks) td.addRule('flatlink', { filter: 'a', replacement: function(c){ return c; } });
}
buildTurndown();
function htmlToMd(html){ return td.turndown(html).replace(/\n{3,}/g,'\n\n').trim(); }
// ---------- Helpers ----------
function toast(msg, isErr, ms){ toastEl.textContent = msg; toastEl.classList.toggle('err', !!isErr); toastEl.classList.add('show');
clearTimeout(toastTimer); toastTimer = setTimeout(function(){ toastEl.classList.remove('show'); }, ms || 2400); }
function pad(n){ return n<10?'0'+n:''+n; }
function today(){ var d=new Date(); return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); }
function nowTime(){ var d=new Date(); return pad(d.getHours())+pad(d.getMinutes()); }
var UML={ 'ä':'ae','ö':'oe','ü':'ue','ß':'ss','Ä':'ae','Ö':'oe','Ü':'ue' };
function slugify(s){ if(!s) return '';
s=s.replace(/[äöüßÄÖÜ]/g,function(c){return UML[c]||c;}).normalize('NFKD').replace(/[̀-ͯ]/g,'');
return s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60).replace(/-+$/g,''); }
function deriveTitle(md){ var L=md.split(/\r?\n/), i, m, s;
for(i=0;i<L.length;i++){ m=L[i].match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/); if(m&&m[1].trim()) return m[1].trim(); }
for(i=0;i<L.length;i++){ s=L[i].trim(); if(!s) continue; if(/^([-*_=#>`|]{1,}|\[.*\])$/.test(s)) continue; return s.replace(/^[#>*\-\s]+/,'').replace(/!?\[([^\]]*)\]\([^)]*\)/g,'$1').replace(/[*_`]/g,'').trim(); }
return ''; }
function computeFilename(md){ if (settings.pattern === 'datetime') return today()+'-'+nowTime();
var slug = slugify(deriveTitle(md)); return today()+(slug?'-'+slug:'-clip'); }
function refreshFilename(flash){ if(filenameEdited) return; var next=computeFilename(editor.value);
if(next!==filename.value){ filename.value=next; if(flash){ fnameBox.classList.remove('flash'); void fnameBox.offsetWidth; fnameBox.classList.add('flash'); } } }
function setFnameTag(){ if(filenameEdited){ fnameTag.textContent = lang==='de'?'eigen':'custom'; fnameTag.classList.remove('auto'); }
else { fnameTag.textContent='auto'; fnameTag.classList.add('auto'); } }
function countWords(v){ return v.trim()?v.trim().split(/\s+/).length:0; }
function updateCounts(){ var v=editor.value, w=countWords(v), loc=lang==='de'?'de-DE':'en-US';
charsEl.textContent=v.length.toLocaleString(loc); wordsEl.textContent=w.toLocaleString(loc); rtEl.textContent=Math.max(1, Math.round(w/200)); }
function updateEmpty(){ main.classList.toggle('has-content', editor.value.length>0); }
function buildFrontMatter(){ if(!settings.frontMatter) return '';
var title=deriveTitle(editor.value)||'Untitled', lines=['---','title: '+JSON.stringify(title),'date: '+today()];
if(settings.source.trim()) lines.push('source: '+JSON.stringify(settings.source.trim()));
if(settings.tags.trim()){ var tg=settings.tags.split(',').map(function(s){return s.trim();}).filter(Boolean); if(tg.length) lines.push('tags: ['+tg.join(', ')+']'); }
lines.push('---',''); return lines.join('\n'); }
function fullContent(){ return buildFrontMatter()+editor.value; }
// ---------- Preview ----------
var pvTimer=null;
function escapeHtml(s){ return s.replace(/[&<>"']/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]; }); }
function renderPreview(){ if(!previewOn) return; var md=editor.value, fmHtml='';
if(settings.frontMatter){ var m=buildFrontMatter().match(/^---\n([\s\S]*?)\n---/);
if(m){ fmHtml='<div class="fm">'+m[1].split('\n').map(function(l){ var i=l.indexOf(':'); if(i<0) return escapeHtml(l);
return '<span><b>'+escapeHtml(l.slice(0,i))+'</b>'+escapeHtml(l.slice(i))+'</span>'; }).join('')+'</div>'; } }
var html=window.marked?window.marked.parse(md,{gfm:true,breaks:false}):escapeHtml(md);
preview.innerHTML=fmHtml+(window.DOMPurify?window.DOMPurify.sanitize(html):escapeHtml(md)); }
function schedulePreview(){ if(!previewOn) return; clearTimeout(pvTimer); pvTimer=setTimeout(renderPreview,120); }
function showSource(key, ok){ srcText.textContent=t(key); srcBadge.dataset.key=key; srcBadge.classList.toggle('ok',!!ok); srcBadge.classList.add('show');
srcBadge.classList.remove('pulse'); void srcBadge.offsetWidth; srcBadge.classList.add('pulse'); }
function hideSource(){ srcBadge.classList.remove('show','ok','pulse'); srcBadge.dataset.key=''; }
function refreshAll(flash){ updateCounts(); refreshFilename(flash); updateEmpty(); schedulePreview(); scheduleHistory(); syncHeadButtons(); }
// ---------- Insert ----------
function insertAtCursor(str){
// execCommand keeps the textarea's native undo stack intact.
editor.focus({ preventScroll: true }); var ok=false;
try { ok = document.execCommand('insertText', false, str); } catch(e){ ok=false; }
if(ok) return;
var s=editor.selectionStart, en=editor.selectionEnd;
editor.value=editor.value.slice(0,s)+str+editor.value.slice(en);
editor.selectionStart=editor.selectionEnd=s+str.length; }
// Clean-up (clean.js): Reddit → structured Markdown; pages → Readability or junk stripping.
function convertHtml(html){
var C=window.Clip2mdClean, r;
if(C && (settings.cleanup || settings.reddit)){
try { r=C.prepare(html, { cleanup: settings.cleanup, reddit: settings.reddit, toMd: htmlToMd, labels: { comments: t('comments') } }); } catch(e){ r=null; }
}
if(!r) return { md: htmlToMd(html), key: 'src_conv' };
var md = r.md != null ? r.md : htmlToMd(r.html);
if(settings.cleanup || r.reddit) md = C.postprocess(md, { reddit: !!r.reddit && settings.reddit });
if(r.title && md.indexOf(r.title) < 0) md = '# ' + r.title + '\n\n' + md;
else if(r.title && md.indexOf('## ' + r.title) === 0) md = md.slice(1);
var key = r.kind==='reddit' ? 'src_reddit' : r.kind==='article' ? 'src_article' : r.kind==='cleaned' ? 'src_cleaned' : 'src_conv';
return { md: md, key: key, changed: r.kind!=='plain' };
}
var lastInsert=null, rawBtn=$('rawBtn');
function ingest(html, text, raw){
var out, key, ok=false, changed=false;
if(!raw && html && html.trim()){
try { var c=convertHtml(html); out=c.md; key=c.key; changed=!!c.changed; ok=true; if(!out && text) { out=text; key='src_raw'; ok=false; changed=false; } }
catch(err){ out=text||''; key='src_fail'; }
} else { out=text||''; key=raw?'src_raw_forced':'src_raw'; }
if(!out) return false;
if(!editor.value.trim()) historyNew();
var start=editor.selectionStart;
insertAtCursor(out); showSource(key, ok); refreshAll(true);
lastInsert = changed ? { start: start, len: out.length, html: html } : null;
rawBtn.hidden = !changed;
return true; }
rawBtn.addEventListener('click', function(){
if(!lastInsert) return;
var plain; try { plain=htmlToMd(lastInsert.html); } catch(e){ return; }
editor.focus(); editor.setSelectionRange(lastInsert.start, lastInsert.start+lastInsert.len);
insertAtCursor(plain); lastInsert=null; rawBtn.hidden=true; showSource('src_conv', true); refreshAll(false); toast(t('raw_done'));
});
editor.addEventListener('paste', function(e){
var cd=e.clipboardData||window.clipboardData; if(!cd) return;
var html=cd.getData('text/html'), text=cd.getData('text/plain'), raw=forceRaw; forceRaw=false;
if(!html && !text) return;
e.preventDefault(); ingest(html, text, raw);
});
editor.addEventListener('input', function(){ hideSource(); rawBtn.hidden=true; lastInsert=null; refreshAll(false); });
// ---------- One-click paste (Async Clipboard API) ----------
function pasteFromClipboard(){
var c = navigator.clipboard;
function fail(err){
if(err && err.name==='NotAllowedError' || !c){ toast(t('paste_denied'), true, 4200); }
else { toast(t('paste_empty'), true); }
editor.focus();
}
function done(html, text){ if(!ingest(html, text, false)) fail({ name: 'Empty' }); }
if(c && c.read){
// Must be the first call inside the click handler, otherwise Safari drops the user gesture.
c.read().then(function(items){
var htmlP=null, textP=null;
for(var i=0;i<items.length;i++){ var ty=items[i].types;
if(!htmlP && ty.indexOf('text/html')>-1) htmlP=items[i].getType('text/html').then(function(b){ return b.text(); });
if(!textP && ty.indexOf('text/plain')>-1) textP=items[i].getType('text/plain').then(function(b){ return b.text(); }); }
return Promise.all([htmlP||'', textP||'']);
}).then(function(r){ done(r[0], r[1]); }, function(err){
if(c.readText && err && err.name!=='NotAllowedError'){ c.readText().then(function(tx){ done('', tx); }, fail); } else fail(err);
});
} else if(c && c.readText){ c.readText().then(function(tx){ done('', tx); }, fail); }
else fail({ name: 'NotAllowedError' });
}
$('pasteBtn').addEventListener('click', pasteFromClipboard);
$('pasteBig').addEventListener('click', pasteFromClipboard);
// ---------- Files (drop + picker) ----------
function loadFile(f){
if(!f) return;
if(f.size > 10*1024*1024){ toast(t('toobig'), true); return; }
if(editor.value.trim() && !confirm(t('replace_confirm'))) return;
var r=new FileReader();
r.onerror=function(){ toast(t('readfail'), true); };
r.onload=function(){ var txt=String(r.result||''), name=f.name.toLowerCase();
if(/\.html?$/.test(name) || f.type==='text/html'){ try{ var cv=convertHtml(txt); txt=cv.md; showSource(cv.key,true); }catch(_){ showSource('src_fail',false); } }
else showSource('src_raw',false);
historyNew(); editor.value=txt; rawBtn.hidden=true; lastInsert=null;
if(!filenameEdited){ var base=slugify(f.name.replace(/\.[^.]+$/,'')); if(base) filename.value=base; }
refreshAll(true); toast(t('loaded')); };
r.readAsText(f);
}
['dragenter','dragover'].forEach(function(ev){ editor.addEventListener(ev, function(e){ e.preventDefault(); main.classList.add('drag'); }); });
['dragleave','dragend'].forEach(function(ev){ editor.addEventListener(ev, function(){ main.classList.remove('drag'); }); });
editor.addEventListener('drop', function(e){ main.classList.remove('drag');
var f=e.dataTransfer&&e.dataTransfer.files&&e.dataTransfer.files[0]; if(!f) return; e.preventDefault(); loadFile(f); });
var fileInput=$('fileInput');
$('openBtn').addEventListener('click', function(){ fileInput.value=''; fileInput.click(); });
fileInput.addEventListener('change', function(){ loadFile(fileInput.files && fileInput.files[0]); });
// ---------- Filename ----------
filename.addEventListener('input', function(){ filenameEdited=filename.value.trim().length>0; setFnameTag(); });
filename.addEventListener('blur', function(){ if(!filename.value.trim()){ filenameEdited=false; setFnameTag(); refreshFilename(false); } });
filename.addEventListener('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); filename.blur(); } });
// ---------- Save / Copy / Clear ----------
function flashEditor(){ var p=editor.parentNode; p.style.borderColor='var(--err)'; setTimeout(function(){ p.style.borderColor=''; },600); }
function save(){
if(!editor.value.trim()){ toast(t('nosave'),true); flashEditor(); return; }
var name=(filename.value.trim()||computeFilename(editor.value)).replace(/[\/\\:*?"<>|]+/g,'-').replace(/\s+/g,'-');
if(!/\.md$/i.test(name)) name+='.md';
var content=fullContent();
// Phones: hand the file to the share sheet (Files, Obsidian, mail …). Desktop: download.
if(coarse && navigator.canShare && typeof File==='function'){
try {
var file=new File([content], name, { type: 'text/markdown' });
if(navigator.canShare({ files: [file] })){
navigator.share({ files: [file], title: name }).then(function(){ toast(t('saved')+' · '+name); }, function(err){ if(err && err.name!=='AbortError') download(content, name); });
return;
}
} catch(e){}
}
download(content, name);
}
function download(content, name){
var blob=new Blob([content],{type:'text/markdown;charset=utf-8'}), url=URL.createObjectURL(blob), a=document.createElement('a');
a.href=url; a.download=name; document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function(){ URL.revokeObjectURL(url); },1500); toast(t('saved')+' · '+name); }
$('save').addEventListener('click', save);
$('copy').addEventListener('click', function(){ if(!editor.value.trim()){ toast(t('nocopy'),true); return; } var v=fullContent();
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(v).then(function(){ toast(t('copied')); }, function(){ legacyCopy(v); }); }
else legacyCopy(v); });
function legacyCopy(v){ var ta=document.createElement('textarea'); ta.value=v; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select();
try{ document.execCommand('copy'); toast(t('copied')); }catch(e){ toast(t('copyfail'),true); } document.body.removeChild(ta); }
$('clear').addEventListener('click', function(){ if(editor.value && !confirm(t('clear_confirm'))) return;
flushHistory(); historyNew(); editor.value=''; filenameEdited=false; hideSource(); rawBtn.hidden=true; setFnameTag(); refreshAll(false); if(!coarse) editor.focus(); });
// ---------- Keyboard ----------
document.addEventListener('keydown', function(e){ var mod=e.metaKey||e.ctrlKey;
if(mod && !e.shiftKey && !e.altKey && (e.key==='s'||e.key==='S')){ e.preventDefault(); save(); return; }
if(mod && e.shiftKey && (e.key==='v'||e.key==='V')){ forceRaw=true; clearTimeout(forceRawTimer); forceRawTimer=setTimeout(function(){ forceRaw=false; },3000); }
if(e.key==='Escape') closeSettings(); });
// ---------- Preview ----------
function setPreview(on){ previewOn=on; main.classList.toggle('split',on); previewBtn.setAttribute('aria-pressed',on?'true':'false'); LS.set('preview',on); if(on) renderPreview(); }
previewBtn.addEventListener('click', function(){ setPreview(!previewOn); });
// ---------- Theme ----------
function setTheme(theme, persist){ document.documentElement.setAttribute('data-theme', theme); if(persist) LS.set('theme', theme);
document.querySelector('meta[name=theme-color]').setAttribute('content', theme==='dark'?'#15171c':'#EAEAE3'); }
themeBtn.addEventListener('click', function(){ setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark', true); });
// ---------- Language ----------
function applyLang(){
document.documentElement.setAttribute('lang', lang);
$('lnkImp').href = lang==='de' ? '/impressum.html' : '/imprint.html';
$('lnkDs').href = lang==='de' ? '/datenschutz.html' : '/privacy.html';
document.querySelectorAll('[data-i18n]').forEach(function(el){ el.textContent=t(el.getAttribute('data-i18n')); });
document.querySelectorAll('[data-i18n-ph]').forEach(function(el){ el.setAttribute('placeholder', t(el.getAttribute('data-i18n-ph'))); });
document.querySelectorAll('[data-i18n-title]').forEach(function(el){ el.setAttribute('title', t(el.getAttribute('data-i18n-title'))); el.setAttribute('aria-label', t(el.getAttribute('data-i18n-title'))); });
var lb=$('langBtn'); lb.textContent = lang==='de'?'EN':'DE'; lb.title = lang==='de'?'English':'Deutsch';
if(coarse){ $('empty').querySelector('p').textContent=t('empty_p'); }
if(srcBadge.dataset.key) srcText.textContent=t(srcBadge.dataset.key);
setFnameTag(); updateCounts(); }
function setLang(l){ lang=l; applyLang(); }
$('langBtn').addEventListener('click', function(){ var l=lang==='de'?'en':'de'; LS.set('langChoice', l); setLang(l); });
// ---------- Settings ----------
var sheets=[settingsPop, $('histPop'), $('helpPop')];
function openSheet(el){ sheets.forEach(function(x){ x.classList.toggle('open', x===el); }); scrim.classList.add('open'); settingsBtn.setAttribute('aria-expanded', el===settingsPop?'true':'false'); }
function closeSettings(){ sheets.forEach(function(x){ x.classList.remove('open'); }); scrim.classList.remove('open'); settingsBtn.setAttribute('aria-expanded','false'); }
settingsBtn.addEventListener('click', function(){ settingsPop.classList.contains('open')?closeSettings():openSheet(settingsPop); });
scrim.addEventListener('click', closeSettings);
document.querySelectorAll('[data-close]').forEach(function(b){ b.addEventListener('click', closeSettings); });
$('helpBtn').addEventListener('click', function(){ $('helpBody').innerHTML=t('help_html'); openSheet($('helpPop')); });
function bindToggle(id, key, after){ var el=$(id); el.checked=!!settings[key];
el.addEventListener('change', function(){ settings[key]=el.checked; LS.set(key,el.checked); if(after) after(); }); }
function bindSelect(id, key, after){ var el=$(id); el.value=settings[key];
el.addEventListener('change', function(){ settings[key]=el.value; LS.set(key,el.value); if(after) after(); }); }
function bindText(id, key){ var el=$(id); el.value=settings[key];
el.addEventListener('input', function(){ settings[key]=el.value; LS.set(key,el.value); schedulePreview(); }); }
var fmFields=$('fmFields');
function syncFmFields(){ fmFields.hidden=!settings.frontMatter; }
bindToggle('fmToggle','frontMatter', function(){ syncFmFields(); refreshAll(false); });
bindText('fmSource','source'); bindText('fmTags','tags');
bindToggle('stripLinks','stripLinks', buildTurndown);
bindToggle('dropImages','dropImages', buildTurndown);
bindSelect('bulletSel','bullet', buildTurndown);
bindSelect('headingSel','heading', buildTurndown);
bindSelect('namePattern','pattern', function(){ refreshFilename(true); });
syncFmFields();
bindToggle('cleanToggle','cleanup');
bindToggle('redditToggle','reddit');
var obsFields=$('obsFields');
bindToggle('obsToggle','obs', function(){ obsFields.hidden=!settings.obs; syncHeadButtons(); });
obsFields.hidden=!settings.obs;
['obsVault','obsFolder'].forEach(function(k){ var el=$(k); el.value=settings[k]; el.addEventListener('input', function(){ settings[k]=el.value.trim(); LS.set(k, settings[k]); }); });
bindToggle('histToggle','hist', function(){
if(settings.hist){ historyNew(); scheduleHistory(); }
else if(confirm(t('hist_off_confirm'))) H.clear();
syncHeadButtons(); });
// ---------- Head buttons ----------
function syncHeadButtons(){
var has=editor.value.trim().length>0;
$('obsBtn').hidden = !(settings.obs && has);
$('histBtn').hidden = !settings.hist;
}
// ---------- Obsidian ----------
$('obsBtn').addEventListener('click', function(){
if(!editor.value.trim()){ toast(t('nocopy'), true); return; }
var content=fullContent();
var name=(filename.value.trim()||computeFilename(editor.value)).replace(/[\/\\:*?"<>|#^\[\]]+/g,'-').replace(/\.md$/i,'');
var folder=(settings.obsFolder||'').replace(/^\/+|\/+$/g,'');
var base='obsidian://new?'+(settings.obsVault?'vault='+encodeURIComponent(settings.obsVault)+'&':'')+(folder?'file='+encodeURIComponent(folder+'/'+name):'name='+encodeURIComponent(name));
function go(u){ toast(t('obs_open')); window.location.href=u; }
// Long threads do not fit into a URL: hand the text over via the clipboard instead.
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(content).then(function(){ go(base+'&clipboard=true'); }, function(){
if(content.length<6000) go(base+'&content='+encodeURIComponent(content)); else toast(t('obs_fail'), true, 4000); });
} else if(content.length<6000) go(base+'&content='+encodeURIComponent(content));
else toast(t('obs_fail'), true, 4000);
});
// ---------- History (IndexedDB, opt-in) ----------
var H=(function(){
var dbp=null, MAX=10;
function db(){ if(dbp) return dbp; dbp=new Promise(function(res, rej){ try { var r=indexedDB.open('clip2md',1);
r.onupgradeneeded=function(){ r.result.createObjectStore('clips',{keyPath:'id'}); };
r.onsuccess=function(){ res(r.result); }; r.onerror=function(){ rej(r.error); }; } catch(e){ rej(e); } }); return dbp; }
function tx(mode, fn){ return db().then(function(d){ return new Promise(function(res, rej){ var x=d.transaction('clips',mode), st=x.objectStore('clips'), out=fn(st);
x.oncomplete=function(){ res(out && out.result!==undefined ? out.result : out); }; x.onerror=function(){ rej(x.error); }; }); }); }
return {
put: function(item){ return tx('readwrite', function(st){ st.put(item); }).then(function(){ return H.list(); }).then(function(all){
if(all.length>MAX) return tx('readwrite', function(st){ all.slice(MAX).forEach(function(i){ st.delete(i.id); }); }); }); },
list: function(){ return tx('readonly', function(st){ return st.getAll(); }).then(function(a){ return (a||[]).sort(function(x,y){ return y.ts-x.ts; }); }); },
del: function(id){ return tx('readwrite', function(st){ st.delete(id); }); },
clear: function(){ return tx('readwrite', function(st){ st.clear(); }); }
};
})();
var histId=null, histTimer=null;
function historyNew(){ histId=null; }
function flushHistory(){ clearTimeout(histTimer); saveHistory(); }
function scheduleHistory(){ if(!settings.hist) return; clearTimeout(histTimer); histTimer=setTimeout(saveHistory, 1200); }
function saveHistory(){
if(!settings.hist || !window.indexedDB) return; var md=editor.value; if(!md.trim()) return;
if(!histId) histId=Date.now();
H.put({ id: histId, ts: Date.now(), title: deriveTitle(md).slice(0,120) || filename.value, name: filename.value, md: md }).catch(function(){});
}
function renderHistory(){
var ul=$('histList'); ul.innerHTML='';
H.list().then(function(items){
$('histEmpty').hidden = items.length>0; $('histClear').hidden = !items.length;
items.forEach(function(it){
var li=document.createElement('li'), b=document.createElement('button'), d=document.createElement('button');
b.className='load'; b.type='button';
var ti=document.createElement('b'); ti.textContent=it.title||it.name||'—';
var meta=document.createElement('span'); meta.textContent=new Date(it.ts).toLocaleString(lang==='de'?'de-DE':'en-GB',{dateStyle:'short',timeStyle:'short'})+' · '+countWords(it.md)+' '+t('words');
b.appendChild(ti); b.appendChild(meta);
b.addEventListener('click', function(){
flushHistory();
editor.value=it.md; histId=it.id; filenameEdited=!!it.name; if(it.name) filename.value=it.name; setFnameTag();
hideSource(); rawBtn.hidden=true; refreshAll(false); closeSettings(); toast(t('hist_loaded')); });
d.className='del'; d.type='button'; d.setAttribute('aria-label','Delete'); d.textContent='✕';
d.addEventListener('click', function(){ H.del(it.id).then(renderHistory); if(histId===it.id) histId=null; });
li.appendChild(b); li.appendChild(d); ul.appendChild(li);
});
}).catch(function(){ $('histEmpty').hidden=false; });
}
$('histBtn').addEventListener('click', function(){ flushHistory(); setTimeout(renderHistory, 50); openSheet($('histPop')); });
$('histClear').addEventListener('click', function(){ if(confirm(t('hist_clear_confirm'))) H.clear().then(renderHistory); });
// ---------- Donate (only shown once a working link is configured) ----------
var DONATE_URL='https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ';
if(DONATE_URL){ document.querySelectorAll('[data-donate]').forEach(function(a){ a.href=DONATE_URL; a.target='_blank'; a.rel='noopener nofollow'; a.hidden=false; }); }
// ---------- Init ----------
setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light', false);
// Language: an explicit choice (language button) wins; otherwise the browser's preferred languages.
// The old 'lang' key is ignored on purpose: earlier versions stored 'en' for everyone.
function browserLang(){
var list=(navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language||'en'];
for(var i=0;i<list.length;i++){ var c=String(list[i]||'').slice(0,2).toLowerCase(); if(c==='de'||c==='en') return c; }
return 'en';
}
var chosen=LS.get('langChoice', null);
setLang(chosen==='de'||chosen==='en' ? chosen : browserLang());
setPreview(previewOn);
setFnameTag(); refreshAll(false);
// ---------- Share target (Android: Share → clip2md) ----------
(function(){
var q; try { q=new URLSearchParams(location.search); } catch(e){ return; }
var title=(q.get('title')||'').trim(), text=(q.get('text')||'').trim(), url=(q.get('url')||'').trim();
if(q.get('from')==='ios'){ try { history.replaceState(null, '', location.pathname); } catch(e){}
setTimeout(function(){ toast(t('ios_hint'), false, 4500); var pb=$('pasteBtn'); pb.classList.add('nudge'); $('pasteBig').classList.add('nudge'); }, 300); return; }
if(!title && !text && !url) return;
if(!url){ var m=text.match(/https?:\/\/\S+$/); if(m){ url=m[0]; text=text.slice(0,m.index).trim(); } }
var md=(title && text.indexOf(title)!==0 ? '# '+title+'\n\n' : '')+text+(url && text.indexOf(url)<0 ? (text?'\n\n':'')+'<'+url+'>' : '');
if(url){ settings.source=url; $('fmSource').value=url; }
historyNew(); editor.value=md; showSource('src_raw', false); refreshAll(true);
try { history.replaceState(null, '', location.pathname); } catch(e){}
})();
if(!coarse) editor.focus();
if('serviceWorker' in navigator){ window.addEventListener('load', function(){ navigator.serviceWorker.register('/sw.js').catch(function(){}); }); }
})();
</script>
</body>
</html>