pandoc 解析 EPUB3 HTML 脚注机制详解:从 epub_html_exts 扩展到 Note AST
2026/9/21 23:20:30 网站建设 项目流程
  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

项目地址:https://gitcode.com/gh_mirrors/pa/pandoc
点击查看免费下载

导读

本文以仓库中 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_haskellExt_epub_html_extsExt_smart同属该分支),而html4html5epubepub2epub3的扩展集合均继承自htmlgetAll "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/rearnoteseFootnotes进入脚注区,收集其中脚注定义
footnote/rearnoteeFootnote将脚注内容存入noteTable,不产生输出块
toceTOC直接丢弃目录(后续由 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_extstype/epub:typefootnotesrearnotes

处理期间会设置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)做两件事:

  1. href="#fn1"中取出标识符fn1,连同当前解析位置记入noteRefPos映射表;
  2. 返回一个临时的RawInline (Format "noteref") "fn1"占位元素。

也就是说,此时脚注内容尚未插入——真正的内容装配发生在整个文档解析完成之后。

五、两阶段机制:noteTable收集 +replaceNotes回填

这是整个脚注解析的精髓:pandoc 采用「先收集定义、后回填引用」的两阶段策略,使脚注引用顺序与定义顺序解耦,且引用可以出现在定义之前。

readHtmlWithDepthparseDoc(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折叠而不会出现在输出中。
  • ↩︎\&crarr;)返回箭头:作为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

项目地址:https://gitcode.com/gh_mirrors/pa/pandoc
点击查看免费下载
上一篇:深入理解prom-client中的Exemplar机制及应用实践
下一篇:为什么选择pydata-sphinx-theme:5个理由让您的Python项目文档脱颖而出

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

标签: 网站建设 企业官网 项目流程 UI设计 前端开发

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询