← Wszystkie wpisy

Typy akcji

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 ? '![' + alt + '](' + 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

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ż

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.

Wypróbuj samodzielnie

Zainstaluj JustZix i wklej dowolny snippet z tego artykułu. Dwie minuty od zera do działającej reguły na wszystkich Twoich urządzeniach.

Pobierz JustZix

Funkcje · Jak to działa · Przykłady · Zastosowania