// ==UserScript== // @name 元素属性复制 // @namespace http://tampermonkey.net/ // @version 0.0.1 // @description 右键复制网页元素内容(类名、文本、HTML、Markdown),支持下载为 Markdown,使用 Vue + Element Plus + Turndown 实现。Markdown下载功能目前只做了掘金、CSDN的兼容(有瑕疵),其余网站没特意试过。 // @author 石小石Orz // @match *://*/* // @license MIT // @require https://unpkg.com/vue@3/dist/vue.global.js // @require https://unpkg.com/turndown/dist/turndown.js // @resource ELEMENT_JS https://cdn.jsdelivr.net/npm/element-plus // @resource elementPlusCss https://cdn.jsdelivr.net/npm/element-plus/dist/index.css // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_download // @grant unsafeWindow // @noframes // @downloadURL https://update.greasyfork.icu/scripts/535102/%E5%85%83%E7%B4%A0%E5%B1%9E%E6%80%A7%E5%A4%8D%E5%88%B6.user.js // @updateURL https://update.greasyfork.icu/scripts/535102/%E5%85%83%E7%B4%A0%E5%B1%9E%E6%80%A7%E5%A4%8D%E5%88%B6.meta.js // ==/UserScript== (function () { 'use strict'; // 添加 Element Plus 样式 GM_addStyle(GM_getResourceText('elementPlusCss')); GM_addStyle(` .tm-hover-highlight { outline: 2px solid rgba(0, 123, 255, 0.7); background-color: rgba(0, 123, 255, 0.1) !important; border-radius: 4px; transition: all 0.2s ease; z-index: 9999; } `); // 加载 Vue 和 Element Plus window.Vue = unsafeWindow.Vue = Vue; const { createApp, ref, reactive } = Vue; const elementPlusJS = GM_getResourceText('ELEMENT_JS'); eval(elementPlusJS); // 插入挂载点 const container = document.createElement('div'); container.id = 'copy-helper-app'; document.body.appendChild(container); // Markdown 转换函数 function htmlToMarkdown(el) { const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', }); // 处理
为代码块,过滤说明文字
turndownService.addRule('code-block', {
filter: 'pre',
replacement: function (content) {
const code = (content.match(/`{1,3}([\s\S]*?)`{1,3}/)?.[1] || content).trim();
return '\n```\n' + code + '\n```\n';
}
});
// 处理
turndownService.addRule('table', {
filter: 'table',
replacement: function (_, node) {
let markdown = '';
const rows = Array.from(node.querySelectorAll('tr'));
const extractText = (td) => td.textContent.trim().replace(/\|/g, '\\|');
rows.forEach((row, i) => {
const cells = Array.from(row.children).map(extractText);
markdown += '| ' + cells.join(' | ') + ' |\n';
if (i === 0) markdown += '| ' + cells.map(() => '---').join(' | ') + ' |\n';
});
return '\n' + markdown + '\n';
}
});
return turndownService.turndown(el.innerHTML);
}
// Vue 应用
const App = {
setup() {
const visible = ref(false);
const buttons = reactive([]);
const pos = reactive({ top: 0, left: 0 });
const setButtonsFor = (el) => {
buttons.length = 0;
const className = el.className?.toString().trim();
const text = el.innerText?.trim();
const html = el.innerHTML?.trim();
const fullHtml = el.outerHTML?.trim();
if (className) buttons.push({ label: '复制类名', content: className });
if (text) buttons.push({ label: '复制文本', content: text });
if (html) buttons.push({ label: '复制网页', content: html, type: 'html' });
if (fullHtml) buttons.push({ label: '复制HTML文本', content: fullHtml, type: 'text' });
if (html) {
const md = htmlToMarkdown(el);
buttons.push({ label: '复制为Markdown', content: md, type: 'text' });
buttons.push({ label: '下载为Markdown', content: md, type: 'markdown' });
}
};
const copy = ({ content, type }) => {
if (type === 'markdown') {
const title = document.title.replace(/[\\/:*?"<>|]/g, '_');
GM_download({
url: 'data:text/markdown;charset=utf-8,' + encodeURIComponent(content),
name: title + '.md',
saveAs: true
});
ElementPlus.ElMessage.success('Markdown 已下载');
} else {
GM_setClipboard(content, type || 'text');
ElementPlus.ElMessage.success('复制成功!');
}
visible.value = false;
deactivate();
};
const updatePosition = (x, y) => {
pos.top = y;
pos.left = x;
};
return { visible, buttons, pos, copy, setButtonsFor, updatePosition };
},
template: `
{{ btn.label }}
`
};
const app = createApp(App);
app.use(ElementPlus);
const vm = app.mount('#copy-helper-app');
let currentElement = null;
let activated = false;
const isValidElement = (el) => {
if (!el || el.nodeType !== 1) return false;
const rect = el.getBoundingClientRect();
return !['html', 'body', 'script', 'style'].includes(el.tagName.toLowerCase()) && rect.width >= 30 && rect.height >= 15;
};
const findValidTarget = (el) => {
while (el && el !== document.body) {
if (isValidElement(el)) return el;
el = el.parentElement;
}
return null;
};
const handleMouseMove = (e) => {
if (!activated) return;
let el = e.target;
if (document.querySelector('#copy-helper-app')?.contains(el)) return;
el = findValidTarget(el);
if (!el) return;
if (el !== currentElement) {
currentElement?.classList.remove('tm-hover-highlight');
vm.visible = false;
currentElement = el;
currentElement.classList.add('tm-hover-highlight');
}
};
const handleContextMenu = (e) => {
if (!activated) return;
let el = e.target;
if (document.querySelector('#copy-helper-app')?.contains(el)) return;
el = findValidTarget(el);
if (!el) {
vm.visible = false;
return;
}
if (el === currentElement) {
e.preventDefault();
vm.setButtonsFor(el);
vm.updatePosition(e.pageX, e.pageY);
vm.visible = true;
} else {
vm.visible = false;
}
};
function activate() {
if (activated) return;
activated = true;
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('contextmenu', handleContextMenu, true);
ElementPlus.ElMessage.info('元素复制脚本已启动,右键高亮区域试试!');
}
function deactivate() {
if (!activated) return;
activated = false;
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('contextmenu', handleContextMenu, true);
currentElement?.classList.remove('tm-hover-highlight');
currentElement = null;
vm.visible = false;
}
GM_registerMenuCommand('启动元素复制脚本', activate);
})();