油猴脚本:复制网页内容为 Markdown 格式
把网页上的文章存进自己的笔记,直接复制粘贴过去往往一团糟:标题层级丢了、代码块变成普通文本、图片链接只剩下相对路径打不开。原因是复制走的是浏览器的富文本剪贴板,那套 HTML 样式和 Markdown 的结构语义不是一回事。
这个油猴脚本的做法是:让你先在页面上把要复制的 DOM 元素圈出来,然后用 turndown 把这段 HTML 转成 Markdown 再写进剪贴板。转换过程中标题、列表、代码块、引用都会映射成对应的 Markdown 语法,图片和链接的相对路径也会补成绝对地址。
交互上是键鼠配合选元素:鼠标悬停高亮候选元素,上下箭头在 DOM 树上切父级 / 子级,左右箭头切相邻兄弟节点,Enter 或点击确认复制,Esc 取消。这套设计解决的是「想要的内容和 DOM 结构不完全对齐」的问题——直接点中的可能是段落里的一个 span,往上跳几级才是整篇正文。
资源链接:https://greasyfork.org/zh-CN/scripts/370299-%E5%A4%8D%E5%88%B6%E4%B8%BAmarkdown%E6%A0%BC%E5%BC%8F
代码:
完整脚本如下。开头的元数据注释是油猴的配置:@require 引入 turndown 这个 HTML 转 Markdown 的库,@match 声明在所有 http/https 页面和本地文件上都生效,@grant none 表示不需要油猴的特权 API、脚本直接跑在页面环境里。
主体逻辑分三块:handleMouseover 等事件处理负责高亮和切换当前选中元素(靠给元素加一个带虚线描边的 class 实现),键盘事件负责在 DOM 树上导航,确认后调用 turndown 转换并写剪贴板。右上角那个按钮可以拖动,也支持 Ctrl+c+c 快捷键激活选择模式。
// ==UserScript==
// @name 复制为Markdown格式
// @namespace https://github.com/nameldk/user-script
// @version 0.3.2
// @description 复制网页内容为Markdown格式。点击右上角copy按钮开始选择内容,点击鼠标或按Enter进行复制。按Esc取消选择,上箭头选择父级,下箭头选择子级,左箭头选择前面的相邻元素,右箭头选择后面的相邻元素。按钮可以拖动。Ctrl+c+c激活。
// @author nameldk
// @require https://unpkg.com/turndown/dist/turndown.js
// @match https://*/*
// @match http://*/*
// @match file:///*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CLASS_HINT = '_myhint_';
let $curElement = null;
let $btn = document.createElement('div');
let turndownService = new TurndownService();
let urlProtocol = document.location.protocol;
let urlOrigin = document.location.origin;
let urlPath =
document.location.pathname.substring(
0,
document.location.pathname.lastIndexOf('/'),
) + '/';
let isHold = 0,
isDrag = 0;
function addStyle(css) {
let s = document.createElement('style');
s.type = 'text/css';
s.textContent = css;
document.body.prepend(s);
}
function showHint($this) {
if ($this) {
$this.classList.add(CLASS_HINT);
}
}
function hideHint($this) {
if ($this) {
$this.classList.remove(CLASS_HINT);
}
}
function handleMouseover(e) {
hideHint($curElement);
let $target = e.target;
$curElement = $target;
showHint($target);
}
function handleMouseout(e) {
let $target = e.target;
hideHint($target);
}
function handleKeyup(e) {
function isValidNode(node) {
return node && node.textContent && node.textContent.trim() !== '';
}
function findNextNode(node) {
if (node) {
var findNode = node.nextElementSibling;
while (findNode) {
if (isValidNode(findNode)) {
return findNode;
} else {
if (findNode.nextElementSibling) {
findNode = findNode.nextElementSibling;
}
}
}
}
return null;
}
if (e.keyCode === 13) {
// enter
process(e);
return false;
} else if (e.keyCode === 27) {
// esc
disable();
} else if (e.keyCode === 38) {
// arrow up
if ($curElement && isValidNode($curElement.parentElement)) {
hideHint($curElement);
$curElement = $curElement.parentElement;
showHint($curElement);
}
} else if (e.keyCode === 37) {
// arrow left
if ($curElement && isValidNode($curElement.previousElementSibling)) {
hideHint($curElement);
$curElement = $curElement.previousElementSibling;
showHint($curElement);
}
} else if (e.keyCode === 40) {
// arrow down
let nextNode = null;
if (
$curElement &&
(nextNode = findNextNode($curElement.firstElementChild))
) {
hideHint($curElement);
$curElement = nextNode;
showHint($curElement);
}
} else if (e.keyCode === 39) {
// arrow right
if ($curElement && isValidNode($curElement.nextElementSibling)) {
hideHint($curElement);
$curElement = $curElement.nextElementSibling;
showHint($curElement);
}
}
}
function disableScroll(e) {
if ([38, 40, 37, 39].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}
function handleClick(e) {
process(e);
return false;
}
function process(e) {
if ($curElement) {
e.preventDefault();
copyIt($curElement);
disable();
showTips();
}
}
function showTips() {
let t = document.createElement('div');
t.style.position = 'fixed';
t.style.width = '80px';
t.style.height = '24px';
t.style.lineHeight = '24px';
t.style.top = '10px';
t.style.right = '50%';
t.style.background = '#68af02';
t.style.fontSize = '14px';
t.style.color = '#fff';
t.style.textAlign = 'center';
t.style.borderRadius = '5px';
t.style.marginLeft = '300px';
t.style.zIndex = 10000;
t.innerHTML = '复制成功';
document.body.prepend(t);
setTimeout(function () {
document.body.removeChild(t);
}, 1000);
}
function copyIt($curElement) {
if ($curElement) {
let html = $curElement.innerHTML;
html = html
.replace(/<figure[\s\S]+?<\/figure>/gi, processFigure)
.replace(/<img[^>]+>/gi, processImg)
.replace(/(<a.+?href=")(.*?")(.*?<\/a>)/gi, parseHref);
let markdown = turndownService.turndown(html);
markdown = markdown.replace(/<img.+?>/g, '');
copyToClipboard(markdown);
}
}
function processFigure(str) {
str = str.replace(/<noscript>[\s\S]*<\/noscript>/, '');
let img = str.match(/<img[^>]+?>/);
if (img) {
return img[0];
}
return str;
}
function processImg(imgStr) {
let src = (imgStr.match(/\ssrc=(["'])(.*?)\1/) || [])[2];
if (!src) return '';
let original = (imgStr.match(/\sdata-original=(["'])(.*?)\1/) || [])[2];
if (original) {
src = original;
}
if (src.toLowerCase().indexOf('http') === 0) {
return '<img src="' + src + '" />';
} else if (src.indexOf('//') === 0) {
src = urlProtocol + src;
} else if (src.indexOf('/') === 0) {
src = urlOrigin + src;
} else {
src = urlPath + src;
}
return '<img src="' + src + '" />';
}
function parseHref(match, head, link, tail) {
if (link.substr(0, 4) === 'http') {
return head + link.replace(/#.*/, '') + tail;
}
var path = document.location.pathname.split('/');
path.pop();
if (
link[0] === '#' ||
link.substr(0, 10) === 'javascript' ||
link === '"'
) {
// "#" "javascript:" ""
return head + '#"' + tail;
} else if (link[0] === '.' && link[1] === '/') {
// "./xxx"
return (
head +
document.location.origin +
path.join('/') +
link.substring(1) +
tail
);
} else if (link[0] === '.' && link[1] === '.' && link[2] === '/') {
// ../xxx
var p2Arr = link.split('../'),
tmpRes = [p2Arr.pop()];
path.pop();
while (p2Arr.length) {
var t = p2Arr.pop();
if (t === '') {
tmpRes.unshift(path.pop());
}
}
return head + document.location.origin + tmpRes.join('/') + tail;
} else if (link.match(/^\/\/.*/)) {
// //xxx.com
return head + document.location.protocol + link + tail;
} else if (link.match(/^\/.*/)) {
// /abc
return head + document.location.origin + link + tail;
} else {
// "abc/xxx"
return (
head + document.location.origin + path.join('/') + '/' + link + tail
);
}
}
function copyToClipboard(text) {
const input = document.createElement('textarea');
input.style.position = 'fixed';
input.style.opacity = 0;
input.value = text;
document.body.appendChild(input);
input.select();
let res = document.execCommand('Copy');
document.body.removeChild(input);
return res;
}
function enable() {
document.addEventListener('mouseover', handleMouseover);
document.addEventListener('mouseout', handleMouseout);
document.addEventListener('click', handleClick);
document.addEventListener('keyup', handleKeyup);
window.addEventListener('keydown', disableScroll, false);
}
function disable() {
if ($curElement) {
hideHint($curElement);
$curElement = null;
}
document.removeEventListener('mouseover', handleMouseover);
document.removeEventListener('mouseout', handleMouseout);
document.removeEventListener('click', handleClick);
document.removeEventListener('keyup', handleKeyup);
window.removeEventListener('keydown', disableScroll, false);
$btn.style.display = 'block';
}
function genDoublePressHandler(keyCode, callback, params) {
var intval = 500;
var lastKeypressTime = 0;
var useCtrl = params && params.useCtrl;
var pressCtrl = 0;
if (useCtrl) {
document.addEventListener(
'keydown',
function (e) {
if (useCtrl && e.keyCode === 17) {
pressCtrl = 1;
}
},
false,
);
document.addEventListener(
'keyup',
function (e) {
if (useCtrl && e.keyCode === 17) {
pressCtrl = 0;
}
},
false,
);
}
return function (e) {
var now = +new Date();
if (e.keyCode === keyCode) {
if (
now - lastKeypressTime <= intval &&
(!useCtrl || (useCtrl && pressCtrl))
) {
callback && callback(e);
lastKeypressTime = 0;
}
}
lastKeypressTime = now;
};
}
function initBtn() {
let topDiff = 0,
leftDiff = 0;
$btn.style.position = 'fixed';
$btn.style.width = '44px';
$btn.style.height = '22px';
$btn.style.lineHeight = '22px';
$btn.style.top = '14%';
$btn.style.right = '1%';
$btn.style.background = '#0084ff';
$btn.style.fontSize = '14px';
$btn.style.color = '#fff';
$btn.style.textAlign = 'center';
$btn.style.borderRadius = '6px';
$btn.style.zIndex = 10000;
$btn.style.cursor = 'pointer';
$btn.style.opacity = 0.1;
$btn.innerHTML = 'copy';
$btn.addEventListener('click', function () {
if (isDrag) {
return false;
}
enable();
this.style.display = 'none';
});
$btn.addEventListener('mouseover', function (e) {
this.style.opacity = 1;
});
$btn.addEventListener('mouseout', function () {
this.style.opacity = 0.1;
});
$btn.addEventListener('mousedown', function (e) {
isHold = 1;
leftDiff = e.pageX - this.offsetLeft;
topDiff = e.pageY - this.offsetTop;
$btn.onmousemove = function (e) {
if (isHold) {
isDrag = 1;
}
if (isDrag) {
this.style.top = e.pageY - topDiff + 'px';
this.style.left = e.pageX - leftDiff + 'px';
this.style.right = 'auto';
}
};
});
document.addEventListener('mouseup', function () {
setTimeout(function () {
isHold = 0;
isDrag = 0;
$btn.onmousemove = null;
}, 0);
});
// Ctrl+c+c
document.addEventListener(
'keyup',
genDoublePressHandler(
67,
function (e) {
$btn.click();
},
{ 'useCtrl': 1 },
),
false,
);
}
function init() {
if (!document.body) {
console.warn('no body');
return;
}
addStyle(
'.' +
CLASS_HINT +
'{background-color: #fafafa; outline: 2px dashed #1976d2; opacity: .8; cursor: pointer; -webkit-transition: opacity .5s ease; transition: opacity .5s ease;}',
);
document.body.prepend($btn);
initBtn();
}
init();
})();

还没有留言,来发表第一条吧~