同一份 Markdown 分发到 4 个技术社区,标题层级为什么只在一处丢失

简介: 同一份 Markdown 源稿分发到四个技术社区,排版却分成四种。本文实测各平台编辑器的内核差异(mditor、tiptap、metaWeblog),给出按内核分派写入通道与结构断言的修法,并附可复用代码。

同一份 Markdown 分发到 4 个技术社区,标题层级为什么只在一处丢失

一、背景:一次分发,四种排版

我把同一批技术稿投到四个平台:一家手机端图文订阅平台、掘金、腾讯云开发者社区、阿里云开发者社区,用的是一条自动化的分发链路:本地 Markdown 源稿 → 按平台适配 → 写进各家的编辑器 → 回读校验。

内容一字不差,排版却分成四种。图文订阅那一端是短段落的节奏感,掘金上标题层级完好,腾讯云上 ## 和 ** 直接裸露在正文里,阿里云上则是一整片没有小标题的平铺段落,读者要滑很久才能找到下一节。

一开始我判断是平台把样式「吃」了,挨个去找各家的兼容性问题。查完之后发现判断错了——四个平台里只有一个是真吃标签,其余两个的问题出在我自己的源稿上。这篇记录定位过程和修法,涉及三段可复用的代码。

二、定位:先分清编辑器的内核

技术社区的编辑器看起来都是「一个框」,内核差别很大,决定了它能接受什么形态的输入。

平台 编辑器内核 接受的输入
阿里云开发者社区 mditor(textarea + 实时预览) Markdown 源
腾讯云开发者社区 tiptap / ProseMirror 富文本 HTML 片段,不收 Markdown
掘金 Markdown 编辑器 Markdown 源
博客园 metaWeblog API HTML 字符串

判断内核有一个很快的办法:看有没有等宽的 textarea。阿里云发文页里能查到 textarea.textarea(可视输入区)和 textarea.mditor-hidden,说明输入的是纯文本源,右侧预览区负责渲染;腾讯云正文区则是一个 div.tiptap.ProseMirror[contenteditable=true],没有 textarea,它保存的是节点树,不保存源文本。

内核不同,同一个「粘贴」动作的结果就完全不同:Markdown 源编辑器会把 ## 解析成标题节点,富文本编辑器只会把它当成两个井号字符。

三、实测:把同一段样本投进两家

样本只有四行,覆盖了标题、加粗、列表、代码块四种结构:

## 二级标题测试

正文一段,带 **加粗** 和 `行内代码`。

- 列表项一
- 列表项二

```python
print(1)
```

分别写进两家的编辑器,再数生成的结构标签和裸露的 Markdown 符号:

阿里云(mditor):预览区生成 H2 × 1、STRONG × 1、UL × 1、LI × 2、PRE × 1、CODE × 2,裸露符号 0 个。结构完整。

腾讯云(ProseMirror):生成 P × 6,H2、STRONG、UL 全部为 0,正文里残留 ## 一处、** 两处。结构全丢,符号裸露。

结论很清楚:阿里云并没有丢样式,它老老实实按 Markdown 渲染;腾讯云则是把 Markdown 当成普通文字收进去了。至于阿里云那篇为什么会变成平铺段落——去看源稿就明白了,那一版的小标题写的是「它是什么?」「怎么赚钱?」,全是纯文本行,一个 ## 都没有。没有结构可渲染,任何编辑器都只能给你一堆 <p>。

四、修法一:按内核分派写入通道

既然内核不同,写入方式就不能只有一种。我在分发层按平台分出三条通道。

通道 A:Markdown 源编辑器(阿里云 mditor、掘金)

直接把 Markdown 写进 textarea。注意必须走原生 setter 并派发 input 事件,否则框架的绑定不会感知到变化,预览区不会刷新:

