
// ==UserScript==
// @name 思源笔记 - 导出/复制格式文本
// @version 3.8
// @description 支持导出/复制粗体/斜体/下划线/Mark文本
(function() {
'use strict';
const countMap = new WeakMap();
// 轻量提示(右下角浮动,不打断操作)
function showNotification(msg) {
const notification = document.createElement('div');
notification.style.position = 'fixed';
notification.style.bottom = '20px';
notification.style.right = '20px';
notification.style.padding = '8px 12px';
notification.style.background = 'rgba(0,0,0,0.7)';
notification.style.color = 'white';
notification.style.borderRadius = '4px';
notification.style.zIndex = '99999';
notification.style.fontSize = '12px';
notification.textContent = msg;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.3s';
setTimeout(() => document.body.removeChild(notification), 300);
}, 2000);
}
// 复制分类文本到剪贴板(适配桌面版Electron)
function copyText(protyle, type) {
const data = countMap.get(protyle) || { bold: [], italic: [], underline: [], mark: [] };
const textList = {
bold: data.bold,
italic: data.italic,
underline: data.underline,
mark: data.mark
}[type];
if (!textList.length) {
showNotification(`未找到${type === 'bold' ? '粗体' : type === 'italic' ? '斜体' : type === 'underline' ? '下划线' : 'Mark'}文本`);
return;
}
const text = textList.join('\n');
navigator.clipboard.writeText(text).then(() => {
showNotification(`已复制${type === 'bold' ? '粗体' : type === 'italic' ? '斜体' : type === 'underline' ? '下划线' : 'Mark'}到剪贴板`);
}).catch(err => {
console.error('剪贴板复制失败,降级处理:', err);
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showNotification(`已用兼容模式复制${type === 'bold' ? '粗体' : type === 'italic' ? '斜体' : type === 'underline' ? '下划线' : 'Mark'}到剪贴板`);
});
}
// 复制全部混合文本到剪贴板
function copyMixedText(protyle) {
const data = countMap.get(protyle) || { bold: [], italic: [], underline: [], mark: [] };
const { bold, italic, underline, mark } = data;
if (!bold.length && !italic.length && !underline.length && !mark.length) {
showNotification('当前笔记中未找到粗体、斜体、下划线和Mark文本');
return;
}
let mixedContent = '';
if (bold.length) {
mixedContent += `=== 粗体(共${bold.length}条) ===\n`;
mixedContent += bold.join('\n') + '\n\n';
}
if (italic.length) {
mixedContent += `=== 斜体(共${italic.length}条) ===\n`;
mixedContent += italic.join('\n') + '\n\n';
}
if (underline.length) {
mixedContent += `=== 下划线(共${underline.length}条) ===\n`;
mixedContent += underline.join('\n') + '\n\n';
}
if (mark.length) {
mixedContent += `=== Mark(共${mark.length}条) ===\n`;
mixedContent += mark.join('\n');
}
navigator.clipboard.writeText(mixedContent).then(() => {
showNotification('已复制全部文本到剪贴板');
}).catch(err => {
console.error('剪贴板复制失败,降级处理:', err);
const textarea = document.createElement('textarea');
textarea.value = mixedContent;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showNotification('已用兼容模式复制全部文本到剪贴板');
});
}
// 导出分类文本
function exportText(protyle, type) {
const data = countMap.get(protyle) || { bold: [], italic: [], underline: [], mark: [] };
const textList = {
bold: data.bold,
italic: data.italic,
underline: data.underline,
mark: data.mark
}[type];
if (!textList.length) {
showNotification(`未找到${type === 'bold' ? '粗体' : type === 'italic' ? '斜体' : type === 'underline' ? '下划线' : 'Mark'}文本`);
return;
}
let title = protyle.querySelector('.protyle-title')?.textContent?.trim() || '未命名';
title = title.replace(/[\/:*?"<>|]/g, '-');
const date = new Date().toISOString().split('T')[0];
const typeName = type === 'bold' ? '粗体' : type === 'italic' ? '斜体' : type === 'underline' ? '下划线' : 'Mark';
const fileName = `[${title}]${typeName}_${date}.txt`;
const blob = new Blob([textList.join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
showNotification(`已导出 ${typeName} 文本`);
}
// 导出全部混合文本
function exportMixedText(protyle) {
const data = countMap.get(protyle) || { bold: [], italic: [], underline: [], mark: [] };
const { bold, italic, underline, mark } = data;
if (!bold.length && !italic.length && !underline.length && !mark.length) {
showNotification('当前笔记中未找到粗体、斜体、下划线和Mark文本');
return;
}
let mixedContent = '';
if (bold.length) {
mixedContent += `=== 粗体(共${bold.length}条) ===\n`;
mixedContent += bold.join('\n') + '\n\n';
}
if (italic.length) {
mixedContent += `=== 斜体(共${italic.length}条) ===\n`;
mixedContent += italic.join('\n') + '\n\n';
}
if (underline.length) {
mixedContent += `=== 下划线(共${underline.length}条) ===\n`;
mixedContent += underline.join('\n') + '\n\n';
}
if (mark.length) {
mixedContent += `=== Mark(共${mark.length}条) ===\n`;
mixedContent += mark.join('\n');
}
let title = protyle.querySelector('.protyle-title')?.textContent?.trim() || '未命名';
title = title.replace(/[\/:*?"<>|]/g, '-');
const date = new Date().toISOString().split('T')[0];
const fileName = `[${title}]粗体+斜体+下划线+Mark_${date}.txt`;
const blob = new Blob([mixedContent], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
showNotification('已导出全部文本');
}
// 更新计数(上半部分导出,下半部分复制)
function updateCount(protyle) {
const btn = protyle.querySelector('#bold-italic-underline-mark-export');
const dropdownBtns = protyle.querySelectorAll('#export-dropdown button');
const content = protyle.querySelector('.protyle-content');
if (!btn || !dropdownBtns.length || !content) return;
const allNodes = content.querySelectorAll(
'.b, [data-type="strong"], .i, [data-type="em"], .u, [data-type="u"], .mark, [data-type="mark"]'
);
const boldList = [];
const italicList = [];
const underlineList = [];
const markList = [];
allNodes.forEach(el => {
const text = el.textContent.trim();
if (!text) return;
if (el.matches('.b, [data-type="strong"]')) boldList.push(text);
else if (el.matches('.i, [data-type="em"]')) italicList.push(text);
else if (el.matches('.u, [data-type="u"]')) underlineList.push(text);
else if (el.matches('.mark, [data-type="mark"]')) markList.push(text);
});
const prev = countMap.get(protyle) || { bold: [], italic: [], underline: [], mark: [] };
if (
boldList.length === prev.bold.length &&
italicList.length === prev.italic.length &&
underlineList.length === prev.underline.length &&
markList.length === prev.mark.length
) return;
const total = boldList.length + italicList.length + underlineList.length + markList.length;
btn.textContent = `📤 导出文本(${total})`;
// 顺序:导出全部 → 导出粗体 → 导出斜体 → 导出下划线 → 导出Mark → 分隔线 → 复制全部 → 复制粗体 → 复制斜体 → 复制下划线 → 复制Mark
dropdownBtns[0].textContent = `导出全部(共${total}条)`;
dropdownBtns[1].textContent = `导出 粗体(${boldList.length}条)`;
dropdownBtns[2].textContent = `导出 斜体(${italicList.length}条)`;
dropdownBtns[3].textContent = `导出 下划线(${underlineList.length}条)`;
dropdownBtns[4].textContent = `导出 Mark(${markList.length}条)`;
dropdownBtns[5].textContent = `复制全部(共${total}条)`;
dropdownBtns[6].textContent = `复制 粗体(${boldList.length}条)`;
dropdownBtns[7].textContent = `复制 斜体(${italicList.length}条)`;
dropdownBtns[8].textContent = `复制 下划线(${underlineList.length}条)`;
dropdownBtns[9].textContent = `复制 Mark(${markList.length}条)`;
countMap.set(protyle, { bold: boldList, italic: italicList, underline: underlineList, mark: markList });
}
// 监听内容变化
function observeContent(protyle) {
const content = protyle.querySelector('.protyle-content');
if (!content) return;
const observer = new MutationObserver(() => {
clearTimeout(protyle.exportTimer);
protyle.exportTimer = setTimeout(() => updateCount(protyle), 300);
});
observer.observe(content, {
childList: true,
subtree: true,
characterData: true
});
}
// 创建按钮与一列下拉菜单(导出和复制中间用一条线分隔)
function addBtn(protyle) {
if (protyle.querySelector('#bold-italic-underline-mark-export')) return;
const breadcrumb = protyle.querySelector('.protyle-breadcrumb');
if (!breadcrumb) return;
const btnHtml = `
<div style="display:inline-block;position:relative;margin-left:8px;">
<button id="bold-italic-underline-mark-export" style="
padding:6px 10px;
background:linear-gradient(135deg,#d2691e,#a0522d);
color:white;border:none;border-radius:6px;
font-size:13px;cursor:pointer;">📤 导出文本(0)</button>
<div id="export-dropdown" style="
display:none;position:absolute;top:100%;left:0;
min-width:160px;background:#fff;border-radius:4px;
box-shadow:0 2px 8px rgba(0,0,0,0.2);z-index:9999;
padding:4px 0;">
<!-- 导出部分 -->
<button data-action="export" data-type="all" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">导出全部(共0条)</button>
<button data-action="export" data-type="bold" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">导出 粗体(0条)</button>
<button data-action="export" data-type="italic" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">导出 斜体(0条)</button>
<button data-action="export" data-type="underline" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">导出 下划线(0条)</button>
<button data-action="export" data-type="mark" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">导出 Mark(0条)</button>
<!-- 分隔线 -->
<hr style="margin:2px 0;border:0;border-top:1px solid #eee;">
<!-- 复制部分 -->
<button data-action="copy" data-type="all" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">复制全部(共0条)</button>
<button data-action="copy" data-type="bold" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">复制 粗体(0条)</button>
<button data-action="copy" data-type="italic" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">复制 斜体(0条)</button>
<button data-action="copy" data-type="underline" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">复制 下划线(0条)</button>
<button data-action="copy" data-type="mark" style="width:100%;padding:4px 12px;text-align:left;border:none;background:transparent;cursor:pointer;font-size:12px;">复制 Mark(0条)</button>
</div>
</div>
`;
breadcrumb.insertAdjacentHTML('beforeend', btnHtml);
const btn = protyle.querySelector('#bold-italic-underline-mark-export');
const dropdown = protyle.querySelector('#export-dropdown');
if (!btn || !dropdown) return;
btn.addEventListener('click', () => exportMixedText(protyle));
btn.onmouseenter = () => dropdown.style.display = 'block';
btn.onmouseleave = () => setTimeout(() => !dropdown.matches(':hover') && (dropdown.style.display = 'none'), 200);
dropdown.onmouseleave = () => dropdown.style.display = 'none';
dropdown.querySelectorAll('button').forEach(item => {
item.addEventListener('mousedown', (e) => {
e.preventDefault(); // 阻止焦点切换
const action = item.dataset.action;
const type = item.dataset.type;
setTimeout(() => {
if (action === 'export') {
type === 'all' ? exportMixedText(protyle) : exportText(protyle, type);
} else if (action === 'copy') {
type === 'all' ? copyMixedText(protyle) : copyText(protyle, type);
}
}, 0);
});
item.onmouseenter = () => item.style.background = '#f5f5f5';
item.onmouseleave = () => item.style.background = 'transparent';
});
updateCount(protyle);
observeContent(protyle);
}
// 初始化
function init() {
const checkLayout = setInterval(() => {
const layoutCenter = document.querySelector('.layout__center');
if (layoutCenter) {
clearInterval(checkLayout);
layoutCenter.querySelectorAll('.protyle').forEach(protyle => addBtn(protyle));
const tabObserver = new MutationObserver(() => {
setTimeout(() => {
layoutCenter.querySelectorAll('.protyle').forEach(protyle => addBtn(protyle));
const activeProtyle = layoutCenter.querySelector('.protyle:not([style*="display: none"])');
if (activeProtyle) updateCount(activeProtyle);
}, 150);
});
tabObserver.observe(layoutCenter, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class']
});
document.addEventListener('click', (e) => {
if (e.target.closest('.layout-tab-bar .item')) {
setTimeout(() => {
const activeProtyle = layoutCenter.querySelector('.protyle:not([style*="display: none"])');
if (activeProtyle) {
addBtn(activeProtyle);
updateCount(activeProtyle);
}
}, 200);
}
});
}
}, 200);
}
init();
})();
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于