Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f543903654 | ||
|
|
0918ee0a65 | ||
|
|
70e6d03005 | ||
|
|
a1a58531d8 | ||
|
|
b955d857bf | ||
|
|
b00633af0f | ||
|
|
13eabc6247 |
No files matched your search
@@ -3,7 +3,7 @@ FROM nginx:1.27-alpine
|
||||
# Static single-page app — no build step.
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY security-headers.conf /etc/nginx/security-headers.conf
|
||||
COPY index.html impressum.html datenschutz.html legal.css icon-mono.svg manifest.webmanifest sw.js icon.svg /usr/share/nginx/html/
|
||||
COPY index.html clean.js impressum.html datenschutz.html imprint.html privacy.html legal.css icon-mono.svg manifest.webmanifest sw.js icon.svg icon-192.png icon-512.png icon-maskable-512.png apple-touch-icon.png /usr/share/nginx/html/
|
||||
COPY vendor/ /usr/share/nginx/html/vendor/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
@@ -25,6 +25,16 @@ Obsidian vault, a wiki, or a repo.
|
||||
picks the HTML version when there is one. Browsers ask once for permission; `⌘V` always works too.
|
||||
- **Smart paste.** Formatted HTML (copy from a browser, e.g. Reddit `⌘A` → `⌘C`) is converted to
|
||||
clean Markdown. Plain text stays raw. Same `⌘V` for both, no mode switch.
|
||||
- **Page clean-up.** Pasting a whole web page drops navigation, cookie banners, share buttons and
|
||||
sidebars; articles are extracted with Mozilla Readability. *Undo clean-up* re-inserts the
|
||||
unfiltered version with one click.
|
||||
- **Reddit threads.** New and old Reddit: post title, subreddit and author on top, no vote counts or
|
||||
Reply/Share leftovers, comments as nested quotes with `(OP)` marked.
|
||||
- **Open in Obsidian.** Optional button that creates the note straight in your vault (vault and
|
||||
folder configurable), via the `obsidian://` URI and the clipboard — no file detour.
|
||||
- **History (opt-in).** Keeps the last 10 clips in this browser's IndexedDB. Off by default.
|
||||
- **iPhone shortcut.** iOS keeps web apps out of the share sheet; a four-step Shortcut fills the gap
|
||||
(see [docs/ios-shortcut.md](docs/ios-shortcut.md) or *iPhone shortcut* in the app footer).
|
||||
- **Force raw.** `⌘/Ctrl+Shift+V` pastes the next clip as plain text, skipping conversion.
|
||||
- **Live preview.** Toggle a side-by-side rendered Markdown view (marked + DOMPurify).
|
||||
- **Auto filename.** Derived from the first heading + date, e.g. `2026-08-11-my-thread.md`.
|
||||
@@ -39,7 +49,8 @@ Obsidian vault, a wiki, or a repo.
|
||||
- **Bilingual.** English and German, switchable in the header.
|
||||
- **Light & dark.** Warm paper, one strong blue, clipboard logo; dark mode one click away.
|
||||
- **Save / copy / clear**, word + character count and reading time.
|
||||
- **Installable PWA**, fully offline after first load.
|
||||
- **Installable PWA**, fully offline after first load. On Android, clip2md shows up in the
|
||||
system share sheet: share a page or selection straight into the editor.
|
||||
|
||||
Everything is **client-side**. No servers, no CDNs, no third-party requests — GDPR-friendly by
|
||||
construction.
|
||||
@@ -78,20 +89,22 @@ Any static host works — point it at this repo. The reference deployment uses
|
||||
## Update vendored libraries
|
||||
|
||||
```bash
|
||||
npm pack turndown turndown-plugin-gfm marked dompurify @fontsource/space-grotesk @fontsource/space-mono
|
||||
npm pack turndown turndown-plugin-gfm marked dompurify @mozilla/readability @fontsource/space-grotesk @fontsource/space-mono
|
||||
# turndown/dist/turndown.js -> vendor/turndown.js
|
||||
# turndown-plugin-gfm/dist/turndown-plugin-gfm.js -> vendor/turndown-plugin-gfm.js
|
||||
# marked/lib/marked.umd.js -> vendor/marked.umd.js
|
||||
# dompurify/dist/purify.min.js -> vendor/purify.min.js
|
||||
# @mozilla/readability/Readability.js -> vendor/Readability.js
|
||||
# @fontsource/*/files/*-latin-{400,500,700}-normal.woff2 -> vendor/fonts/
|
||||
```
|
||||
|
||||
Bump the `CACHE` constant in `sw.js` whenever an asset changes so clients pick it up.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE). Turndown, marked, DOMPurify, Space Grotesk and Space Mono are under their respective MIT/Apache/OFL licenses.
|
||||
|
||||
## Support
|
||||
|
||||
clip2md is free. If it saves you time: [buy me a coffee via PayPal](https://www.paypal.com/donate/?business=info%40tillheidrich.de¤cy_code=EUR&item_name=clip2md).
|
||||
clip2md is free. If it saves you time: [buy me a coffee via PayPal](https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ) · Gefällt dir clip2md? [Spendier mir einen Kaffee](https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE). Turndown, marked, DOMPurify, Mozilla Readability, Space Grotesk and Space Mono are under their respective MIT/Apache/OFL licenses.
|
||||
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,159 @@
|
||||
/* clip2md — page clean-up and Reddit formatting. Runs entirely in the browser.
|
||||
Exposes window.Clip2mdClean = { prepare, postprocess }. */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var BUTTON_WORDS = /^(reply|share|report|save|saved|follow|following|subscribe|unsubscribe|join|joined|award|give award|upvote|downvote|vote|like|likes|comment|comments|copy link|copy text|embed|print|tweet|pin it|email|more|more options|show more|show less|read more|see more|continue reading|load more|view more comments|back to top|skip to content|skip to main content|accept|accept all|reject|reject all|allow all|decline|manage cookies|cookie settings|close|dismiss|sign in|log in|sign up|antworten|teilen|melden|speichern|folgen|abonnieren|beitreten|gefällt mir|kommentieren|link kopieren|drucken|mehr|mehr anzeigen|weniger anzeigen|weiterlesen|mehr laden|nach oben|zum inhalt springen|akzeptieren|alle akzeptieren|ablehnen|alle ablehnen|einstellungen|cookie-einstellungen|schließen|anmelden|registrieren)$/i;
|
||||
var JUNK_ATTR = /(^|[\s_-])(cookie|consent|gdpr|cmp|banner|newsletter|subscribe|share|sharing|social|advert|ad-slot|ads|sponsor|promo|popup|modal|overlay|sidebar|breadcrumb|related|recommend|paywall|toolbar|skip-link|comment-form|site-header|site-footer|masthead)([\s_-]|$)/i;
|
||||
var JUNK_SELECTORS = 'nav, aside, footer, form, button, input, select, textarea, dialog, iframe, noscript, template, [hidden], [aria-hidden="true"], [role="navigation"], [role="banner"], [role="contentinfo"], [role="dialog"], [role="alertdialog"], [role="complementary"], [role="search"], [role="menu"], [role="menubar"], [role="toolbar"]';
|
||||
|
||||
function textLen(el) { return el ? (el.textContent || '').replace(/\s+/g, ' ').trim().length : 0; }
|
||||
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
||||
|
||||
// ---------- Generic page clean-up ----------
|
||||
function stripJunk(root) {
|
||||
var total = textLen(root) || 1, removed = 0;
|
||||
Array.prototype.slice.call(root.querySelectorAll(JUNK_SELECTORS)).forEach(function (el) {
|
||||
// Never drop the bulk of the content just because it sits in an odd wrapper.
|
||||
if (el.isConnected && textLen(el) < total * 0.5) { el.remove(); removed++; }
|
||||
});
|
||||
Array.prototype.slice.call(root.querySelectorAll('[class], [id]')).forEach(function (el) {
|
||||
if (!el.isConnected || /^(html|body|main|article)$/i.test(el.tagName)) return;
|
||||
var sig = (el.getAttribute('class') || '') + ' ' + (el.id || '');
|
||||
if (JUNK_ATTR.test(sig) && textLen(el) < total * 0.25) { el.remove(); removed++; }
|
||||
});
|
||||
Array.prototype.slice.call(root.querySelectorAll('a, span, div, li, p, strong, b')).forEach(function (el) {
|
||||
if (!el.isConnected) return;
|
||||
var t = (el.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (t && t.length < 32 && BUTTON_WORDS.test(t) && !el.querySelector('img')) { el.remove(); removed++; }
|
||||
});
|
||||
Array.prototype.slice.call(root.querySelectorAll('img')).forEach(function (img) {
|
||||
var w = parseInt(img.getAttribute('width'), 10), h = parseInt(img.getAttribute('height'), 10);
|
||||
var alt = (img.getAttribute('alt') || '') + ' ' + (img.getAttribute('src') || '');
|
||||
if ((w && w <= 48) || (h && h <= 48) || /avatar|icon|logo|emoji|spacer|pixel|tracking/i.test(alt)) { img.remove(); removed++; }
|
||||
});
|
||||
return removed;
|
||||
}
|
||||
|
||||
function looksLikePage(doc) {
|
||||
return !!doc.querySelector('article, main, [role="main"], nav, header, footer') || doc.querySelectorAll('a').length > 25;
|
||||
}
|
||||
|
||||
// ---------- Reddit ----------
|
||||
function isReddit(doc, html) {
|
||||
if (doc.querySelector('shreddit-comment, shreddit-post, .thing.comment, .thing.link, [data-testid="comment"]')) return true;
|
||||
return /reddit\.com\/(r|user)\//i.test(html) && /\b(reply|upvote|antworten)\b/i.test(doc.body ? doc.body.textContent : '');
|
||||
}
|
||||
|
||||
function quote(lines, depth) {
|
||||
var p = new Array(depth + 1).join('> ');
|
||||
return lines.map(function (l) { return (p + l).replace(/\s+$/, ''); });
|
||||
}
|
||||
|
||||
function nearest(el, sel) { var p = el.parentElement; return p ? p.closest(sel) : null; }
|
||||
|
||||
function redditToMd(doc, toMd, labels) {
|
||||
var out = [], post, title, author, sub, body, comments = [];
|
||||
|
||||
// New Reddit (shreddit web components)
|
||||
post = doc.querySelector('shreddit-post');
|
||||
var sc = doc.querySelectorAll('shreddit-comment');
|
||||
if (post || sc.length) {
|
||||
title = post && post.getAttribute('post-title');
|
||||
author = post && post.getAttribute('author');
|
||||
sub = post && post.getAttribute('subreddit-prefixed-name');
|
||||
body = post && post.querySelector('[slot="text-body"]');
|
||||
var top = Array.prototype.filter.call(sc, function (c) { return !nearest(c, 'shreddit-comment'); });
|
||||
var renderNew = function (c, depth) {
|
||||
var a = c.getAttribute('author') || '[deleted]';
|
||||
var bodyEl = Array.prototype.filter.call(c.querySelectorAll('[slot="comment"]'), function (b) { return b.closest('shreddit-comment') === c; })[0];
|
||||
var lines = ['**u/' + a + '**' + (author && a === author ? ' (OP)' : ''), ''];
|
||||
lines = lines.concat((bodyEl ? toMd(bodyEl.innerHTML) : '').split('\n'));
|
||||
var res = quote(lines, depth);
|
||||
Array.prototype.filter.call(c.querySelectorAll('shreddit-comment'), function (k) { return nearest(k, 'shreddit-comment') === c; })
|
||||
.forEach(function (k) { res.push(quote([''], depth)[0]); res = res.concat(renderNew(k, depth + 1)); });
|
||||
return res;
|
||||
};
|
||||
top.forEach(function (c) { comments.push(renderNew(c, 1).join('\n')); });
|
||||
} else {
|
||||
// Old Reddit
|
||||
var link = doc.querySelector('.thing.link');
|
||||
var oc = doc.querySelectorAll('.thing.comment');
|
||||
if (!link && !oc.length) return null;
|
||||
if (link) {
|
||||
var ta = link.querySelector('a.title'); title = ta && ta.textContent.trim();
|
||||
var au = link.querySelector('.tagline .author'); author = au && au.textContent.trim();
|
||||
var sr = link.querySelector('.tagline .subreddit'); sub = sr && sr.textContent.trim();
|
||||
body = link.querySelector('.usertext-body .md');
|
||||
}
|
||||
var topOld = Array.prototype.filter.call(oc, function (c) { return !nearest(c, '.thing.comment'); });
|
||||
var renderOld = function (c, depth) {
|
||||
var entry = c.querySelector(':scope > .entry') || c;
|
||||
var au2 = entry.querySelector('.tagline .author'); var a = au2 ? au2.textContent.trim() : '[deleted]';
|
||||
var md = entry.querySelector('.usertext-body .md');
|
||||
var lines = ['**u/' + a + '**' + (author && a === author ? ' (OP)' : ''), ''].concat((md ? toMd(md.innerHTML) : '').split('\n'));
|
||||
var res = quote(lines, depth);
|
||||
Array.prototype.filter.call(c.querySelectorAll('.thing.comment'), function (k) { return nearest(k, '.thing.comment') === c; })
|
||||
.forEach(function (k) { res.push(quote([''], depth)[0]); res = res.concat(renderOld(k, depth + 1)); });
|
||||
return res;
|
||||
};
|
||||
topOld.forEach(function (c) { comments.push(renderOld(c, 1).join('\n')); });
|
||||
}
|
||||
|
||||
if (!title) { var h1 = doc.querySelector('h1'); title = h1 && h1.textContent.trim(); }
|
||||
if (title) out.push('# ' + title, '');
|
||||
var meta = [sub, author ? 'u/' + author : ''].filter(Boolean).join(' · ');
|
||||
if (meta) out.push('*' + meta + '*', '');
|
||||
if (body) { var b = toMd(body.innerHTML); if (b) out.push(b, ''); }
|
||||
if (comments.length) { out.push('---', '', '## ' + labels.comments + ' (' + comments.length + ')', '', comments.join('\n\n')); }
|
||||
var res = out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||
return res.length > 20 ? res : null;
|
||||
}
|
||||
|
||||
var REDDIT_LINE = /^(reply|share|report|save|follow|award|upvote|downvote|vote|more replies|\d+ more repl(y|ies)|level \d+|op|edited|•|·|u\/[\w-]+ avatar|r\/[\w-]+ icon|\d+(\.\d+)?\s*[kKmM]?|\d+\s*(s|m|h|d|w|mo|y|yr|min|hr)\.?\s*ago|go to comments|sort by:?|best|top|new|controversial|old|q&a|comments|antworten|teilen|melden)$/i;
|
||||
|
||||
function postprocess(md, opts) {
|
||||
var lines = md.split('\n'), out = [], inFence = false;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var l = lines[i], t = l.trim();
|
||||
if (/^(```|~~~)/.test(t)) { inFence = !inFence; out.push(l); continue; }
|
||||
if (inFence) { out.push(l); continue; }
|
||||
var bare = t.replace(/^[-*+]\s+/, '').replace(/^\[([^\]]*)\]\([^)]*\)$/, '$1').trim();
|
||||
if (bare && BUTTON_WORDS.test(bare)) continue;
|
||||
if (opts.reddit && bare && REDDIT_LINE.test(bare)) continue;
|
||||
if (/^\[\s*\]\([^)]*\)$/.test(t) || /^[|•·\-–—]+$/.test(t) && !/^-{3,}$/.test(t)) continue;
|
||||
out.push(l.replace(/\[\s*\]\([^)]*\)/g, ''));
|
||||
}
|
||||
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||
}
|
||||
|
||||
/* prepare(html, opts) → { md?, html?, kind, title? }
|
||||
opts: { cleanup, reddit, toMd(htmlString)→md, labels:{comments} } */
|
||||
function prepare(html, opts) {
|
||||
var doc;
|
||||
try { doc = new DOMParser().parseFromString(html, 'text/html'); } catch (e) { return { html: html, kind: 'plain' }; }
|
||||
if (!doc || !doc.body) return { html: html, kind: 'plain' };
|
||||
|
||||
var reddit = isReddit(doc, html);
|
||||
if (opts.reddit && reddit) {
|
||||
try { var rmd = redditToMd(doc, opts.toMd, opts.labels); if (rmd) return { md: rmd, kind: 'reddit', reddit: true }; } catch (e) {}
|
||||
}
|
||||
if (!opts.cleanup) return { html: html, kind: 'plain', reddit: reddit };
|
||||
|
||||
var before = textLen(doc.body);
|
||||
if (before > 1200 && typeof window.Readability === 'function' && looksLikePage(doc) && !reddit) {
|
||||
try {
|
||||
var art = new window.Readability(doc.cloneNode(true), { charThreshold: 250, keepClasses: false }).parse();
|
||||
if (art && art.content && (art.textContent || '').trim().length > before * 0.3) {
|
||||
var content = art.content;
|
||||
if (art.title && content.indexOf(art.title) < 0) content = '<h1>' + esc(art.title) + '</h1>' + content;
|
||||
return { html: content, kind: 'article', title: art.title, reddit: reddit };
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
var n = stripJunk(doc.body);
|
||||
return { html: doc.body.innerHTML, kind: n ? 'cleaned' : 'plain', reddit: reddit };
|
||||
}
|
||||
|
||||
window.Clip2mdClean = { prepare: prepare, postprocess: postprocess };
|
||||
})();
|
||||
@@ -8,6 +8,8 @@
|
||||
<title>Datenschutzerklärung — clip2md</title>
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/legal.css" />
|
||||
<link rel="alternate" hreflang="de" href="https://clip2md.heidrich-digital.de/datenschutz.html" />
|
||||
<link rel="alternate" hreflang="en" href="https://clip2md.heidrich-digital.de/privacy.html" />
|
||||
<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>
|
||||
@@ -18,6 +20,7 @@
|
||||
<nav>
|
||||
<a href="/impressum.html">Impressum</a>
|
||||
<a href="/datenschutz.html" aria-current="page">Datenschutz</a>
|
||||
<a class="lang" href="/privacy.html" hreflang="en" lang="en" title="English">EN</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -26,6 +29,7 @@
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<div class="card">
|
||||
|
||||
|
||||
<p class="intro">
|
||||
Der Text, den Sie auf dieser Seite einfügen, wird ausschließlich in Ihrem Browser
|
||||
umgewandelt. Er wird nicht übertragen, nicht gespeichert und nicht ausgewertet. Es gibt
|
||||
@@ -64,6 +68,12 @@
|
||||
Auf Smartphones übergibt „Speichern" die Datei an das Teilen-Menü Ihres Betriebssystems; wohin
|
||||
Sie sie von dort weitergeben, entscheiden Sie selbst.
|
||||
</p>
|
||||
<p>
|
||||
Das Aufräumen eingefügter Webseiten und die Aufbereitung von Reddit-Threads laufen ebenfalls
|
||||
vollständig in Ihrem Browser. Der optionale Knopf „In Obsidian öffnen" legt den Text in die
|
||||
Zwischenablage und übergibt ihn an die Obsidian-App auf Ihrem Gerät; an uns oder Dritte wird
|
||||
dabei nichts übertragen.
|
||||
</p>
|
||||
|
||||
<h2>3. Lokale Speicherung Ihrer Einstellungen</h2>
|
||||
<p>
|
||||
@@ -74,6 +84,13 @@
|
||||
eingefügten Inhalte. Es handelt sich nicht um Cookies. Sie können die Werte jederzeit über
|
||||
die Einstellungen Ihres Browsers löschen.
|
||||
</p>
|
||||
<p>
|
||||
Nur wenn Sie in den Einstellungen „Letzte 10 Clips behalten" einschalten, speichert die Seite
|
||||
die zuletzt bearbeiteten Texte in der lokalen Datenbank Ihres Browsers (<span class="mono">IndexedDB</span>).
|
||||
Die Funktion ist standardmäßig ausgeschaltet. Die Einträge verlassen Ihr Gerät nicht, sind nur in
|
||||
diesem Browser sichtbar und lassen sich im Verlauf einzeln oder vollständig löschen; beim
|
||||
Ausschalten werden Sie gefragt, ob der Verlauf gelöscht werden soll.
|
||||
</p>
|
||||
<p>
|
||||
Rechtsgrundlage für diese ausschließlich lokale Speicherung ist Ihr Wunsch nach einer
|
||||
funktionsfähigen Bedienoberfläche; ein Zugriff unsererseits findet nicht statt.
|
||||
@@ -132,12 +149,12 @@
|
||||
vermeiden möchten, lassen Sie die Vorschau ausgeschaltet.
|
||||
</p>
|
||||
|
||||
<h2>8. Spenden über PayPal</h2>
|
||||
<h2>8. Unterstützung über PayPal</h2>
|
||||
<p>
|
||||
Auf der Seite finden Sie einen Link „Spenden" bzw. „Buy me a coffee". Es handelt sich um einen
|
||||
einfachen Link; beim Aufruf von clip2md werden keine Inhalte, Skripte oder Cookies von PayPal
|
||||
geladen und keine Daten an PayPal übertragen. Erst wenn Sie den Link anklicken, verlassen Sie
|
||||
diese Seite und werden zu PayPal weitergeleitet.
|
||||
Auf der Seite finden Sie einen Link „Spenden" bzw. „Spendier mir einen Kaffee". Es handelt sich
|
||||
um einen einfachen Link; beim Aufruf von clip2md werden keine Inhalte, Skripte oder Cookies von
|
||||
PayPal geladen und keine Daten an PayPal übertragen. Erst wenn Sie den Link anklicken, verlassen
|
||||
Sie diese Seite und gelangen zu einer Zahlungsseite von PayPal.
|
||||
</p>
|
||||
<p>
|
||||
Anbieter ist die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449
|
||||
@@ -146,11 +163,13 @@
|
||||
<a href="https://www.paypal.com/de/legalhub/privacy-full" rel="noopener nofollow">paypal.com/de/legalhub/privacy-full</a>.
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie spenden, erhalten wir von PayPal die zur Abwicklung nötigen Angaben: Ihren Namen, Ihre
|
||||
E-Mail-Adresse, den Betrag, das Datum und gegebenenfalls eine Nachricht, die Sie hinzufügen.
|
||||
Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Abwicklung der Zuwendung) sowie Art. 6 Abs. 1
|
||||
lit. c DSGVO in Verbindung mit den steuer- und handelsrechtlichen Aufbewahrungspflichten
|
||||
(in der Regel zehn Jahre). Eine weitere Nutzung, etwa für Werbung, findet nicht statt.
|
||||
Wenn Sie einen Betrag zahlen, erhalten wir von PayPal die zur Abwicklung nötigen Angaben,
|
||||
insbesondere Ihren Namen, Ihre E-Mail-Adresse, den Betrag, Datum und Uhrzeit sowie die
|
||||
Transaktionsnummer. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Abwicklung der Zahlung)
|
||||
sowie Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit den steuer- und handelsrechtlichen
|
||||
Aufbewahrungspflichten (in der Regel acht bis zehn Jahre). Eine weitere Nutzung, etwa für
|
||||
Werbung, findet nicht statt. Die Zahlung ist freiwillig; eine Gegenleistung ist damit nicht
|
||||
verbunden.
|
||||
</p>
|
||||
|
||||
<h2>9. Ihre Rechte</h2>
|
||||
@@ -174,7 +193,7 @@
|
||||
<div class="legal-foot">
|
||||
<a href="/">← Zurück zu clip2md</a>
|
||||
<a href="/impressum.html">Impressum</a>
|
||||
<a href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&currency_code=EUR&item_name=clip2md" target="_blank" rel="noopener nofollow">Spenden</a>
|
||||
<a href="https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ" target="_blank" rel="noopener nofollow">Spenden</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# iPhone shortcut for clip2md
|
||||
|
||||
iOS does not let web apps into the share sheet. This Shortcut copies the current Safari page and opens clip2md.
|
||||
|
||||
1. Open **Shortcuts**, tap **+**, name it **clip2md**.
|
||||
2. Tap **ⓘ**, enable **Show in Share Sheet**, allow **Safari web pages**, **Rich text**, **Text**.
|
||||
3. Add **Get Details of Safari Web Page** → **Page Contents** (input: Shortcut Input).
|
||||
4. Add **Copy to Clipboard**.
|
||||
5. Add **Open URLs** → `https://clip2md.heidrich-digital.de/?from=ios` (use your own domain when self-hosting).
|
||||
|
||||
In Safari: Share → **clip2md** → tap **Paste**. iOS asks once whether clip2md may paste.
|
||||
|
||||
For a selection only, no Shortcut is needed: copy it and use the Paste button.
|
||||
|
||||
---
|
||||
|
||||
## Deutsch
|
||||
|
||||
1. App **Kurzbefehle** öffnen, **+** tippen, Name **clip2md**.
|
||||
2. **ⓘ** tippen, **Im Share-Sheet anzeigen** einschalten, **Safari-Webseiten**, **Rich Text**, **Text** zulassen.
|
||||
3. Aktion **Details von Safari-Webseite abrufen** → **Seiteninhalt**.
|
||||
4. Aktion **In Zwischenablage kopieren**.
|
||||
5. Aktion **URLs öffnen** → `https://clip2md.heidrich-digital.de/?from=ios`
|
||||
|
||||
In Safari: Teilen → **clip2md** → **Einfügen** tippen.
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -8,6 +8,8 @@
|
||||
<title>Impressum — clip2md</title>
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/legal.css" />
|
||||
<link rel="alternate" hreflang="de" href="https://clip2md.heidrich-digital.de/impressum.html" />
|
||||
<link rel="alternate" hreflang="en" href="https://clip2md.heidrich-digital.de/imprint.html" />
|
||||
<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>
|
||||
@@ -18,6 +20,7 @@
|
||||
<nav>
|
||||
<a href="/impressum.html" aria-current="page">Impressum</a>
|
||||
<a href="/datenschutz.html">Datenschutz</a>
|
||||
<a class="lang" href="/imprint.html" hreflang="en" lang="en" title="English">EN</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -26,6 +29,7 @@
|
||||
<h1>Impressum</h1>
|
||||
<div class="card">
|
||||
|
||||
|
||||
<h2>Angaben gemäß § 5 DDG</h2>
|
||||
<table class="legal-table">
|
||||
<tbody>
|
||||
@@ -65,7 +69,7 @@
|
||||
<h2>Urheberrecht und Lizenz</h2>
|
||||
<p>
|
||||
Der Quellcode von clip2md steht unter der MIT-Lizenz. Die eingebundenen Bibliotheken
|
||||
(Turndown, Marked, DOMPurify, MIT- bzw. Apache-Lizenz) und die Schriftfamilien Space Grotesk und Space Mono stehen unter ihren jeweiligen
|
||||
(Turndown, Marked, DOMPurify, Mozilla Readability; MIT- bzw. Apache-Lizenz) und die Schriftfamilien Space Grotesk und Space Mono stehen unter ihren jeweiligen
|
||||
freien Lizenzen (Schriften: SIL Open Font License). Texte und Gestaltung dieser Seite unterliegen dem deutschen Urheberrecht.
|
||||
</p>
|
||||
|
||||
@@ -73,7 +77,7 @@
|
||||
<div class="legal-foot">
|
||||
<a href="/">← Zurück zu clip2md</a>
|
||||
<a href="/datenschutz.html">Datenschutzerklärung</a>
|
||||
<a href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&currency_code=EUR&item_name=clip2md" target="_blank" rel="noopener nofollow">Spenden</a>
|
||||
<a href="https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ" target="_blank" rel="noopener nofollow">Spenden</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<!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="robots" content="noindex" />
|
||||
<meta name="theme-color" content="#EAEAE3" />
|
||||
<title>Legal notice — clip2md</title>
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/legal.css" />
|
||||
<link rel="alternate" hreflang="de" href="https://clip2md.heidrich-digital.de/impressum.html" />
|
||||
<link rel="alternate" hreflang="en" href="https://clip2md.heidrich-digital.de/imprint.html" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><svg viewBox="0 0 24 24" aria-hidden="true"><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"/></svg><span>clip<span class="two">2</span>md</span></a>
|
||||
<nav>
|
||||
<a href="/imprint.html" aria-current="page">Legal notice</a>
|
||||
<a href="/privacy.html">Privacy</a>
|
||||
<a class="lang" href="/impressum.html" hreflang="de" lang="de" title="Deutsch">DE</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="legal">
|
||||
<span class="mono-label">Legal</span>
|
||||
<h1>Legal notice</h1>
|
||||
<div class="card">
|
||||
<p class="intro">This is a translation provided for convenience. The German version (<a href="/impressum.html">Impressum</a>) is legally binding.</p>
|
||||
|
||||
<h2>Information pursuant to § 5 DDG (German Digital Services Act)</h2>
|
||||
<table class="legal-table">
|
||||
<tbody>
|
||||
<tr><th>Provider</th><td>Till Heidrich – Content Creation</td></tr>
|
||||
<tr><th>Owner</th><td>Till Heidrich</td></tr>
|
||||
<tr><th>Address</th><td>Rotdornweg 17<br />21255 Tostedt<br />Germany</td></tr>
|
||||
<tr><th>Email</th><td><a href="mailto:info@tillheidrich.de">info@tillheidrich.de</a></td></tr>
|
||||
<tr><th>Phone</th><td><a href="tel:+4915773953819">+49 1577 3953819</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>VAT</h2>
|
||||
<p>
|
||||
As a small business within the meaning of § 19 (1) of the German VAT Act (UStG), no VAT is
|
||||
charged and therefore no VAT identification number is held.
|
||||
</p>
|
||||
|
||||
<h2>Responsible for content pursuant to § 18 (2) MStV</h2>
|
||||
<p>Till Heidrich, address as above.</p>
|
||||
|
||||
<h2>Dispute resolution</h2>
|
||||
<p>
|
||||
The European Commission provides a platform for online dispute resolution:
|
||||
<a href="https://ec.europa.eu/consumers/odr/" rel="nofollow noopener">ec.europa.eu/consumers/odr</a>.
|
||||
We are neither obliged nor willing to take part in dispute resolution proceedings before a
|
||||
consumer arbitration board.
|
||||
</p>
|
||||
|
||||
<h2>Liability for links</h2>
|
||||
<p>
|
||||
This site contains links to external third-party websites whose content we cannot influence.
|
||||
The respective provider is always responsible for that content. No legal violations were
|
||||
apparent at the time of linking. Should we become aware of any, we will remove such links
|
||||
immediately.
|
||||
</p>
|
||||
|
||||
<h2>Copyright and licence</h2>
|
||||
<p>
|
||||
The source code of clip2md is released under the MIT licence. The bundled libraries
|
||||
(Turndown, Marked, DOMPurify, Mozilla Readability; MIT or Apache licence) and the typefaces
|
||||
Space Grotesk and Space Mono (SIL Open Font License) are subject to their respective free
|
||||
licences. Texts and design of this site are protected by German copyright law.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="legal-foot">
|
||||
<a href="/">← Back to clip2md</a>
|
||||
<a href="/privacy.html">Privacy policy</a>
|
||||
<a href="https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ" target="_blank" rel="noopener nofollow">Donate</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,9 +9,10 @@
|
||||
<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="icon.svg" />
|
||||
<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>
|
||||
@@ -111,6 +112,14 @@
|
||||
.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; }
|
||||
@@ -156,6 +165,7 @@
|
||||
.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); }
|
||||
@@ -206,6 +216,29 @@
|
||||
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); }
|
||||
@@ -270,6 +303,11 @@
|
||||
.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; }
|
||||
@@ -302,7 +340,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -347,6 +385,15 @@
|
||||
<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">
|
||||
@@ -371,10 +418,11 @@
|
||||
<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 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">Impressum</a>
|
||||
<a href="/datenschutz.html">Datenschutz</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>
|
||||
|
||||
@@ -411,6 +459,32 @@
|
||||
</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">
|
||||
@@ -420,12 +494,25 @@
|
||||
</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";
|
||||
@@ -433,7 +520,18 @@
|
||||
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',
|
||||
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',
|
||||
@@ -452,7 +550,18 @@
|
||||
},
|
||||
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',
|
||||
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',
|
||||
@@ -487,7 +596,8 @@
|
||||
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')
|
||||
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);
|
||||
|
||||
@@ -523,7 +633,7 @@
|
||||
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(); }
|
||||
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'); }
|
||||
@@ -557,7 +667,7 @@
|
||||
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(); }
|
||||
function refreshAll(flash){ updateCounts(); refreshFilename(flash); updateEmpty(); schedulePreview(); scheduleHistory(); syncHeadButtons(); }
|
||||
|
||||
// ---------- Insert ----------
|
||||
function insertAtCursor(str){
|
||||
@@ -568,14 +678,40 @@
|
||||
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;
|
||||
var out, key, ok=false, changed=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; } }
|
||||
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;
|
||||
insertAtCursor(out); showSource(key, ok); refreshAll(true); return true; }
|
||||
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;
|
||||
@@ -583,7 +719,7 @@
|
||||
if(!html && !text) return;
|
||||
e.preventDefault(); ingest(html, text, raw);
|
||||
});
|
||||
editor.addEventListener('input', function(){ hideSource(); refreshAll(false); });
|
||||
editor.addEventListener('input', function(){ hideSource(); rawBtn.hidden=true; lastInsert=null; refreshAll(false); });
|
||||
|
||||
// ---------- One-click paste (Async Clipboard API) ----------
|
||||
function pasteFromClipboard(){
|
||||
@@ -619,9 +755,9 @@
|
||||
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); } }
|
||||
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);
|
||||
editor.value=txt;
|
||||
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);
|
||||
@@ -671,7 +807,7 @@
|
||||
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(); });
|
||||
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;
|
||||
@@ -691,6 +827,8 @@
|
||||
// ---------- 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'))); });
|
||||
@@ -698,14 +836,17 @@
|
||||
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'); });
|
||||
function setLang(l){ lang=l; applyLang(); }
|
||||
$('langBtn').addEventListener('click', function(){ var l=lang==='de'?'en':'de'; LS.set('langChoice', l); setLang(l); });
|
||||
|
||||
// ---------- 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(); });
|
||||
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(); }); }
|
||||
@@ -723,13 +864,119 @@
|
||||
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);
|
||||
var navLang=(navigator.language||'en').slice(0,2).toLowerCase();
|
||||
setLang(LS.get('lang', navLang==='de'?'de':'en'));
|
||||
// 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(){}); }); }
|
||||
|
||||
@@ -62,3 +62,12 @@ table.legal-table td { padding: 0.3rem 0; vertical-align: top; overflow-wrap: an
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; } }
|
||||
.card > :first-child { margin-top: 18px; }
|
||||
.topbar nav a.lang { font-family: var(--mono); font-size: 0.72rem; letter-spacing: .06em; border: 1px solid var(--line); margin-left: 4px; }
|
||||
.topbar nav a.lang:hover { color: var(--accent); border-color: var(--accent); background: transparent; }
|
||||
@media (max-width: 560px) {
|
||||
.topbar { gap: 8px; padding-left: 12px; padding-right: 12px; }
|
||||
.brand { font-size: 1.05rem; }
|
||||
.topbar nav { flex-wrap: nowrap; gap: 2px; }
|
||||
.topbar nav a { padding: 0 8px; font-size: 0.8rem; }
|
||||
.topbar nav a.lang { padding: 0 7px; margin-left: 2px; }
|
||||
}
|
||||
@@ -8,7 +8,38 @@
|
||||
"background_color": "#EAEAE3",
|
||||
"theme_color": "#EAEAE3",
|
||||
"icons": [
|
||||
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
|
||||
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<!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="robots" content="noindex" />
|
||||
<meta name="theme-color" content="#EAEAE3" />
|
||||
<title>Privacy policy — clip2md</title>
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/legal.css" />
|
||||
<link rel="alternate" hreflang="de" href="https://clip2md.heidrich-digital.de/datenschutz.html" />
|
||||
<link rel="alternate" hreflang="en" href="https://clip2md.heidrich-digital.de/privacy.html" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><svg viewBox="0 0 24 24" aria-hidden="true"><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"/></svg><span>clip<span class="two">2</span>md</span></a>
|
||||
<nav>
|
||||
<a href="/imprint.html">Legal notice</a>
|
||||
<a href="/privacy.html" aria-current="page">Privacy</a>
|
||||
<a class="lang" href="/datenschutz.html" hreflang="de" lang="de" title="Deutsch">DE</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="legal">
|
||||
<span class="mono-label">Legal</span>
|
||||
<h1>Privacy policy</h1>
|
||||
<div class="card">
|
||||
<p class="intro">This is a translation provided for convenience. The German version (<a href="/datenschutz.html">Datenschutzerklärung</a>) is legally binding.</p>
|
||||
|
||||
<p>
|
||||
The text you paste on this site is converted exclusively in your browser. It is not
|
||||
transmitted, stored or analysed. There are no cookies, no analytics, no embedded third-party
|
||||
services and no externally loaded fonts. Only the technical access data that arises whenever a
|
||||
website is requested is collected.
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>
|
||||
Till Heidrich – Content Creation<br />
|
||||
Till Heidrich<br />
|
||||
Rotdornweg 17, 21255 Tostedt, Germany<br />
|
||||
Email: <a href="mailto:info@tillheidrich.de">info@tillheidrich.de</a><br />
|
||||
Phone: <a href="tel:+4915773953819">+49 1577 3953819</a>
|
||||
</p>
|
||||
<p>There is no legal requirement to appoint a data protection officer.</p>
|
||||
|
||||
<h2>2. Processing of pasted content</h2>
|
||||
<p>
|
||||
Converting formatted text into Markdown happens entirely client-side, by JavaScript in your
|
||||
browser. Content you paste never leaves your device and is discarded when you close or reload
|
||||
the page. To that extent, we do not process any personal data within the meaning of the GDPR.
|
||||
</p>
|
||||
<p>
|
||||
Saving the result as a <code>.md</code> file and copying it to the clipboard also happen locally
|
||||
through the corresponding browser interfaces. No data is sent to us.
|
||||
</p>
|
||||
<p>
|
||||
The “Paste” button reads your device's clipboard. This only happens when you press the button
|
||||
and, depending on the browser, only after you have given permission. As with pasting via the
|
||||
keyboard, the content is converted locally only. On smartphones, “Save” hands the file to your
|
||||
operating system's share sheet; where you pass it on from there is up to you.
|
||||
</p>
|
||||
<p>
|
||||
Cleaning up pasted web pages and formatting Reddit threads also run entirely in your browser.
|
||||
The optional “Open in Obsidian” button places the text on the clipboard and hands it to the
|
||||
Obsidian app on your device; nothing is transmitted to us or third parties.
|
||||
</p>
|
||||
|
||||
<h2>3. Local storage of your settings</h2>
|
||||
<p>
|
||||
So that the site remembers your preferences, it stores values in your browser's local storage
|
||||
(<span class="mono">localStorage</span>), such as your chosen language, light or dark
|
||||
appearance and your conversion settings. These values stay on your device, are not transmitted
|
||||
to us and contain none of the content you paste. They are not cookies. You can delete them at
|
||||
any time in your browser settings.
|
||||
</p>
|
||||
<p>
|
||||
Only if you switch on “Keep the last 10 clips” in the settings does the site store the most
|
||||
recently edited texts in your browser's local database (<span class="mono">IndexedDB</span>).
|
||||
This feature is off by default. The entries never leave your device, are only visible in this
|
||||
browser and can be deleted individually or all at once in the history; when you switch the
|
||||
feature off, you are asked whether the history should be deleted.
|
||||
</p>
|
||||
<p>
|
||||
The basis for this purely local storage is your wish for a working user interface; we have no
|
||||
access to it.
|
||||
</p>
|
||||
|
||||
<h2>4. Offline use (Progressive Web App)</h2>
|
||||
<p>
|
||||
The site registers a service worker that stores the required program files in your browser's
|
||||
cache, so clip2md also works without an internet connection. Only program files of this site
|
||||
are cached, never your content. You can remove the cache via your browser's site data.
|
||||
</p>
|
||||
|
||||
<h2>5. Server log files</h2>
|
||||
<p>When this site is requested, the hosting provider automatically stores data in log files:</p>
|
||||
<ul>
|
||||
<li>IP address of the requesting device</li>
|
||||
<li>date and time of access</li>
|
||||
<li>name and URL of the requested file</li>
|
||||
<li>amount of data transferred and whether the request succeeded</li>
|
||||
<li>browser and operating system used</li>
|
||||
<li>where applicable, the previously visited page (referrer)</li>
|
||||
</ul>
|
||||
<p>
|
||||
The legal basis is Art. 6 (1) (f) GDPR. Our legitimate interest lies in the technically
|
||||
error-free operation and security of the service. The data is deleted after seven days at the
|
||||
latest and is not combined with other data sources.
|
||||
</p>
|
||||
|
||||
<h2>6. Hosting</h2>
|
||||
<p>
|
||||
This site is operated on servers of Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen,
|
||||
Germany. A data processing agreement pursuant to Art. 28 GDPR is in place with the provider.
|
||||
Processing takes place exclusively within the European Union. Data transfer to this site is
|
||||
encrypted via HTTPS.
|
||||
</p>
|
||||
|
||||
<h2>7. No cookies, no tracking</h2>
|
||||
<p>
|
||||
This site sets no cookies and uses no analytics, advertising or audience measurement services.
|
||||
No content is loaded from third-party servers, in particular no fonts and no scripts from
|
||||
content delivery networks. A consent banner is therefore not required.
|
||||
</p>
|
||||
<p>
|
||||
One exception concerns the optional preview: if the text you paste contains images hosted on
|
||||
third-party servers, your browser loads those images from there when the preview is shown. The
|
||||
respective server learns your IP address in the process. None of your text is transmitted, and
|
||||
the origin is not sent thanks to <span class="mono">Referrer-Policy: no-referrer</span>. If you
|
||||
want to avoid this, keep the preview switched off.
|
||||
</p>
|
||||
|
||||
<h2>8. Support via PayPal</h2>
|
||||
<p>
|
||||
The site contains a “Donate” or “Buy me a coffee” link. It is a plain link; when you open
|
||||
clip2md, no content, scripts or cookies are loaded from PayPal and no data is sent to PayPal.
|
||||
Only when you click the link do you leave this site and reach a PayPal payment page.
|
||||
</p>
|
||||
<p>
|
||||
The provider is PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449
|
||||
Luxembourg. PayPal itself is responsible for processing on its pages; its privacy statement
|
||||
applies:
|
||||
<a href="https://www.paypal.com/de/legalhub/privacy-full" rel="noopener nofollow">paypal.com/de/legalhub/privacy-full</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you pay an amount, PayPal provides us with the details needed to process it, in particular
|
||||
your name, email address, the amount, date and time, and the transaction number. The legal basis
|
||||
is Art. 6 (1) (b) GDPR (processing the payment) and Art. 6 (1) (c) GDPR in conjunction with
|
||||
statutory tax and commercial retention obligations (usually eight to ten years). The data is not
|
||||
used for any other purpose, such as advertising. The payment is voluntary; nothing is provided
|
||||
in return.
|
||||
</p>
|
||||
|
||||
<h2>9. Your rights</h2>
|
||||
<p>
|
||||
You have the right of access (Art. 15 GDPR), rectification (Art. 16), erasure (Art. 17),
|
||||
restriction of processing (Art. 18), data portability (Art. 20) and the right to object to
|
||||
processing based on Art. 6 (1) (f) GDPR (Art. 21). To exercise these rights, please contact the
|
||||
address above.
|
||||
</p>
|
||||
|
||||
<h2>10. Right to lodge a complaint</h2>
|
||||
<p>
|
||||
You may lodge a complaint with a data protection supervisory authority. The competent authority
|
||||
is the State Commissioner for Data Protection of Lower Saxony (Die Landesbeauftragte für den
|
||||
Datenschutz Niedersachsen), Prinzenstraße 5, 30159 Hannover, Germany.
|
||||
</p>
|
||||
|
||||
<h2>11. Last updated</h2>
|
||||
<p>This privacy policy was last updated in September 2026.</p>
|
||||
|
||||
</div>
|
||||
<div class="legal-foot">
|
||||
<a href="/">← Back to clip2md</a>
|
||||
<a href="/imprint.html">Legal notice</a>
|
||||
<a href="https://www.paypal.com/ncp/payment/54FPXDFBMP5TJ" target="_blank" rel="noopener nofollow">Donate</a>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,19 +2,25 @@
|
||||
Pages (navigations, .html): network first, cache only as offline fallback —
|
||||
so new or changed pages are never frozen for existing users.
|
||||
Static assets (vendor, fonts, icons): cache first. Bump CACHE on asset changes. */
|
||||
var CACHE = 'clip2md-v4';
|
||||
var CACHE = 'clip2md-v8';
|
||||
var ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/impressum.html',
|
||||
'/datenschutz.html',
|
||||
'/imprint.html',
|
||||
'/privacy.html',
|
||||
'/legal.css',
|
||||
'/icon.svg',
|
||||
'/icon-192.png',
|
||||
'/apple-touch-icon.png',
|
||||
'/manifest.webmanifest',
|
||||
'/vendor/turndown.js',
|
||||
'/vendor/turndown-plugin-gfm.js',
|
||||
'/vendor/marked.umd.js',
|
||||
'/vendor/purify.min.js',
|
||||
'/vendor/Readability.js',
|
||||
'/clean.js',
|
||||
'/vendor/fonts/space-grotesk-latin-400-normal.woff2',
|
||||
'/vendor/fonts/space-grotesk-latin-500-normal.woff2',
|
||||
'/vendor/fonts/space-grotesk-latin-700-normal.woff2',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2010 Arc90 Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||