def fill_markdown_source(cdp, md):
    """写 Markdown 源编辑器:原生 setter + input 事件,缺一不可。"""
    js = """
    (() => {
      const t = document.querySelector('textarea.textarea');
      if (!t) return -1;
      const set = Object.getOwnPropertyDescriptor(
          window.HTMLTextAreaElement.prototype, 'value').set;
      set.call(t, %s);
      t.dispatchEvent(new Event('input', {bubbles: true}));
      return t.value.length;
    })()
    """
    return cdp.eval(js % json.dumps(md))

通道 B:富文本编辑器(腾讯云 ProseMirror)

不能喂 Markdown,要先转成 HTML,再直插。insertText 只会得到纯文本节点,insertHTML 才会生成真正的标题节点:

def fill_rich_editor(cdp, html):
    """写富文本编辑器:必须 insertHTML,insertText 只会得到一堆 <p>。"""
    js = """
    (() => {
      const e = document.querySelector('.ProseMirror');
      if (!e) return -1;
      e.focus();
      document.execCommand('insertHTML', false, %s);
      return e.innerText.length;
    })()
    """
    return cdp.eval(js % json.dumps(html))

通道 C:只收 HTML 的接口(博客园 metaWeblog)

这类没有编辑器可操作,newPost / editPost 的 description 字段直接收 HTML 字符串。Markdown 必须在本地转完再传,平台端不做任何解析。

五、修法二:把结构断言放进发布门禁

排版问题难对付的地方在于「发出去才知道」。所以我把结构检查做成发布前的断言:写入完成后,直接数目标区域里的标题节点,对不上就不许提交。

def assert_structure(cdp, scope, expect_h2):
    """提交前自检:标题节点数必须达标,且不得残留 Markdown 符号。"""
    js = """
    (() => {
      const root = document.querySelector('%s');
      if (!root) return {err: 'root-not-found'};
      const t = root.innerText || '';
      return {
        h2: root.querySelectorAll('h2, h3').length,
        raw_hash: (t.match(/^#{1,6} /gm) || []).length,
        raw_star: (t.match(/\\*\\*/g) || []).length
      };
    })()
    """
    r = cdp.eval(js % scope)
    if r.get('err') or r['h2'] < expect_h2:
        raise SystemExit('结构断言失败:期望至少 %d 个标题,实际 %d'
                         % (expect_h2, r['h2']))
    if r['raw_hash'] or r['raw_star']:
        raise SystemExit('结构断言失败:正文残留 Markdown 符号 %d/%d'
                         % (r['raw_hash'], r['raw_star']))
    return True

注意断言要排除 pre / code 节点:代码块里本身就会含 ## 和 **(本篇的样本就是例子),不排除会把示例当成残留误报。我写这篇稿时就地触发过一次——正文结构全对,断言却报「残留 1 处 ##、4 处 **」,排查十分钟才发现命中的是代码块里的示例。

这套断言抓出过我两次事故:一次是富文本通道错用了 insertText,标题数恒为 0;一次是源稿漏写小节标记,标题数虽然过线,但正文里的字号全一样。后者说明断言只能证明「有结构」,证不了「结构对」——所以我又加了一条字数下限,防止有人为了过断言硬凑标题。

六、两条踩过的坑

坑一:图文版那套「伪小标题」是个不小的隐患。

图文版为了手机上读着顺,把每个小节压成一句一段,「它是什么?」这种问句单独一行当小标题用。这套排版在手机上有效,在技术社区就是灾难:同一份稿子渲染出来是上百个 <p>,没有任何标题层级,读者无法跳读,平台侧也可能判成「排版混乱」。

结论是图文稿不能当母版。分发链路里应该维护一份带完整 Markdown 结构的母稿,给图文平台的那一版由母稿降级生成,而不是反过来。

坑二:同步工具只保证「内容到达」,不保证「结构到达」。

我起初用同步助手把 Markdown 一次性推到各家草稿箱,返回都是成功。但「成功」只意味着文字进去了,结构有没有进去取决于目标平台的适配器:源编辑器会解析,富文本适配器可能整段塞成纯文本。所以判据不能看接口的成功码,必须回读目标页面的节点计数。

这两条合起来是一个更一般的教训:跨平台分发要校验的是「读者看到的东西」,不是「我发出去的东西」。

