一个一键动作,把任何页面复制为干净的 Markdown
你读到值得保留的东西,想把它放进笔记。从浏览器里复制粘贴会拖进导航菜单、三个广告、一个 cookie 栏和一团乱麻般的内联样式。你真正想要的是干净的 Markdown:标题、段落、链接、代码块 —— 别无他物。本文构建一个 JustZix 动作按钮,一键就做到这件事。
为什么是 Markdown,为什么是动作按钮
Markdown 是通用的笔记格式 —— 它能干净地粘进 Obsidian、Notion、GitHub issue、你的编辑器、一个纯文本文件。这里的目标是 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;
}
}
注意链接处理:我们用 URL 构造函数把相对 href 相对于 location.href 解析,这样复制的链接在你粘到别处时仍然有效。
转换块级元素
块级转换处理结构性的部件 —— 标题、段落、列表、代码块、引用块 —— 并用空行把它们连起来。
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>,只会递归进它的子节点。最坏情况下,一个奇异的元素降级成它的纯文本 —— 永远不会降级成什么都没有。
先选区,后文章
这个按钮应该聪明:如果你有选中的文字,只转换那部分;否则转换主文章。Selection API 给我们一个 range,我们可以把它克隆进一个 fragment。
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;
}
一个克隆的选区 fragment 可能从元素中间开始,所以块级转换可能看到部分节点 —— 那没关系,我们的「默认递归」规则会优雅地处理它。
把它接到一个 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 类型的动作,把它标为「复制为 Markdown」,并把上面的代码粘成它的处理函数。JZ.toast() 是 JustZix 用于快速页内确认的助手 —— 很方便,因为剪贴板写入否则是无声的。
值得加上的改进
- 表格 —— 加一个
TABLE分支,输出用竖线分隔的行,并在表头后加一个---分隔符。 - 代码语言 —— 许多网站把语言放在一个像
language-js的类里;读它,并把它附在起始的三个反引号后面。 - Front matter —— 把页面标题和 URL 作为一个小小的头部前置进去,这样你知道笔记来自哪里。
- 剥离图片 —— 如果你只想要文字,干脆去掉
IMG分支。
为什么用剪贴板,而不是下载
你本可以触发一个 .md 文件下载,但剪贴板在记笔记上胜出:你点按钮,切到笔记应用,粘贴。没有文件要找,没有重命名,没有清理。navigator.clipboard.writeText 这个调用需要一个用户手势 —— 而一次按钮点击就是,所以它直接就能用。
另见
- 为更好的 PDF 定制打印样式表 —— 从凌乱页面提取干净内容的另一种方式。
- 一个实时无障碍审计浮层 —— 更多实用的 DOM 遍历 JavaScript。
一个按钮、干净的 Markdown、零清理。安装 JustZix,用上面的代码加一个 BUTTON 动作,开始按你的笔记应用真正想要的方式收集网络。
为这篇文章评分
暂无评分 — 成为第一个。