[js] 导出当前文档中的格式文本(也可复制到剪切版)

image.png

// ==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();
})();
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119773 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1985 回帖
1 操作
JeffreyChen 在 2025-10-13 00:33:23 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 342 关注
  • WebComponents

    Web Components 是 W3C 定义的标准,它给了前端开发者扩展浏览器标签的能力,可以方便地定制可复用组件,更好的进行模块化开发,解放了前端开发者的生产力。

    1 引用 • 18 关注
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    182 引用 • 400 回帖
  • Visio
    1 引用 • 2 回帖
  • Sillot

    Insights(注意当前设置 master 为默认分支)

    汐洛彖夲肜矩阵(Sillot T☳Converbenk Matrix),致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点。其中汐洛绞架(Sillot-Gibbet)基于自思源笔记(siyuan-note),前身是思源笔记汐洛版(更早是思源笔记汐洛分支),是智慧新录乄终端(多端融合,移动端优先)。

    主仓库地址:Hi-Windom/Sillot

    文档地址:sillot.db.sc.cn

    注意事项:

    1. ⚠️ 汐洛仍在早期开发阶段,尚不稳定
    2. ⚠️ 汐洛并非面向普通用户设计,使用前请了解风险
    3. ⚠️ 汐洛绞架基于思源笔记,开发者尽最大努力与思源笔记保持兼容,但无法实现 100% 兼容
    29 引用 • 25 回帖 • 152 关注
  • 创业

    你比 99% 的人都优秀么?

    81 引用 • 1396 回帖 • 1 关注
  • PWA

    PWA(Progressive Web App)是 Google 在 2015 年提出、2016 年 6 月开始推广的项目。它结合了一系列现代 Web 技术,在网页应用中实现和原生应用相近的用户体验。

    14 引用 • 69 回帖 • 186 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    960 引用 • 946 回帖
  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖 • 2 关注
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    348 引用 • 765 回帖 • 1 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    46 引用 • 114 回帖 • 139 关注
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    502 引用 • 1397 回帖 • 240 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 633 关注
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 691 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    60 引用 • 22 回帖 • 2 关注
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖 • 1 关注
  • ZooKeeper

    ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 HBase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

    61 引用 • 29 回帖 • 14 关注
  • Hadoop

    Hadoop 是由 Apache 基金会所开发的一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。

    95 引用 • 122 回帖 • 634 关注
  • V2Ray
    1 引用 • 15 回帖 • 4 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • AWS
    11 引用 • 28 回帖 • 2 关注
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖 • 14 关注
  • 电影

    这是一个不能说的秘密。

    125 引用 • 610 回帖
  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    200 引用 • 545 回帖
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 724 关注