Akcja jednym kliknięciem, która kopiuje dowolną stronę jako czysty Markdown
Czytasz coś wartego zachowania i chcesz mieć to w swoich notatkach. Kopiuj-wklej z przeglądarki wciąga menu nawigacji, trzy reklamy, baner cookie i plątaninę stylów inline. To, czego naprawdę chcesz, to czysty Markdown: nagłówek, akapity, linki, bloki kodu — nic więcej. Ten artykuł buduje przycisk akcji JustZix, który robi dokładnie to, jednym kliknięciem.
Dlaczego Markdown i dlaczego przycisk akcji
Markdown to uniwersalny format notatek — wkleja się czysto do Obsidian, Notion, zgłoszeń na GitHubie, Twojego edytora, zwykłego pliku tekstowego. Celem tutaj jest przycisk na pasku akcji JustZix, który konwertuje bieżące zaznaczenie tekstu albo cały artykuł do Markdown i wrzuca go do schowka. Bez DevTools, bez dodatkowej aplikacji, bez ręcznego sprzątania.
Akcja typu BUTTON jest do tego idealna: pokazuje opisany przycisk na pasku akcji, a kliknięcie uruchamia Twój JavaScript. Ten JavaScript napiszemy teraz.
Strategia konwersji
Przechodzimy DOM rekurencyjnie. Dla każdego węzła decydujemy: czy to tekst (wyemituj go), czy element, który umiemy skonwertować (wyemituj dla niego Markdown), czy coś do pominięcia (nawigacja, skrypt, reklamy). Cokolwiek, czego nie rozpoznamy, zagłębiamy się w to — aby nieznane opakowania nie traciły swojej treści. To zachowanie rekurencji jako wartości domyślnej jest awaryjnym przejściem do zwykłego tekstu.
// Elements we never want in the output
const SKIP = new Set([
'SCRIPT', 'STYLE', 'NAV', 'HEADER', 'FOOTER', 'ASIDE',
'NOSCRIPT', 'IFRAME', 'FORM', 'BUTTON', 'SVG'
]);
function isHidden(el) {
const s = getComputedStyle(el);
return s.display === 'none' || s.visibility === 'hidden';
}
Konwersja elementów inline
Konwersja inline obsługuje formatowanie ciągu tekstu: pogrubienie, kursywę, kod, linki. Zwraca ciąg znaków, zagłębiając się w dzieci, aby zagnieżdżone znaczniki inline działały.
function inline(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.replace(/\s+/g, ' ');
}
if (node.nodeType !== Node.ELEMENT_NODE) return '';
if (SKIP.has(node.tagName) || isHidden(node)) return '';
const inner = [...node.childNodes].map(inline).join('');
switch (node.tagName) {
case 'STRONG': case 'B': return '**' + inner.trim() + '**';
case 'EM': case 'I': return '*' + inner.trim() + '*';
case 'CODE': return '`' + inner.trim() + '`';
case 'BR': return ' \n';
case 'A': {
const href = node.getAttribute('href') || '';
const abs = href ? new URL(href, location.href).href : '';
return abs ? '[' + inner.trim() + '](' + abs + ')' : inner;
}
default: return inner;
}
}
Zwróć uwagę na obsługę linków: rozwiązujemy względne href względem location.href za pomocą konstruktora URL, więc skopiowany link nadal działa, gdy wkleisz go gdzie indziej.
Konwersja elementów blokowych
Konwersja blokowa obsługuje elementy strukturalne — nagłówki, akapity, listy, bloki kodu, cytaty blokowe — i łączy je pustymi liniami.
function block(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.trim();
}
if (node.nodeType !== Node.ELEMENT_NODE) return '';
if (SKIP.has(node.tagName) || isHidden(node)) return '';
const t = node.tagName;
if (/^H[1-6]$/.test(t)) {
return '#'.repeat(+t[1]) + ' ' + inline(node).trim();
}
if (t === 'P') return inline(node).trim();
if (t === 'BLOCKQUOTE') {
return inline(node).trim()
.split('\n').map(l => '> ' + l).join('\n');
}
if (t === 'PRE') {
return '```\n' + node.textContent.replace(/\n$/, '') + '\n```';
}
if (t === 'UL' || t === 'OL') {
const ordered = t === 'OL';
return [...node.children]
.filter(li => li.tagName === 'LI')
.map((li, i) => (ordered ? (i + 1) + '. ' : '- ')
+ inline(li).trim())
.join('\n');
}
if (t === 'HR') return '---';
if (t === 'IMG') {
const alt = node.getAttribute('alt') || '';
const src = node.src || '';
return src ? '' : '';
}
// Unknown wrapper: recurse, keep the children
return [...node.childNodes].map(block)
.filter(Boolean).join('\n\n');
}
Ostatnia linia to awaryjne przejście: <div>, <section> albo <article>, dla którego nie mamy specjalnej reguły, po prostu zagłębia się w swoje dzieci. W najgorszym razie egzotyczny element degraduje do swojego zwykłego tekstu — nigdy do niczego.
Najpierw zaznaczenie, potem artykuł
Przycisk powinien być sprytny: jeśli masz zaznaczony tekst, konwertuj tylko jego; w przeciwnym razie konwertuj główny artykuł. API Selection daje nam zakres, który możemy sklonować do fragmentu.
function getRoot() {
const sel = window.getSelection();
if (sel && sel.rangeCount && !sel.isCollapsed) {
const frag = sel.getRangeAt(0).cloneContents();
const wrap = document.createElement('div');
wrap.appendChild(frag);
return wrap;
}
// No selection: best guess at the main content
return document.querySelector(
'article, main, [role="main"], .post, .content'
) || document.body;
}
Sklonowany fragment zaznaczenia może zaczynać się w środku elementu, więc konwersja blokowa może widzieć częściowe węzły — to w porządku, nasza reguła rekurencji jako wartości domyślnej radzi sobie z tym z gracją.
Podpięcie tego do akcji BUTTON i kopiowanie
Teraz złóż elementy, zbuduj końcowy ciąg znaków i zapisz go do schowka. To jest treść Twojej akcji BUTTON.
const root = getRoot();
const md = [...root.childNodes]
.map(block)
.filter(Boolean)
.join('\n\n')
.replace(/\n{3,}/g, '\n\n') // collapse extra blank lines
.trim();
navigator.clipboard.writeText(md)
.then(() => JZ.toast('Copied ' + md.length + ' chars of Markdown'))
.catch(() => {
// Fallback for older clipboard restrictions
const ta = document.createElement('textarea');
ta.value = md;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
JZ.toast('Copied (fallback)');
});
Aby zamienić to w przycisk: dodaj do reguły akcję typu BUTTON, opisz ją „Kopiuj jako Markdown” i wklej powyższy kod jako jej handler. JZ.toast() to helper JustZix do szybkiego potwierdzenia na stronie — przydatny, bo zapisy do schowka są poza tym ciche.
Usprawnienia warte dodania
- Tabele — dodaj przypadek
TABLE, który emituje wiersze rozdzielone pionowymi kreskami z separatorem---po nagłówku. - Język kodu — wiele stron umieszcza język w klasie w stylu
language-js; odczytaj go i dołącz po otwierającym potrójnym backticku. - Front matter — poprzedź tytułem strony i URL-em jako małym nagłówkiem, abyś wiedział, skąd pochodzi notatka.
- Usuwanie obrazów — jeśli chcesz samego tekstu, usuń przypadek
IMGw całości.
Dlaczego schowek, a nie pobieranie
Mógłbyś zamiast tego wyzwolić pobranie pliku .md, ale schowek wygrywa przy robieniu notatek: klikasz przycisk, przełączasz się do aplikacji do notatek, wklejasz. Żadnego pliku do znalezienia, zmiany nazwy, sprzątania. Wywołanie navigator.clipboard.writeText potrzebuje gestu użytkownika — którym jest kliknięcie przycisku, więc po prostu działa.
Zobacz też
- Własny arkusz druku dla lepszych PDF-ów — inny sposób na wyciągnięcie czystej treści z bałaganiarskiej strony.
- Nakładka audytu dostępności na żywo — więcej praktycznego JavaScriptu chodzącego po DOM.
Jeden przycisk, czysty Markdown, zero sprzątania. Zainstaluj JustZix, dodaj akcję BUTTON z powyższym kodem i zacznij kolekcjonować sieć w sposób, jakiego naprawdę chce Twoja aplikacja do notatek.
Oceń ten wpis
Brak ocen — oceń jako pierwszy.