[js] 选中部分文本复制出 Markdown 格式

如果官方考虑能给个选项就更好了 😄 ,我这个实现还是有点拉的

效果图

screenshot20250203131812.gif

修改前是这个样子的:

选择部分文字时无法正确复制公式.gif

同时不影响原有块复制实现(块内直接按 Ctrl+C)(注意到控制台中输出了 Not Edit Block):

不影响原有块复制实现.gif

实现基本思路

前端不太懂,俺结合论坛内其他大佬的实现、还有 Deepseek 的帮助下,搓出来的。一并表示感谢。

观察到复制出的文本包含两种类型,一种是 text/plain,即纯文本结果;另一种是 text/html,即包含格式的富文本结果。所以总思路是将 text/html 转换为 Markdown。

screenshot20250203133053.png

具体实现方式如下:

  1. 判断是否是选中部分文本。考虑到对整个块进行复制时是可以正常复制出 Markdown 文本的,为了不干扰选中整个块的情况,需判断是否选中部分文本。实现相对鸡贼,是通过判断 Toolbar 是否冒出来判断的;
  2. 从原始复制结果中读出来 text/html 内容,并借助 Protyle 提供的助手函数将 html 变回 Markdown
  3. 将 Markdown 结果写入剪切板。为保证在编辑器内复制效果不受影响,先读取了原本的 text/html,然后将读上来的 text/html 和 Markdown 结果一起写入剪切板。

已知问题

受限于“判断是否选中部分文本”的实现方式,在选中文本后,需直接按下Ctrl+C进行复制,不可进行其他点击操作;这意味着通过右键菜单进行复制时,本脚本无法生效。

脚本内容

!(function () {
    // 获取Protyle对象,以使用其中的BlockDOM2StdMd助手函数
    function getProtyle() {
        try {
            if (document.getElementById("sidebar")) return siyuan.mobile.editor.protyle;
            const currDoc = siyuan?.layout?.centerLayout?.children.map(item => item.children.find(item => item.headElement?.classList.contains('item--focus') && (item.panelElement.closest('.layout__wnd--active') || item.panelElement.closest('[data-type="wnd"]')))).find(item => item);
            return currDoc?.model.editor.protyle;
        } catch (e) {
            console.error(e);
            return null;
        }
    };
    // 判断是否选中部分文本
    function isNotEditBlock() {
        const p = getProtyle(); // 甜菜,看toolbar是否出来,判断是否选中部分文本
        if(p.toolbar.element.className.includes('fn__none')) return true;
        return false;
    };

    let toolbar_show = false;
    document.addEventListener(
        "mouseup", async (event) =>{
            setTimeout(async () => {
                if(isNotEditBlock()){
                    toolbar_show = false;
                }else{
                    toolbar_show = true;
                }
            }, 20)
    });

    document.addEventListener(
      "copy",
      async (event) => {
        setTimeout(async () => {
          try {
            // 判断是不是选中部分文本,如果是的话才处理
            if(toolbar_show == false){
                console.info("Not Edit ablock");
            }else{
                let mddata = '';
        
                let clipboardItems = await navigator.clipboard.read();
                for (const clipboardItem of clipboardItems) {
                    for (const type of clipboardItem.types) {
                        if(type == "text/html"){ // 读取剪切板中的text/html数据,然后借助助手函数把html转换为md
                            const blob = await clipboardItem.getType(type);
                            // 创建FileReader对象,以做到用文本方式读取blob对象
                            const reader = new FileReader();
                            reader.readAsText(blob, 'utf-8');
                            reader.onload = function(e) {
                                const htmlString = reader.result; // 读取后的结果
                                // console.log('htmlString: ' + htmlString);
                                // 因为读出来的 text/html 可能包含多个并列的 p 标签,需要遍历处理
                                const parser = new DOMParser();
                                const doc = parser.parseFromString(htmlString, 'text/html');
                                const pTags = doc.querySelectorAll('p'); // 获取所有 p 标签
                    
                                const p = getProtyle();
                        
                    
                                pTags.forEach((pTag, index) => {
                                    // console.log(`pTag[${index}].innerHTML: ` + pTag.innerHTML);
                                    // 如果需要在每个处理结果之间加上换行符,可以在后面拼接 "\n"
                                    mddata += p.lute.HTML2Md(pTag.innerHTML).trimEnd() + "\n";
                                });
                            };
                        }
                
                    }
                }

                // 仅修改text/plain,text/html不动
                clipboardItems = await navigator.clipboard.read();
                const newItems = [];
                for (const item of clipboardItems) {
                    const types = item.types;
                    const newData = {};

                    for (const type of types) {
                        // 其他类型直接保留原数据
                        newData[type] = await item.getType(type);
                    }
                    newData['text/plain'] = new Blob([mddata], { type: 'text/plain' });
                    newItems.push(new ClipboardItem(newData));
                }

                // 将修改后的内容写回剪贴板
                await navigator.clipboard.write(newItems);
            }

    
    
          } catch (err) {
            console.error("读取剪贴板内容失败:", err);
          }
        }, 100);
      },
      { passive: false, capture: true }
    );
  })();

参考了如下工作,感谢各位大佬的分享:

  1. [js] 复制行级代码或图片时, 替换原始复制 - 链滴 (ld246.com)
  2. [js] 思源复制内容添加块链接 - 链滴 (ld246.com)
  3. [js] 快速改样式 ----// 利用 protyle.toolbar.setInlineMark 进行块更新后保存 - 链滴 (ld246.com),其中选出来 Protyle 的函数很有用。
  • 思源笔记

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

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

    23831 引用 • 96888 回帖
2 操作
jielahou 在 2025-02-03 13:41:29 更新了该帖
jielahou 在 2025-02-03 13:38:41 更新了该帖

相关帖子

欢迎来到这里!

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

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