复制内容到其他地方时,如何保留其空行?

我知道在阅读模式下复制,可以不带MD格式,直接复制内容,但是,我希望能复制的时候保留空行,因为我写东西习惯于段落之间保留一个空行,我的笔记也是用这种方式进行排版格式化的,但是在复制到其他地方时,这些空行空格缩进都会丢失,导致我不得不使用第三方工具再排版一次,非常麻烦。

还有有序列表,复制的时候,怎样才能保留前面的数字?

直接复制应该不会有空行,似乎 Obsidian 在阅读模式下的渲染逻辑和编辑模式不一样,很多要素都不一样(吐槽已久的阅读模式与编辑模式视觉效果不一致

所以建议看看“导出”相关的插件,比如导出为 Word 或者其他可以复制的格式,算是曲线救国了……

编辑模式下也复制不到空行,还会带一堆MD格式……这个真的很难受啊

这问题我也理解的不太好,
目前认为, 本质是在 html 视角下, 一些可见字符其实更像是 “渲染样式”, 不算 “内容”
最典型就是有序列表的 1. 2. 3.


但是在复制到其他地方时,这些空行空格缩进都会丢失

首先在阅读视图里是要损失掉一些信息的, 不能认为跟编辑视图下完全一致:

  • 即使 markdown 里十个连续空行, 转为 html 后也是紧挨着的俩段落
  • Ob 风格的注释 %%...%% 在阅读视图里不会显示
  • 有序列表序号会遵循 css 定义的样式, 无视用户指定的数字

另一方面, 这除了跟 Ob 阅读视图转 html 的信息损失有关, 也跟 “粘贴到的程序能否完整识别信息” 有关

选中文本复制时是做了不少工作的, 它会存 html 格式 + plain text 格式各一份, 但粘贴时要看对面应用程序能支持啥格式: 例子就是 copy 同一份富文本 (来自网页, 来自 Ob 阅读视图都行) 粘贴到记事本以及粘贴到 Office 系列, 会看到它输出格式不一样, Office 就能保持有序列表的编号, 记事本显然不行

Ob 里可以控制台贴以下代码, 然后阅读视图随便复制点内容, 点击按钮就能看 “到底复制到了什么” – 是 html + plain text 各存一遍

// 在控制台执行以下代码创建一个临时按钮
const btn = document.createElement('button');
btn.textContent = '读取剪贴板';
btn.style.position = 'fixed';
btn.style.top = '100px';
btn.style.right = '10px';
btn.style.zIndex = '9999';
btn.style.padding = '10px';
btn.style.background = '#007bff';
btn.style.color = 'white';
btn.style.border = 'none';
btn.style.borderRadius = '4px';

btn.onclick = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      // 读取纯文本
      try {
        const plainText = await clipboardItem.getType('text/plain');
        console.log('纯文本内容:', await plainText.text());
      } catch (e) {
        console.log('无纯文本内容');
      }
      
      // 读取HTML
      try {
        const html = await clipboardItem.getType('text/html');
        console.log('HTML内容:', await html.text());
      } catch (e) {
        console.log('无HTML内容');
      }
    }
  } catch (err) {
    console.error('读取剪贴板失败:', err);
  }
};

document.body.appendChild(btn);
new Notice('已添加剪贴板读取按钮到页面右上角');


这些空行空格缩进都会丢失 有序列表复制的时候,怎样才能保留前面的数字

说实话, 如果链接和加粗之类的较少, 有序序号和段间空行较多, 还不如复制 markdown 格式, 然后删掉加粗来的快

其余办法我能想到的是

  • Ob 阅读视图转网页, 然后找网页里的剪藏器, 它们一般对 html 转换细节做得较好
  • 利用现成的定制的 html 转文本工具, 搜了下有个 require('html-to-text') 库似乎是专干这个事的

最后实在不行了, 还有个手动办法是定制一个支持空行和序号的剪切版修复脚本, 即手动实现一点点的 html-to-text 核心功能
原型就是类似下面这种的, 复制后点击按钮, 会读一遍剪切版里的 html 元素, 给 orderlist 序号打补丁并输出纯文本
(Ob 控制台贴以下代码, 注意一定在次要仓库里测试好了再用)

// 创建第二个按钮 - 转换HTML为格式化文本
const convertBtn = document.createElement('button');
convertBtn.textContent = '转换HTML为格式化文本';
convertBtn.style.position = 'fixed';
convertBtn.style.top = '150px';  // 放在第一个按钮下方
convertBtn.style.right = '10px';
convertBtn.style.zIndex = '9999';
convertBtn.style.padding = '10px';
convertBtn.style.background = '#28a745';
convertBtn.style.color = 'white';
convertBtn.style.border = 'none';
convertBtn.style.borderRadius = '4px';

