Markdown 实时预览怎么实现双向同步?

迷人的天佑 阅读 4

我在做一个 Markdown 编辑器,左边是输入框,右边是预览区。现在的问题是:我改了左边内容,右边能实时更新;但用户点击右边预览里的某个段落,没法自动定位到左边对应位置。这双向同步到底咋搞啊?

试过给每个块加 data-line 属性,也监听了预览区的 click 事件,但不知道怎么准确算出光标该跳到编辑器哪一行。比如这段:

const markdown = <code># 标题

这是一个段落。

- 列表项1
- 列表项2</code>;

点“列表项1”时,怎么让 textarea 的光标跳到第5行?有现成的库推荐吗?还是得自己解析 AST?

我来解答 赞 0 收藏
二维码
手机扫码查看
1 条解答
 ___瑞娜
这个问题其实就是经典的 source map 思路,得在渲染阶段把源码行号记录下来。

核心思路是这样:在解析 Markdown 的时候,给每个生成的 HTML 元素打上 data-line 标记,记录它对应的源码行号。点击预览区的时候,拿到这个行号,让编辑器跳过去。

用 marked.js 的话,可以配合它的 tokenizer 或者 walkTokens 钩子来实现。给你一个比较稳妥的方案:

import { marked } from 'marked';
import DOMPurify from 'dompurify';

const renderer = new marked.Renderer();

// 重写 heading、paragraph、list 等方法,加上行号
const originalHeading = renderer.heading.bind(renderer);
renderer.heading = function(token) {
const html = originalHeading(token);
return html.replace(/^/, );
};

// 类似地处理其他元素...
// 或者用 walkTokens 更省事

marked.use({
walkTokens(token) {
if (token.line !== undefined) {
token.attr = token.attr || {};
token.attr['data-line'] = token.line;
}
}
});

function renderMarkdown(text) {
const html = marked.parse(text);
// 防止注入,这一步不能省
return DOMPurify.sanitize(html, {
ADD_ATTR: ['data-line']
});
}


点击预览区定位的逻辑:

previewArea.addEventListener('click', (e) => {
const target = e.target.closest('[data-line]');
if (!target) return;

const lineNum = parseInt(target.dataset.line, 10);
const lines = editor.value.split('n');

// 算出该行在 textarea 里的偏移量
let offset = 0;
for (let i = 0; i < lineNum && i < lines.length; i++) {
offset += lines[i].length + 1; // +1 是换行符
}

editor.focus();
editor.setSelectionRange(offset, offset + lines[lineNum]?.length || 0);
});


说几个坑点。marked 的 token.line 有时候不太准,特别是列表嵌套的情况,建议用 marked 的 lexer 先跑一遍,手动维护一个行号映射表。

还有个方案是用 markdown-it 配合 markdown-it-source-map 插件,它对行号的追踪更精确一些,但配置起来稍微麻烦。

安全方面要注意,预览区渲染的内容一定要经过 DOMPurify 处理,防止 XSS。用户输入的 Markdown 可能包含恶意脚本,特别是允许 HTML 标签的时候。另外 data-line 属性要在白名单里加上,不然会被 sanitize 掉。

如果项目时间紧,直接用 Toast UI Editor 或者 ByteMD 这类成熟方案,它们内置了这个功能。自己造轮子容易踩坑,尤其是列表、代码块、表格这些复杂结构的行号计算。
点赞 1
2026-03-02 20:28