七、延伸:如果把这套分发搬到云上

目前的链路跑在本地,靠一台常驻浏览器和一个状态文件。如果要多人共用,一个可行的形态是:稿件与素材放对象存储(OSS),分发任务由函数计算(FC)定时触发,幂等键、平台状态、回读结果落数据库。

设计上有三个点必须带着走:一是幂等键(同一条稿 + 同一个平台只允许一次写入),二是失败可重放(平台侧状态未知时先回读再决定,不做盲目重试),三是结构断言作为门禁而不是事后检查。这套东西的难点不在调度,在于「平台没有幂等语义,得自己在本地建账」。

以上是方案设想,尚未实测;本文的实测数据只覆盖前六节的编辑器对照。

八、小结

「样式丢了」听起来像平台 bug,实际拆开是两件不同的事:源稿有没有结构,目标内核吃不吃这种结构。前者靠母稿规范解决,后者靠按内核分派写入通道解决,两者都可以用节点计数验证。

排查顺序也值得记一下:先数目标页面的标题节点,再判断是编辑器不收还是源稿没给。跳过这一步直接猜平台兼容性,会像我一样先怀疑错方向。

参考资料

相关文章
|
云安全 安全 网络安全
|
7天前
|
JavaScript 安全 前端开发
Nodejs Brotli decoder 代码执行漏洞 (CVE-2026-111497)10629
Node.js发布安全更新修复Brotli解码器高危RCE漏洞(CVE-2026-111497),建议受影响版本用户立即升级。
2009 1
|
14天前
|
JSON 程序员 数据格式
最新版Notepad++下载、安装、配置保姆级教程(附官网安装包)
Notepad++ 是一款免费开源、轻量快速的 Windows 文本编辑器,支持多标签、语法高亮、代码折叠与宏录制,体积小、启动快、资源占用低,兼容 x64/x32/ARM 架构,是程序员和文字工作者的高效利器。
3200 0
|
13天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
2295 44
|
10天前
|
Web App开发 人工智能 前端开发
2026 年 AI 浏览器怎么选?Ego Lite、Tabbit、Chrome DevTools MCP 深度对比,开发者到底该用哪个?
2026年AI浏览器选型指南:Ego Lite专注多Agent协同(Mac优先),Tabbit国产首选,原生支持Windows与Codex控制;Chrome DevTools MCP非浏览器而是开发者利器,深度集成Console/Network/Performance调试能力,让AI真正“看懂并验证”前端代码。
1434 3
|
7天前
|
安全 应用服务中间件 Go
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
PHP2Shell 双 RCE 已遭在野利用:WordPress、Drupal 全在射程内,nginx 其实另有洞。拆成因与修复顺序
PHP2Shell 漏洞拆解:WordPress 与 nginx 为何中招
|
8天前
|
开发者 索引
DeepSeekHarness大肥鱼自带插件市场了?加个地址就能用
Open Plugin Market 是一个开放插件索引平台,收录800+优质DSH插件,支持分类浏览、智能推荐与随机发现。无需安装市场,粘贴索引地址即可一键接入,网页版直访 dshopen.org,助力小开发者被看见。(239字)
1746 1
DeepSeekHarness大肥鱼自带插件市场了?加个地址就能用
|
6天前
|
人工智能 Linux API
Magpie, 轻量本地AI网关!免费Token用不完!
Magpie 是开源AI编程模型调度网关,上线一月获1.8万Star。它通过本地高性能网关统一托管多平台API密钥、账号与免费额度,实现跨Agent的模型秒级切换、静默额度轮换与资源复用,显著降低碎片化管理成本。(239字)
Magpie, 轻量本地AI网关!免费Token用不完!
|
6天前
|
安全 程序员 应用服务中间件
PHP 被曝两大致命漏洞,请所有程序员抓紧做这件事!
PHP 官方发布了紧急安全更新,一口气修复了两个严重漏洞。如果网站和服务器用了 PHP 应该怎么处理?漏洞的原理是什么?本文给你讲清楚
704 0