752 lines
51 KiB
HTML
752 lines
51 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 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; }
|
|
|
|
/* ---------- 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; }
|
|
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; }
|
|
|
|
/* ---------- 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; }
|
|
}
|
|
@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" href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&currency_code=EUR&item_name=clip2md" 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>
|
|
</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" href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&currency_code=EUR&item_name=clip2md" target="_blank" rel="noopener nofollow" data-i18n="coffee">Buy me a coffee</a>
|
|
<a href="https://github.com/tillheidrich/clip2md" target="_blank" rel="noopener">GitHub</a>
|
|
<a href="/impressum.html">Impressum</a>
|
|
<a href="/datenschutz.html">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_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 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>
|
|
(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 via PayPal',
|
|
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 per PayPal unterstützen',
|
|
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')
|
|
};
|
|
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]+/,'').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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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(); }
|
|
|
|
// ---------- 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; }
|
|
function ingest(html, text, raw){
|
|
var out, key, ok=false;
|
|
if(!raw && html && html.trim()){
|
|
try { out=htmlToMd(html); key='src_conv'; ok=true; if(!out && text) { out=text; key='src_raw'; ok=false; } }
|
|
catch(err){ out=text||''; key='src_fail'; }
|
|
} else { out=text||''; key=raw?'src_raw_forced':'src_raw'; }
|
|
if(!out) return false;
|
|
insertAtCursor(out); showSource(key, ok); refreshAll(true); return true; }
|
|
|
|
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(); 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{ txt=htmlToMd(txt); showSource('src_conv',true); }catch(_){ showSource('src_fail',false); } }
|
|
else showSource('src_raw',false);
|
|
editor.value=txt;
|
|
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;
|
|
editor.value=''; filenameEdited=false; hideSource(); 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);
|
|
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; LS.set('lang',l); applyLang(); }
|
|
$('langBtn').addEventListener('click', function(){ setLang(lang==='de'?'en':'de'); });
|
|
|
|
// ---------- Settings ----------
|
|
function openSettings(){ settingsPop.classList.add('open'); scrim.classList.add('open'); settingsBtn.setAttribute('aria-expanded','true'); }
|
|
function closeSettings(){ settingsPop.classList.remove('open'); scrim.classList.remove('open'); settingsBtn.setAttribute('aria-expanded','false'); }
|
|
settingsBtn.addEventListener('click', function(){ settingsPop.classList.contains('open')?closeSettings():openSettings(); });
|
|
scrim.addEventListener('click', closeSettings);
|
|
|
|
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();
|
|
|
|
// ---------- Init ----------
|
|
setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light', false);
|
|
var navLang=(navigator.language||'en').slice(0,2).toLowerCase();
|
|
setLang(LS.get('lang', navLang==='de'?'de':'en'));
|
|
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(!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; }
|
|
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>
|