Действие в один клик, копирующее любую страницу в чистый Markdown
Ты прочитал что-то стоящее и хочешь сохранить это в свои заметки. Копирование-вставка из браузера тащит за собой меню навигации, три рекламы, баннер cookie и клубок inline-стилей. На самом деле тебе нужен чистый Markdown: заголовок, абзацы, ссылки, блоки кода — и ничего больше. Эта статья собирает кнопку-действие JustZix, которая делает ровно это в один клик.
Почему Markdown и почему кнопка-действие
Markdown — универсальный формат заметок: он чисто вставляется в Obsidian, Notion, GitHub issues, твой редактор, обычный текстовый файл. Цель здесь — кнопка в панели действий JustZix, которая конвертирует либо текущее выделение текста, либо всю статью в Markdown и кладёт его в буфер обмена. Никакого DevTools, никакого лишнего приложения, никакой ручной зачистки.
Действие типа BUTTON идеально для этого: оно показывает подписанную кнопку в панели действий, и клик по ней запускает твой JavaScript. Этот JavaScript мы сейчас и напишем.
Стратегия конвертации
Мы рекурсивно обходим DOM. Для каждого узла мы решаем: это текст (выдаём его), или элемент, который мы умеем конвертировать (выдаём Markdown для него), или то, что надо пропустить (навигация, скрипт, реклама). Во всё, что мы не распознали, мы заходим рекурсивно — чтобы неизвестные обёртки не теряли свой контент. Это поведение рекурсии по умолчанию — запасной вариант для простого текста.
// 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';
}
Конвертация строчных элементов
Строчная конвертация обрабатывает форматирование внутри текста: жирный, курсив, код, ссылки. Она возвращает строку, рекурсивно заходя в потомков, чтобы вложенные строчные теги работали.
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;
}
}
Обрати внимание на обработку ссылок: мы разрешаем относительные href относительно location.href с помощью конструктора URL, чтобы скопированная ссылка по-прежнему работала, когда ты вставишь её в другом месте.
Конвертация блочных элементов
Блочная конвертация обрабатывает структурные части — заголовки, абзацы, списки, блоки кода, цитаты — и соединяет их пустыми строками.
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');
}
Последняя строка — это запасной вариант: <div>, <section> или <article>, для которых у нас нет специального правила, просто рекурсивно заходит в своих потомков. В худшем случае экзотический элемент деградирует до своего простого текста — но никогда до ничего.
Сначала выделение, потом статья
Кнопка должна быть умной: если у тебя выделен текст, конвертируй только его; иначе конвертируй основную статью. API Selection даёт нам диапазон, который можно клонировать во фрагмент.
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;
}
Клонированный фрагмент выделения может начинаться посреди элемента, так что блочная конвертация может увидеть частичные узлы — это нормально, наше правило рекурсии по умолчанию изящно с этим справляется.
Подключаем к действию BUTTON и копируем
Теперь собери части воедино, построй итоговую строку и запиши её в буфер обмена. Это тело твоего действия 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)');
});
Чтобы превратить это в кнопку: добавь к правилу действие типа BUTTON, подпиши его «Copy as Markdown» и вставь код выше как его обработчик. JZ.toast() — это хелпер JustZix для быстрого подтверждения на странице — удобно, потому что запись в буфер обмена иначе беззвучна.
Доработки, которые стоит добавить
- Таблицы — добавь ветку
TABLE, выдающую строки, разделённые вертикальной чертой, с разделителем---после заголовка. - Язык кода — многие сайты кладут язык в класс вроде
language-js; прочитай его и добавь после открывающих трёх обратных кавычек. - Front matter — добавь в начало заголовок страницы и URL небольшой шапкой, чтобы знать, откуда пришла заметка.
- Удаление картинок — если тебе нужен только текст, убери ветку
IMGполностью.
Почему буфер обмена, а не скачивание
Ты мог бы запускать вместо этого скачивание файла .md, но для ведения заметок буфер обмена выигрывает: кликаешь по кнопке, переключаешься в приложение заметок, вставляешь. Никакого файла, который надо искать, никакого переименования, никакой зачистки. Вызову navigator.clipboard.writeText нужен жест пользователя — а клик по кнопке как раз им является, так что оно просто работает.
Смотри также
- Свой стиль печати для лучших PDF — другой способ извлечь чистый контент с беспорядочной страницы.
- Живой оверлей аудита доступности — больше практичного JavaScript для обхода DOM.
Одна кнопка, чистый Markdown, ноль зачистки. Установи JustZix, добавь действие BUTTON с кодом выше — и начни собирать веб так, как этого на самом деле хочет твоё приложение заметок.
Оцени эту статью
Оценок пока нет — оцени первым.