- 文档
- 开发工具
- CLI
【免费下载链接】pandoc
Universal markup converter
导读
本文以仓库中 test/command/7884.md 这份命令行黄金测试(golden test)为切入点,深入剖析 pandoc 的 HTML 读取器在启用epub_html_exts扩展后,如何识别 EPUB3 风格的 XHTML 结构(epub:type属性、noteref脚注引用、文档末尾脚注区),并将其转换为 Pandoc 原生 AST(Native)中的Note内联元素。读完本文,你将掌握:测试文件的结构与运行方式、epub_html_exts扩展的启用范围、脚注解析的完整两阶段机制(收集脚注定义 → 回填引用),以及从块级到行内级的全部解析分支。
一、测试文件全景:一条命令、一段输入、一份期望输出
test/command/7884.md是 pandoc 仓库中典型的命令行黄金测试文件。这类文件的格式约定为:用```代码块包裹,代码块内第一行是待执行的 pandoc 命令行,^D之前是标准输入内容,^D之后(仍在代码块内)是期望的标准输出。这些文件由 test/Tests/Command.hs 驱动,在实际测试时会把命令和输入喂给 pandoc,再将输出与期望输出逐字比对。
本测试的完整内容如下:
% pandoc -f html+epub_html_exts -t native <body epub:type="bodymatter"> <section id="chapter-1" class="level1">| Ext_epub_html_exts -- ^ Recognise the EPUB extended version of HTML从源码结构看,它被列入了getAll "html"返回的默认扩展集合(Extensions.hs 中Ext_literate_haskell、Ext_epub_html_exts、Ext_smart同属该分支),而html4、html5、epub、epub2、epub3的扩展集合均继承自html(getAll "epub" = getAll "html",见 Extensions.hs)。因此它实际覆盖了 HTML 与 EPUB 系列的所有读取场景。
启用该扩展后,HTML 读取器会额外识别一系列 EPUB 专有的语义结构,其核心判断依据是标签上的type/epub:type属性,以及role属性。在 src/Text/Pandoc/Readers/HTML.hs 中可以看到统一取值逻辑:
let type' = fromMaybe "" $ lookup "type" attr <|> lookup "epub:type" attr role = fromMaybe "" $ lookup "role" attr epubExts = extensionEnabled Ext_epub_html_exts exts即:优先读取type,其次读取epub:type,二者之一命中即生效——这保证同一份 HTML 在 EPUB2 与 EPUB3 两种标注风格下都能解析。
三、块级解析:章节、脚注区与脚注定义
启用epub_html_exts后,block解析器(HTML.hs)会按type'/epub:type'的值分派到不同处理函数,形成一张完整的 EPUB 结构映射表:
epub:type/type取值 | 处理函数 | 行为 |
|---|---|---|
含chapter的 sectioning 元素 | eSection | 标记章节上下文并解析内容 |
footnotes/rearnotes | eFootnotes | 进入脚注区,收集其中脚注定义 |
footnote/rearnote | eFootnote | 将脚注内容存入noteTable,不产生输出块 |
toc | eTOC | 直接丢弃目录(后续由 writer 重新生成) |
含titlepage的 section/grouping 元素 | eTitlePage | 丢弃标题页容器 |
role="doc-endnotes" | eFootnotes | 兼容 ARIA 语义的脚注区 |
3.1 章节识别:eSection
测试输入中的<section id="chapter-1" class="level1">content <- pInTags tag block updateState $ \s -> s {noteTable = M.insert ident content (noteTable s)}
注意两个细节:其一,eFootnote仅登记内容而不返回任何块,这就是期望输出中脚注区整体消失的原因;其二,<li>内嵌的返回链接<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a>只是普通链接文本,并不会干扰脚注内容本身的解析,因此Note内的段落是干净的[Str "This", Space, Str "is", Space, Str "a", Space, Str "test"]。
3.3 脚注区容器:eFootnotes
<section class="footnotes footnotes-end-of-document" epub:type="footnotes">由eFootnotes(HTML.hs)处理。该函数有两个入口条件(二选一即可):
role属性为doc-endnotes(EPUB 无障碍语义);- 启用
epub_html_exts且type/epub:type为footnotes或rearnotes。
处理期间会设置inFootnotes = True,内部解析得到的块如果为空(本测试即如此,因为脚注都被eFootnote单独消费掉了),则整个容器不输出;如果内部还残留非脚注内容,则保留为Div。
四、行内解析:noteref引用如何变成Note
正文中的<a href="#fn1" class="footnote-ref" id="fnref1" epub:type="noteref">1</a>是脚注引用点。在inline解析器(HTML.hs)中,a标签有一个专门的先行分支:
"a" | extensionEnabled Ext_epub_html_exts exts , Just "noteref" <- lookup "type" attr <|> lookup "epub:type" attr , Just ('#',_) <- lookup "href" attr >>= T.uncons -> eNoteref | Just "doc-noteref" <- lookup "role" attr , Just ('#',_) <- lookup "href" attr >>= T.uncons -> eNoteref | otherwise -> pLink两个条件都要求href以#开头(即指向文档内锚点),分别兼容epub:type="noteref"与role="doc-noteref"两种标注方式;不满足条件时回退为普通链接pLink。
eNoteref(HTML.hs)做两件事:
- 从
href="#fn1"中取出标识符fn1,连同当前解析位置记入noteRefPos映射表; - 返回一个临时的
RawInline (Format "noteref") "fn1"占位元素。
也就是说,此时脚注内容尚未插入——真正的内容装配发生在整个文档解析完成之后。
五、两阶段机制:noteTable收集 +replaceNotes回填
这是整个脚注解析的精髓:pandoc 采用「先收集定义、后回填引用」的两阶段策略,使脚注引用顺序与定义顺序解耦,且引用可以出现在定义之前。
在readHtmlWithDepth的parseDoc(HTML.hs)中可以看到完整流程:
blocks <- fixPlains False . mconcat <$> manyTill block eof meta <- stateMeta . parserState <$> getState bs' <- replaceNotes (B.toList blocks) reportLogMessages return $ Pandoc meta $ extractMain bs'- 阶段一(解析):
manyTill block eof逐块解析整篇文档。期间正文的noteref变成RawInline "noteref"占位符并记录位置,文末脚注区里的eFootnote把脚注内容按id存入noteTable。 - 阶段二(回填):
replaceNotes(HTML.hs)用walkM遍历 AST,遇到RawInline (Format "noteref") ref时从noteTable查表替换:
replaceNotes' noteTbl (RawInline (Format "noteref") ref) = maybe warnNotFound (pure . Note . B.toList) $ M.lookup ref noteTbl查表命中的脚注内容被包装为Note内联元素——这正是期望输出中Note [Para [Str "This", ...]]的来源。若未命中(例如引用了不存在的id),则借助第一阶段记录的noteRefPos定位到引用点,发出ReferenceNotFound日志警告,并生成一个空的Note []兜底(HTML.hs),避免整个解析失败。
测试期望输出中每个Para末尾的Note,以及脚注区整体消失,正是这一两阶段机制的直接结果。脚注内容最终以Note形式嵌入正文 AST,后续无论转换为 EPUB、LaTeX 还是 Markdown,writer 都能据此重新生成正确的脚注结构与返回链接。
六、测试文件中的其他可挖掘点
本测试输入还包含了几个值得留意的结构:
<body epub:type="bodymatter">:pBody(HTML.hs)会顺带读取lang/xml:lang属性写入文档元数据,但bodymatter这类语义值不影响解析结果。<hr />:位于脚注区内部,因整个容器被eFootnotes折叠而不会出现在输出中。↩︎(\↵)返回箭头:作为footnote-back链接的可见文本被解析器正常消费,但不会污染Note内容,说明解析器对脚注项内部任意内容的处理是稳健的。
若想验证其他 EPUB 结构的行为,可在类似测试中补充epub:type="toc"(将被eTOC丢弃、由 writer 重新生成)、epub:type="rearnotes"/rearnote(尾注,走与脚注相同的eFootnotes/eFootnote分支)以及<switch>/<case>媒体条件块(eSwitch/eCase,见 HTML.hs,按required-namespace匹配 MathML 等命名空间)。
七、如何查看与运行该测试
- 查看测试:直接阅读 test/command/7884.md,其命令、输入、期望输出三段式结构即为测试的全部内容。
- 手动复现:在安装了 pandoc 的环境中执行
pandoc -f html+epub_html_exts -t native,粘贴本文第一节的输入(以^D结束),即可得到与期望输出一致的 Native AST。 - 测试驱动:该文件属于
test/command/目录下由 test/Tests/Command.hs 统一驱动的黄金测试集;除该文件外,test/command/ 目录还包含大量同类用例(如脚注、表格、媒体格式等),可作为理解 pandoc 各功能解析行为的索引。 - 扩展定义:
epub_html_exts的正式定义与扩展继承关系见 src/Text/Pandoc/Extensions.hs,完整解析实现见 src/Text/Pandoc/Readers/HTML.hs。
结语
test/command/7884.md以极简的篇幅浓缩了 pandoc HTML 读取器中 EPUB 语义处理的核心链路:epub_html_exts扩展开启识别能力,epub:type属性驱动块级与行级分派,noteref占位符与noteTable映射表完成两阶段装配,最终在原生 AST 中呈现为标准的Note元素。理解这条链路,无论是排查 EPUB 转换中的脚注问题,还是为自定义 HTML 结构编写过滤与转换逻辑,都会事半功倍。
- 文档
- 开发工具
- CLI
【免费下载链接】pandoc
Universal markup converter
相关推荐
pandoc JATS 阅读器脚注交叉引用解析机制详解:从 `<xref ref-type="fn">` 到 Pandoc Note
pandoc JATS 阅读器脚注交叉引用解析机制详解:从 <xref ref type="fn" 到 Pandoc Note 导读 本文以 pandoc 官方
文档开发工具CLIPandoc MediaWiki 阅读器对多行 `<ref>` 脚注的解析与 AST 输出详解
Pandoc MediaWiki 阅读器对多行 <ref 脚注的解析与 AST 输出详解 导读 本文以 Pandoc 仓库中的命令行回归测试 test/comm
文档开发工具CLIPandoc latex_macros 扩展实战:LaTeX 宏定义的解析与展开机制详解
Pandoc latex_macros 扩展实战:LaTeX 宏定义的解析与展开机制详解 本文以 pandoc 仓库中的黄金测试 test/command/58
文档开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考