修复“粘贴 Markdown 无格式”的实践笔记

68a9bf5027bd2d30529618d02025/9/5 13 分钟阅读

在自建博客中从 Typora/网页粘贴内容时,只显示成普通文字;而在 Typora 中能保留标题、列表、代码块。本文记录问题成因与最小可行修复。

背景与问题

  • 根因:大多数应用复制的是 HTML(text/html),而 textarea 只能接收纯文本。直接粘贴会丢失格式,ReactMarkdown 也就无法按 Markdown 渲染。
  • 现状:我们已用 ReactMarkdown + remark-gfm 做预览,但编辑输入源没有转换步骤。

解决思路

  • 接管粘贴事件:在 textareaonPaste 中读取剪贴板的 text/html,若存在则用 Turndown 将 HTML 转为 Markdown,再插回光标处。
  • 保持预览链路不变content 始终保存 Markdown 字符串,预览继续走 ReactMarkdown

关键实现

  • 安装依赖
npm i turndown
npm i -D @types/turndown
  • 在编辑页中初始化 Turndown,并接管粘贴
import TurndownService from 'turndown';

const turndownRef = useRef<TurndownService | null>(null);
if (!turndownRef.current) {
  turndownRef.current = new TurndownService({
    headingStyle: 'atx',
    codeBlockStyle: 'fenced',
  });
}

<textarea
  ref={textareaRef}
  value={content}
  onChange={e => setContent(e.target.value)}
  onPaste={e => {
    const html = e.clipboardData?.getData('text/html');
    const plain = e.clipboardData?.getData('text');
    if (html) {
      e.preventDefault();
      const md = turndownRef.current!.turndown(html);
      const ta = textareaRef.current!;
      const { selectionStart: s, selectionEnd: t } = ta;
      const next = content.slice(0, s) + md + content.slice(t);
      setContent(next);
      setTimeout(() => ta.setSelectionRange(s + md.length, s + md.length), 0);
    } else if (plain?.includes('\t')) {
      // 粘贴纯文本时的小优化:将 Tab 替换为两个空格,避免列表缩进错位
      e.preventDefault();
      const fixed = plain.replace(/\t/g, '  ');
      const ta = textareaRef.current!;
      const { selectionStart: s, selectionEnd: t } = ta;
      const next = content.slice(0, s) + fixed + content.slice(t);
      setContent(next);
      setTimeout(() => ta.setSelectionRange(s + fixed.length, s + fixed.length), 0);
    }
  }}
/>
  • 预览(保持不变)
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
  {content}
</ReactMarkdown>

核心知识点

  • 剪贴板 MIME 类型:浏览器粘贴事件可读多种类型,常见为 text/htmltext/plain。优先取 text/html 再降级。
  • textarea 限制:不支持富文本,必须“先转换后插入”。若需要更复杂排版,应使用 contenteditable 或富文本编辑器。
  • Turndown 的作用:将 HTML 结构“降级”为 Markdown;可通过规则定制(如表格、代码块样式)。
  • 光标与选区恢复:粘贴后需手动设置 setSelectionRange,保证编辑体验流畅。
  • 安全性:你启用了 rehypeRaw,允许渲染原始 HTML。上线建议搭配 rehype-sanitize 或移除 rehypeRaw,避免 XSS。
  • 用户体验:保留“纯文本粘贴”路径(如 Shift+Ctrl+V),并对制表符做最小替换,降低格式错位。

小结

通过“粘贴事件拦截 + Turndown 转换”,把来自 Typora/网页的 HTML 在落入编辑器前就转为 Markdown,从而让内容源与预览渲染保持一致,最终解决了“粘贴后无格式”的问题。