Compare commits

..
7 Commits
Author SHA1 Message Date
till f543903654 README-Screenshots aktualisiert (neues Design, Reddit-Aufbereitung, Spenden) 2026-09-16 12:52:56 +02:00
till 0918ee0a65 Impressum: Sprachumschalter und hreflang nachgereicht 2026-09-16 12:50:22 +02:00
till 70e6d03005 App-Sprache folgt Browsersprache, Footer-Links je Sprache (index.html nachgereicht) 2026-09-16 12:50:11 +02:00
till a1a58531d8 Rechtstexte auf Englisch (imprint, privacy), App-Sprache folgt Browsersprache, SW-Cache v8
- Sprache: explizite Wahl per Knopf wird gemerkt (langChoice), sonst navigator.languages (de/en, Rest englisch); alter lang-Schlüssel wird ignoriert
- Footer-Links zeigen je nach Sprache auf Impressum/Datenschutz oder Legal notice/Privacy
- Rechtstexte mit Sprachumschalter und hreflang; englische Fassung mit Hinweis, dass die deutsche verbindlich ist
2026-09-16 12:48:35 +02:00
till b955d857bf Spenden über PayPal-Zahlungslink wieder aktiv (DE/EN), Datenschutz §8, SW-Cache v7 2026-09-16 12:43:51 +02:00
till b00633af0f Aufräumen, Reddit-Aufbereitung, Obsidian, Verlauf, iPhone-Kurzbefehl; Spenden vorerst aus
- clean.js: Webseiten aufräumen (Mozilla Readability für Artikel, sonst Navigation/Cookie-Banner/Knöpfe entfernen), Knopf 'Ohne Aufräumen'
- Reddit (neu und alt): Titel, Subreddit, Autor, keine Votes/Reply-Reste, Kommentare als verschachtelte Zitate mit (OP)
- Optionaler Knopf 'In Obsidian öffnen' (obsidian://new, Vault/Ordner einstellbar, Text über Zwischenablage)
- Optionaler Verlauf der letzten 10 Clips in IndexedDB, standardmäßig aus
- Anleitung iPhone-Kurzbefehl in der App und docs/ios-shortcut.md, ?from=ios weist auf Einfügen hin
- Spenden-Links ausgeblendet, bis ein funktionierender Link hinterlegt ist; PayPal-Abschnitt aus der Datenschutzerklärung entfernt
- Datenschutz um Verlauf und Obsidian ergänzt, SW-Cache v6
2026-09-16 12:39:26 +02:00
till 13eabc6247 PNG-Icons für iOS/Android, Teilen-Ziel auf Android, SW-Cache v5 2026-09-16 12:27:49 +02:00
20 changed files with 3631 additions and 50 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ FROM nginx:1.27-alpine
# Static single-page app — no build step. # Static single-page app — no build step.
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY security-headers.conf /etc/nginx/security-headers.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/ COPY vendor/ /usr/share/nginx/html/vendor/
EXPOSE 80 EXPOSE 80
+20 -7
View File
@@ -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. 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 - **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. 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. - **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). - **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`. - **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. - **Bilingual.** English and German, switchable in the header.
- **Light & dark.** Warm paper, one strong blue, clipboard logo; dark mode one click away. - **Light & dark.** Warm paper, one strong blue, clipboard logo; dark mode one click away.
- **Save / copy / clear**, word + character count and reading time. - **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 Everything is **client-side**. No servers, no CDNs, no third-party requests — GDPR-friendly by
construction. construction.
@@ -78,20 +89,22 @@ Any static host works — point it at this repo. The reference deployment uses
## Update vendored libraries ## Update vendored libraries
```bash ```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/dist/turndown.js -> vendor/turndown.js
# turndown-plugin-gfm/dist/turndown-plugin-gfm.js -> vendor/turndown-plugin-gfm.js # turndown-plugin-gfm/dist/turndown-plugin-gfm.js -> vendor/turndown-plugin-gfm.js
# marked/lib/marked.umd.js -> vendor/marked.umd.js # marked/lib/marked.umd.js -> vendor/marked.umd.js
# dompurify/dist/purify.min.js -> vendor/purify.min.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/ # @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. 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 ## Support
clip2md is free. If it saves you time: [buy me a coffee via PayPal](https://www.paypal.com/donate/?business=info%40tillheidrich.de&currency_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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+159
View File
@@ -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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 };
})();
+30 -11
View File
@@ -8,6 +8,8 @@
<title>Datenschutzerklärung — clip2md</title> <title>Datenschutzerklärung — clip2md</title>
<link rel="icon" href="/icon.svg" type="image/svg+xml" /> <link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/legal.css" /> <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> <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) {} 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> </script>
@@ -18,6 +20,7 @@
<nav> <nav>
<a href="/impressum.html">Impressum</a> <a href="/impressum.html">Impressum</a>
<a href="/datenschutz.html" aria-current="page">Datenschutz</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> </nav>
</header> </header>
@@ -26,6 +29,7 @@
<h1>Datenschutzerklärung</h1> <h1>Datenschutzerklärung</h1>
<div class="card"> <div class="card">
<p class="intro"> <p class="intro">
Der Text, den Sie auf dieser Seite einfügen, wird ausschließlich in Ihrem Browser 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 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 Auf Smartphones übergibt „Speichern" die Datei an das Teilen-Menü Ihres Betriebssystems; wohin
Sie sie von dort weitergeben, entscheiden Sie selbst. Sie sie von dort weitergeben, entscheiden Sie selbst.
</p> </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> <h2>3. Lokale Speicherung Ihrer Einstellungen</h2>
<p> <p>
@@ -74,6 +84,13 @@
eingefügten Inhalte. Es handelt sich nicht um Cookies. Sie können die Werte jederzeit über eingefügten Inhalte. Es handelt sich nicht um Cookies. Sie können die Werte jederzeit über
die Einstellungen Ihres Browsers löschen. die Einstellungen Ihres Browsers löschen.
</p> </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> <p>
Rechtsgrundlage für diese ausschließlich lokale Speicherung ist Ihr Wunsch nach einer Rechtsgrundlage für diese ausschließlich lokale Speicherung ist Ihr Wunsch nach einer
funktionsfähigen Bedienoberfläche; ein Zugriff unsererseits findet nicht statt. funktionsfähigen Bedienoberfläche; ein Zugriff unsererseits findet nicht statt.
@@ -132,12 +149,12 @@
vermeiden möchten, lassen Sie die Vorschau ausgeschaltet. vermeiden möchten, lassen Sie die Vorschau ausgeschaltet.
</p> </p>
<h2>8. Spenden über PayPal</h2> <h2>8. Unterstützung über PayPal</h2>
<p> <p>
Auf der Seite finden Sie einen Link „Spenden" bzw. „Buy me a coffee". Es handelt sich um einen Auf der Seite finden Sie einen Link „Spenden" bzw. „Spendier mir einen Kaffee". Es handelt sich
einfachen Link; beim Aufruf von clip2md werden keine Inhalte, Skripte oder Cookies von PayPal um einen einfachen Link; beim Aufruf von clip2md werden keine Inhalte, Skripte oder Cookies von
geladen und keine Daten an PayPal übertragen. Erst wenn Sie den Link anklicken, verlassen Sie PayPal geladen und keine Daten an PayPal übertragen. Erst wenn Sie den Link anklicken, verlassen
diese Seite und werden zu PayPal weitergeleitet. Sie diese Seite und gelangen zu einer Zahlungsseite von PayPal.
</p> </p>
<p> <p>
Anbieter ist die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449 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>. <a href="https://www.paypal.com/de/legalhub/privacy-full" rel="noopener nofollow">paypal.com/de/legalhub/privacy-full</a>.
</p> </p>
<p> <p>
Wenn Sie spenden, erhalten wir von PayPal die zur Abwicklung nötigen Angaben: Ihren Namen, Ihre Wenn Sie einen Betrag zahlen, erhalten wir von PayPal die zur Abwicklung nötigen Angaben,
E-Mail-Adresse, den Betrag, das Datum und gegebenenfalls eine Nachricht, die Sie hinzufügen. insbesondere Ihren Namen, Ihre E-Mail-Adresse, den Betrag, Datum und Uhrzeit sowie die
Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Abwicklung der Zuwendung) sowie Art. 6 Abs. 1 Transaktionsnummer. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Abwicklung der Zahlung)
lit. c DSGVO in Verbindung mit den steuer- und handelsrechtlichen Aufbewahrungspflichten sowie Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit den steuer- und handelsrechtlichen
(in der Regel zehn Jahre). Eine weitere Nutzung, etwa für Werbung, findet nicht statt. 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> </p>
<h2>9. Ihre Rechte</h2> <h2>9. Ihre Rechte</h2>
@@ -174,7 +193,7 @@
<div class="legal-foot"> <div class="legal-foot">
<a href="/">← Zurück zu clip2md</a> <a href="/">← Zurück zu clip2md</a>
<a href="/impressum.html">Impressum</a> <a href="/impressum.html">Impressum</a>
<a href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&amp;currency_code=EUR&amp;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> </div>
</main> </main>
</body> </body>
+25
View File
@@ -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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 193 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+6 -2
View File
@@ -8,6 +8,8 @@
<title>Impressum — clip2md</title> <title>Impressum — clip2md</title>
<link rel="icon" href="/icon.svg" type="image/svg+xml" /> <link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/legal.css" /> <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> <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) {} 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> </script>
@@ -18,6 +20,7 @@
<nav> <nav>
<a href="/impressum.html" aria-current="page">Impressum</a> <a href="/impressum.html" aria-current="page">Impressum</a>
<a href="/datenschutz.html">Datenschutz</a> <a href="/datenschutz.html">Datenschutz</a>
<a class="lang" href="/imprint.html" hreflang="en" lang="en" title="English">EN</a>
</nav> </nav>
</header> </header>
@@ -26,6 +29,7 @@
<h1>Impressum</h1> <h1>Impressum</h1>
<div class="card"> <div class="card">
<h2>Angaben gemäß § 5 DDG</h2> <h2>Angaben gemäß § 5 DDG</h2>
<table class="legal-table"> <table class="legal-table">
<tbody> <tbody>
@@ -65,7 +69,7 @@
<h2>Urheberrecht und Lizenz</h2> <h2>Urheberrecht und Lizenz</h2>
<p> <p>
Der Quellcode von clip2md steht unter der MIT-Lizenz. Die eingebundenen Bibliotheken 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. freien Lizenzen (Schriften: SIL Open Font License). Texte und Gestaltung dieser Seite unterliegen dem deutschen Urheberrecht.
</p> </p>
@@ -73,7 +77,7 @@
<div class="legal-foot"> <div class="legal-foot">
<a href="/">← Zurück zu clip2md</a> <a href="/">← Zurück zu clip2md</a>
<a href="/datenschutz.html">Datenschutzerklärung</a> <a href="/datenschutz.html">Datenschutzerklärung</a>
<a href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&amp;currency_code=EUR&amp;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> </div>
</main> </main>
</body> </body>
+85
View File
@@ -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>
+271 -24
View File
@@ -9,9 +9,10 @@
<meta name="apple-mobile-web-app-title" content="clip2md" /> <meta name="apple-mobile-web-app-title" content="clip2md" />
<link rel="manifest" href="manifest.webmanifest" /> <link rel="manifest" href="manifest.webmanifest" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" /> <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> <title>clip2md — Paste to Markdown</title>
<script> <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) {} 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> </script>
<style> <style>
@@ -111,6 +112,14 @@
.pill.donate { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); } .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:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.pill.donate svg { width: 16px; height: 16px; } .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 ---------- */ /* ---------- Action bar ---------- */
.bar { display: flex; align-items: stretch; gap: 8px; padding: 16px var(--gut) 12px; flex-wrap: wrap; } .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::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 .regmark::after { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); }
.pane-head .spacer { flex: 1; } .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; } main:not(.split) .preview-pane { display: none; }
.editor-pane:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); } .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); } 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 { color: var(--ink-soft); text-decoration: none; padding: 6px 0; }
footer nav a:hover { color: var(--accent); } footer nav a:hover { color: var(--accent); }
footer nav a.coffee { color: var(--accent); font-weight: 500; } 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 ---------- */ /* ---------- 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 { 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); } .pop.open { animation: sheet .24s var(--ease); }
@keyframes sheet { from { transform: translateY(40px); opacity: 0; } to { transform: none; opacity: 1; } } @keyframes sheet { from { transform: translateY(40px); opacity: 0; } to { transform: none; opacity: 1; } }
footer { font-size: 0.8rem; } 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) { @media (max-width: 380px) {
.brand { font-size: 1.05rem; } .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> <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>
<button id="langBtn" class="pill lang" title="Deutsch">DE</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&amp;currency_code=EUR&amp;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> <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> <span class="lbl" data-i18n="donate">Donate</span>
</a> </a>
@@ -347,6 +385,15 @@
<span class="regmark" aria-hidden="true"></span><span class="mono-label">Markdown</span> <span class="regmark" aria-hidden="true"></span><span class="mono-label">Markdown</span>
<span class="spacer"></span> <span class="spacer"></span>
<span class="badge mono-label" id="srcBadge" aria-live="polite"><span class="led"></span><span id="srcText"></span></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> </div>
<textarea id="editor" spellcheck="false" autocapitalize="off" autocorrect="off" aria-label="Markdown editor"></textarea> <textarea id="editor" spellcheck="false" autocapitalize="off" autocorrect="off" aria-label="Markdown editor"></textarea>
<div class="empty" id="empty"> <div class="empty" id="empty">
@@ -371,10 +418,11 @@
<span data-i18n="privacy">Runs in your browser. Nothing is uploaded.</span> <span data-i18n="privacy">Runs in your browser. Nothing is uploaded.</span>
<span class="spacer"></span> <span class="spacer"></span>
<nav aria-label="Footer"> <nav aria-label="Footer">
<a class="coffee" href="https://www.paypal.com/donate/?business=info%40tillheidrich.de&amp;currency_code=EUR&amp;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="https://github.com/tillheidrich/clip2md" target="_blank" rel="noopener">GitHub</a>
<a href="/impressum.html">Impressum</a> <a href="/impressum.html" id="lnkImp" data-i18n="l_imp">Impressum</a>
<a href="/datenschutz.html">Datenschutz</a> <a href="/datenschutz.html" id="lnkDs" data-i18n="l_ds">Datenschutz</a>
</nav> </nav>
</footer> </footer>
@@ -411,6 +459,32 @@
</select> </select>
</div> </div>
<div class="sep"></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> <h3 class="mono-label" data-i18n="s_file">Filename</h3>
<div class="field"> <div class="field">
<select id="namePattern" aria-label="Filename pattern"> <select id="namePattern" aria-label="Filename pattern">
@@ -420,12 +494,25 @@
</div> </div>
</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> <div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="vendor/turndown.js"></script> <script src="vendor/turndown.js"></script>
<script src="vendor/turndown-plugin-gfm.js"></script> <script src="vendor/turndown-plugin-gfm.js"></script>
<script src="vendor/marked.umd.js"></script> <script src="vendor/marked.umd.js"></script>
<script src="vendor/purify.min.js"></script> <script src="vendor/purify.min.js"></script>
<script src="vendor/Readability.js"></script>
<script src="clean.js"></script>
<script> <script>
(function () { (function () {
"use strict"; "use strict";
@@ -433,7 +520,18 @@
var T = { var T = {
en: { en: {
tagline: 'Paste → Markdown', preview: 'Preview', file: 'File', save: 'Save', copy: 'Copy', clear: 'Clear', open: 'Open', paste: 'Paste', 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_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.', 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', paste_big: 'Paste from clipboard', k_paste: 'paste', k_save: 'save', k_drop: 'or drop a file',
@@ -452,7 +550,18 @@
}, },
de: { de: {
tagline: 'Einfügen → Markdown', preview: 'Vorschau', file: 'Datei', save: 'Speichern', copy: 'Kopieren', clear: 'Leeren', open: 'Öffnen', paste: 'Einfügen', 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_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.', 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', paste_big: 'Aus Zwischenablage einfügen', k_paste: 'einfügen', k_save: 'speichern', k_drop: 'oder Datei hineinziehen',
@@ -487,7 +596,8 @@
var settings = { var settings = {
frontMatter: LS.get('frontMatter', false), source: LS.get('source', ''), tags: LS.get('tags', ''), frontMatter: LS.get('frontMatter', false), source: LS.get('source', ''), tags: LS.get('tags', ''),
stripLinks: LS.get('stripLinks', false), dropImages: LS.get('dropImages', false), 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); 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,''); } 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; 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++){ 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 ''; } return ''; }
function computeFilename(md){ if (settings.pattern === 'datetime') return today()+'-'+nowTime(); function computeFilename(md){ if (settings.pattern === 'datetime') return today()+'-'+nowTime();
var slug = slugify(deriveTitle(md)); return today()+(slug?'-'+slug:'-clip'); } 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'); 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'); } srcBadge.classList.remove('pulse'); void srcBadge.offsetWidth; srcBadge.classList.add('pulse'); }
function hideSource(){ srcBadge.classList.remove('show','ok','pulse'); srcBadge.dataset.key=''; } 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 ---------- // ---------- Insert ----------
function insertAtCursor(str){ function insertAtCursor(str){
@@ -568,14 +678,40 @@
var s=editor.selectionStart, en=editor.selectionEnd; var s=editor.selectionStart, en=editor.selectionEnd;
editor.value=editor.value.slice(0,s)+str+editor.value.slice(en); editor.value=editor.value.slice(0,s)+str+editor.value.slice(en);
editor.selectionStart=editor.selectionEnd=s+str.length; } 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){ function ingest(html, text, raw){
var out, key, ok=false; var out, key, ok=false, changed=false;
if(!raw && html && html.trim()){ 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'; } catch(err){ out=text||''; key='src_fail'; }
} else { out=text||''; key=raw?'src_raw_forced':'src_raw'; } } else { out=text||''; key=raw?'src_raw_forced':'src_raw'; }
if(!out) return false; 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){ editor.addEventListener('paste', function(e){
var cd=e.clipboardData||window.clipboardData; if(!cd) return; var cd=e.clipboardData||window.clipboardData; if(!cd) return;
@@ -583,7 +719,7 @@
if(!html && !text) return; if(!html && !text) return;
e.preventDefault(); ingest(html, text, raw); 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) ---------- // ---------- One-click paste (Async Clipboard API) ----------
function pasteFromClipboard(){ function pasteFromClipboard(){
@@ -619,9 +755,9 @@
var r=new FileReader(); var r=new FileReader();
r.onerror=function(){ toast(t('readfail'), true); }; r.onerror=function(){ toast(t('readfail'), true); };
r.onload=function(){ var txt=String(r.result||''), name=f.name.toLowerCase(); 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); 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; } if(!filenameEdited){ var base=slugify(f.name.replace(/\.[^.]+$/,'')); if(base) filename.value=base; }
refreshAll(true); toast(t('loaded')); }; refreshAll(true); toast(t('loaded')); };
r.readAsText(f); r.readAsText(f);
@@ -671,7 +807,7 @@
try{ document.execCommand('copy'); toast(t('copied')); }catch(e){ toast(t('copyfail'),true); } document.body.removeChild(ta); } 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; $('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 ---------- // ---------- Keyboard ----------
document.addEventListener('keydown', function(e){ var mod=e.metaKey||e.ctrlKey; document.addEventListener('keydown', function(e){ var mod=e.metaKey||e.ctrlKey;
@@ -691,6 +827,8 @@
// ---------- Language ---------- // ---------- Language ----------
function applyLang(){ function applyLang(){
document.documentElement.setAttribute('lang', lang); 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]').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-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'))); }); 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(coarse){ $('empty').querySelector('p').textContent=t('empty_p'); }
if(srcBadge.dataset.key) srcText.textContent=t(srcBadge.dataset.key); if(srcBadge.dataset.key) srcText.textContent=t(srcBadge.dataset.key);
setFnameTag(); updateCounts(); } setFnameTag(); updateCounts(); }
function setLang(l){ lang=l; LS.set('lang',l); applyLang(); } function setLang(l){ lang=l; applyLang(); }
$('langBtn').addEventListener('click', function(){ setLang(lang==='de'?'en':'de'); }); $('langBtn').addEventListener('click', function(){ var l=lang==='de'?'en':'de'; LS.set('langChoice', l); setLang(l); });
// ---------- Settings ---------- // ---------- Settings ----------
function openSettings(){ settingsPop.classList.add('open'); scrim.classList.add('open'); settingsBtn.setAttribute('aria-expanded','true'); } var sheets=[settingsPop, $('histPop'), $('helpPop')];
function closeSettings(){ settingsPop.classList.remove('open'); scrim.classList.remove('open'); settingsBtn.setAttribute('aria-expanded','false'); } 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'); }
settingsBtn.addEventListener('click', function(){ settingsPop.classList.contains('open')?closeSettings():openSettings(); }); 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); 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]; 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(); }); } 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('headingSel','heading', buildTurndown);
bindSelect('namePattern','pattern', function(){ refreshFilename(true); }); bindSelect('namePattern','pattern', function(){ refreshFilename(true); });
syncFmFields(); 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 ---------- // ---------- Init ----------
setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light', false); setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'dark':'light', false);
var navLang=(navigator.language||'en').slice(0,2).toLowerCase(); // Language: an explicit choice (language button) wins; otherwise the browser's preferred languages.
setLang(LS.get('lang', navLang==='de'?'de':'en')); // 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); setPreview(previewOn);
setFnameTag(); refreshAll(false); 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(!coarse) editor.focus();
if('serviceWorker' in navigator){ window.addEventListener('load', function(){ navigator.serviceWorker.register('/sw.js').catch(function(){}); }); } if('serviceWorker' in navigator){ window.addEventListener('load', function(){ navigator.serviceWorker.register('/sw.js').catch(function(){}); }); }
+9
View File
@@ -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; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; } }
.card > :first-child { margin-top: 18px; } .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; }
}
+35 -4
View File
@@ -8,7 +8,38 @@
"background_color": "#EAEAE3", "background_color": "#EAEAE3",
"theme_color": "#EAEAE3", "theme_color": "#EAEAE3",
"icons": [ "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"
}
}
}
+184
View File
@@ -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>
+7 -1
View File
@@ -2,19 +2,25 @@
Pages (navigations, .html): network first, cache only as offline fallback — Pages (navigations, .html): network first, cache only as offline fallback —
so new or changed pages are never frozen for existing users. so new or changed pages are never frozen for existing users.
Static assets (vendor, fonts, icons): cache first. Bump CACHE on asset changes. */ Static assets (vendor, fonts, icons): cache first. Bump CACHE on asset changes. */
var CACHE = 'clip2md-v4'; var CACHE = 'clip2md-v8';
var ASSETS = [ var ASSETS = [
'/', '/',
'/index.html', '/index.html',
'/impressum.html', '/impressum.html',
'/datenschutz.html', '/datenschutz.html',
'/imprint.html',
'/privacy.html',
'/legal.css', '/legal.css',
'/icon.svg', '/icon.svg',
'/icon-192.png',
'/apple-touch-icon.png',
'/manifest.webmanifest', '/manifest.webmanifest',
'/vendor/turndown.js', '/vendor/turndown.js',
'/vendor/turndown-plugin-gfm.js', '/vendor/turndown-plugin-gfm.js',
'/vendor/marked.umd.js', '/vendor/marked.umd.js',
'/vendor/purify.min.js', '/vendor/purify.min.js',
'/vendor/Readability.js',
'/clean.js',
'/vendor/fonts/space-grotesk-latin-400-normal.woff2', '/vendor/fonts/space-grotesk-latin-400-normal.woff2',
'/vendor/fonts/space-grotesk-latin-500-normal.woff2', '/vendor/fonts/space-grotesk-latin-500-normal.woff2',
'/vendor/fonts/space-grotesk-latin-700-normal.woff2', '/vendor/fonts/space-grotesk-latin-700-normal.woff2',
+13
View File
@@ -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.
+2786
View File
File diff suppressed because it is too large. Load diff