pandoc 将 HTML 高亮代码块转换为 GFM 围栏代码块:hljs 类与 language- 前缀的处理机制
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
导读
在从 HTML 文档迁移到 GitHub 风格 Markdown(GFM)的工作流中,一个高频需求是把语法高亮器(如 highlight.js)产出的<pre><code class="hljs language-bash">代码块原样还原为可读的围栏代码块。pandoc 的 HTML 阅读器与 Markdown 书写器在这一环节有明确、可复现的行为:它会合并pre与code上的属性、剥离language-前缀并输出为带语言标注的反引号围栏。本文以 pandoc 仓库中的命令测试用例 test/command/11701.md 为线索,结合 HTML 阅读器 与 Markdown 书写器 的源码实现,完整讲解这一转换链路,并给出可复制的命令行操作与边界行为说明。
测试用例原文:最小可复现的转换场景
仓库中 test/command/11701.md 是 pandoc 自带的回归测试用例(golden test),完整内容如下:
% pandoc -f html -t gfm <pre style="white-space: pre-wrap"><code class="hljs language-bash">echo hello </code></pre> ^D ``` bash echo hello ```它描述了这样一次转换:
- 输入格式:HTML(
-f html),包含一个典型的 highlight.js 输出片段——外层<pre>带有行内样式white-space: pre-wrap,内层<code>带有class="hljs language-bash"; - 输出格式:GFM(
-t gfm); - 预期结果:输出为以三个反引号包裹的围栏代码块
``` bash,内容为echo hello。
这个用例虽短,却浓缩了三个关键技术点:代码块属性的跨标签合并、hljs等纯装饰类的处理、language-前缀到语言标注的映射。
亲手复现:在终端运行这条命令
将测试用例保存为输入文件后,可以直接用 pandoc 复现(在 pandoc 仓库根目录下执行):
pandoc -f html -t gfm <<'EOF' <pre style="white-space: pre-wrap"><code class="hljs language-bash">echo hello </code></pre> EOF预期输出:
echo hello注意输出围栏后的语言标注为bash而非hljs language-bash。如果去掉language-bash而只保留class="hljs",pandoc 会退化为输出一个不带语言标注的围栏代码块;这说明hljs本身不被当作语言信息处理,只有language-*形式的类才会被识别为语法语言。
底层原理:HTML 阅读器如何解析代码块
转换的第一步发生在 HTML 阅读器中。在 src/Text/Pandoc/Readers/HTML.hs 里,pre标签由解析函数pCodeBlock处理(L686-L704),其核心逻辑分为三步:
- 合并
pre与code的属性:解析器依次匹配<pre>与<code>两个开标签,然后通过attr' <> codeAttr把两者的属性拼接起来。注释明确说明“pre's attributes take precedence”(pre 的属性优先级更高),因为toAttr在遇到重复属性时保留第一个,而拼接顺序是 pre 在前。这正是测试用例里<pre style="white-space: pre-wrap">与<code class="...">能够协同生效的原因——它们分别贡献style和class,互不冲突。 - 剥离
language-前缀:modifyClasses对class属性中的每个词调用stripLanguagePrefix,其实现为T.stripPrefix "language-"(L691)。于是language-bash变成bash,从而在后续书写器中直接对应 GFM 的语言标注。 - 规整内容文本:
manyTill pAny (pCloses "pre" <|> eof)收集pre内部的所有内容,并把<br>标签转换为换行符(见tagToText,L706-L709);最后用T.unsnoc去掉末尾多余的换行,构造出CodeBlock(B.codeBlockWith attr result)。
值得注意的是解析器的调度位置(L239):"pre" -> pCodeBlock <|> pPreBlock,即优先尝试按代码块解析,失败时回退到通用预格式化块(pPreBlock)解析。这一设计保证了普通<pre>文本段落不会被误判。
HTML 阅读器测试套件 test/Tests/Readers/HTML.hs 中有一组针对性测试与上述逻辑一一对应(L130-L140):
, testGroup "code block" [ test html "attributes in pre > code element" $ "<pre><code id=\"a\" class=\"python\">\nprint('hi')\n</code></pre>" =?> codeBlockWith ("a", ["python"], []) "\nprint('hi')" , test html "attributes in pre take precedence" $ "<pre id=\"c\"><code id=\"d\">print('hi mom!')\n</code></pre>" =?> codeBlockWith ("c", [], []) "print('hi mom!')" ]第一个用例验证属性合并(id与class分别来自code与pre);第二个用例验证重复属性冲突时 pre 优先(id="c"胜出)。这些测试从另一个角度印证了 11701 用例中属性的流向。
输出侧:Markdown 书写器如何生成围栏代码块
转换的第二步由 Markdown 书写器完成。在 src/Text/Pandoc/Writers/Markdown.hs 的blockToMarkdown'中,CodeBlock的分支(L580-L611)按以下优先级决定输出形态:
- 若目标变体为Commonmark(GFM 属于此类)或启用了
backtick_code_blocks扩展,输出反引号围栏代码块; - 否则若启用了
fenced_code_blocks扩展,输出波浪线(~)围栏代码块; - 都不满足时,退化为 4 空格缩进(tab stop)的缩进式代码块。
因此-t gfm走的是第一条路径,产出``` bash格式。围栏长度的选择也很讲究:endlineLen会扫描代码内容,统计以```或~~~开头的行,将围栏长度至少定为“内容中最长围栏长度 + 1”(初始最小值为 3),从而避免围栏与内容冲突——这是把任意 HTML 代码块安全搬进 Markdown 的关键细节。
语言标注的生成由getLangFromClasses(L969-L976)完成:
-- Identify the class in a list of classes that corresponds to -- the language syntax. language-X turns to X. getLangFromClasses :: WriterOptions -> [Text] -> Maybe Text getLangFromClasses opts cs = case find ("language-" `T.isPrefixOf`) cs of Just x -> Just (T.drop 9 x) Nothing -> case [x | x <- cs, isJust (lookupSyntax x (writerSyntaxMap opts))] of (x:_) -> Just x [] -> Nothing它优先查找形如language-X的类并把language-前缀去掉(T.drop 9恰好去掉前缀的 9 个字符);找不到时,再借助writerSyntaxMap检查某个类是否匹配已配置的语法映射表(lookupSyntax)。结合阅读器侧的stripLanguagePrefix,hljs类在两侧都不会被误认为语言——这正是 11701 用例输出``` bash而非``` hljs language-bash的完整原因。
实用要点:把这一机制用在你的 HTML→Markdown 工作流中
基于以上源码行为,可以总结出以下可操作的迁移经验:
- 高亮器输出可直接转换:由 highlight.js、Prism 等工具渲染出的
<pre><code class="hljs language-xxx">结构,pandoc 开箱即用(pandoc -f html -t gfm),无需预处理去类名;language-前缀会被自动还原为 GFM 语言标注。 hljs等纯样式类会被丢弃:阅读器只剥离language-前缀,hljs会作为普通 class 进入内部Attr;而 GFM 书写时语言标注仅由getLangFromClasses从language-*类或语法映射中推导,因此hljs不会出现在输出围栏上。若你希望保留自定义类,需要显式开启fenced_code_attributes或attributes扩展(见 L604-L611 中attrs的生成逻辑)。pre与code属性取并集、冲突时 pre 优先:若两个标签都带class,它们会按词合并;若出现id等重复属性,pre上的值胜出。迁移前可据此预判转换结果。- 尾随换行与围栏安全由引擎兜底:阅读器会剔除代码内容末尾的换行,书写器会自动加长围栏以避免与内容中的反引号行冲突,因此 HTML 里换行混乱的代码块也能得到规范的 GFM 输出。
如果还想验证更多 HTML→Markdown 的边界行为,可直接阅读 test/Tests/Readers/HTML.hs 中的 code block 测试组,或参考 test/command/ 目录下的其他回归用例,它们是理解 pandoc 转换语义最权威的“活文档”。
【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考