convertBtn.onclick = async () => {
  try {
    // 获取剪贴板中的HTML内容
    const clipboardItems = await navigator.clipboard.read();
    let htmlContent = '';
    
    for (const clipboardItem of clipboardItems) {
      try {
        const htmlBlob = await clipboardItem.getType('text/html');
        htmlContent = await htmlBlob.text();
        break; // 找到HTML内容就停止
      } catch (e) {
        continue;
      }
    }
    
    if (!htmlContent) {
      alert('剪贴板中没有找到HTML内容');
      return;
    }
    
    // 转换HTML为格式化文本
    const formattedText = htmlToTextWithFormatting(htmlContent);
    
    // 显示结果
    console.log('格式化后的文本输出:');
    console.log('-------------------');
    console.log(formattedText);
    console.log('-------------------');
    
    // 复制到剪贴板
    await navigator.clipboard.writeText(formattedText);
    new Notice(`已转换并复制到剪贴板!\n结果输出到控制台且复制到剪贴板\n ${formattedText}`);

  } catch (err) {
    console.error('转换失败:', err);
    alert('转换失败: ' + err.message);
  }
};

// 添加到页面
document.body.appendChild(convertBtn);

// HTML转换函数
function htmlToTextWithFormatting(html) {
  const temp = document.createElement('div');
  temp.innerHTML = html;
  
  // 处理有序列表
  const olElements = temp.querySelectorAll('ol');
  olElements.forEach(ol => {
  let listCounter = 1; // 重置计数器
  
  // 计算当前ol的嵌套深度
  const getIndentLevel = (element) => {
    let level = 0;
    let parent = element.parentElement;
    while (parent && parent !== temp) {
      if (parent.tagName === 'OL' || parent.tagName === 'UL') {
        level++;
      }
      parent = parent.parentElement;
    }
    return level;
  };
  
  const currentLevel = getIndentLevel(ol);
  
  // 处理当前ol下的直接li子元素
  const topLevelLis = Array.from(ol.children).filter(child => child.tagName === 'LI');
  
  topLevelLis.forEach(li => {
    // 计算缩进 - 每层缩进4个空格
    const indent = '    '.repeat(currentLevel);
    
    // 处理子列表(会在递归中处理)
    const subLists = li.querySelectorAll('ol');
    //subLists.forEach(subOl => {
    //  const subLis = subOl.querySelectorAll('li');
    //  subLis.forEach((subLi, subIndex) => {
    //    const subIndent = '    '.repeat(getIndentLevel(subOl));
    //    subLi.textContent = `${subIndent}${subIndex + 1}. ${subLi.textContent.trim()}\n`;
    //  });
    //});
    
    
    subLists.forEach(subOl => {
      const subLis = subOl.querySelectorAll('li');
      subLis.forEach((subLi, subIndex) => {
        const subIndent = '    '.repeat(getIndentLevel(subOl));
        const prefix = subIndex === 0 ? '\n' : ''; // 在第一个subLi元素前加换行
        subLi.textContent = `${prefix}${subIndent}${subIndex + 1}. ${subLi.textContent.trim()}\n`;
      });
    });
    
    
    
    
    
    // 处理当前项
    li.textContent = `${indent}${listCounter++}. ${li.textContent.trim()}\n`;
  });
});
  
  // 处理无序列表(转换为*)
  const ulElements = temp.querySelectorAll('ul');
  ulElements.forEach(ul => {
    const lis = ul.querySelectorAll('li');
    lis.forEach(li => {
      li.textContent = `* ${li.textContent}\n`;
    });
  });
  
  // 保留段落换行
  const pElements = temp.querySelectorAll('p');
  pElements.forEach(p => {
    p.innerHTML = p.innerHTML + '\n\n';
  });
  
  // 处理换行符
  const brElements = temp.querySelectorAll('br');
  brElements.forEach(br => {
    br.replaceWith('\n');
  });
  
  // 处理div换行
  const divElements = temp.querySelectorAll('div');
  divElements.forEach(div => {
    if (!div.querySelector('p') && !div.querySelector('br')) {
      div.innerHTML = div.innerHTML + '\n';
    }
  });
  
  // 移除多余空白
  return temp.textContent
    .replace(/\n{3,}/g, '\n\n')  // 多个空行合并为两个
    .trim();
}

new Notice('已添加"转换HTML为格式化文本"按钮到页面右上角');
1 个赞

感谢这么详细的分析,眼下看来没什么好的解决方案,这也算是用黑曜石写作然后分享到其他地方时的一个问题了