pandoc 的 --top-level-division=chapter 实战:从命令行测试用例看 Markdown 标题到 ConTeXt 章节命令的映射

【免费下载链接】pandoc Universal markup converter 【免费下载链接】pandoc 项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

本文以仓库中 test/command/3968.md 这个最小化命令行测试为切入点,完整解读 pandoc 的 --top-level-division 选项如何把 Markdown 的 #/## 标题层次平移为 ConTeXt 的 \startchapter/\startsection 章节环境,并结合源码剖析其四种取值、默认启发式规则与标识符生成机制。读完本文,你将掌握该选项在 ConTeXt 及其他格式下的准确行为,并能独立复现与验证这一测试。

从测试用例说起:一次最小的 Markdown → ConTeXt 转换

test/command/3968.md 是一个典型的 pandoc 命令行 golden test(期望输出测试)。文件全文是一个反引号代码块,其中依次包含:待执行的命令、通过 stdin(以 ^D 表示输入结束)传入的 Markdown 原文,以及期望的标准输出:

% pandoc --top-level-division=chapter -t context
# Chapter

## Section
^D
\startchapter[title={Chapter},reference={chapter}]

\startsection[title={Section},reference={section}]

\stopsection

\stopchapter

逐段拆解这条测试规格:

  • % pandoc --top-level-division=chapter -t context:以 context(ConTeXt)为输出格式,并显式指定顶层分节类型为 chapter;
  • # Chapter 与 ## Section:两级 Markdown 标题,经转换后分别对应 \startchapter 与 \startsection;
  • title={...} 携带标题文字,reference={...} 携带该标题自动生成的标识符;
  • \stopsection 与 \stopchapter 成对闭合,形成完整的 ConTeXt 章节环境。

该用例要验证的核心行为是:当 --top-level-division=chapter 生效时,一级标题必须映射为 chapter 而非默认的 section,二级标题则映射为 section,即整个标题层级向下平移一级。

--top-level-division:命令行选项与四种取值

--top-level-division 的定义位于 src/Text/Pandoc/App/CommandLineOptions.hs,它接受一个必填参数,合法取值只有四种:

取值效果
section顶层标题映射为 section
chapter顶层标题映射为 chapter
part顶层标题映射为 part
default由启发式规则自动推断(见下文)

参数解析实现中还内置了严格的校验逻辑:传入上述四种取值之外的任何字符串,都会触发 PandocOptionError 并提示 Argument of --top-level division must be section, chapter, part, or default。换句话说,这一选项是"白名单"式的,不允许自由命名分节类型。

这四种取值在内部被建模为 TopLevelDivision 代数数据类型,定义于 src/Text/Pandoc/Options.hs:

data TopLevelDivision = TopLevelPart      -- ^ Top-level headers become parts
                      | TopLevelChapter   -- ^ Top-level headers become chapters
                      | TopLevelSection   -- ^ Top-level headers become sections
                      | TopLevelDefault   -- ^ Top-level type is determined via
                                          --   heuristics
                      deriving (Show, Read, Eq, Data, Typeable, Generic)

该类型是 WriterOptions(writerTopLevelDivision 字段)的组成部分,因此不仅命令行可以设置,通过 API 调用 writeConTeXt、writeLaTeX 等函数时同样可以编程指定。默认值为 TopLevelDefault。

默认值的启发式规则:什么时候自动变成 chapter

MANUAL.txt 对 --top-level-division 的默认行为有明确说明(见 MANUAL.txt):

  • 层级顺序固定为 part > chapter > section,选定顶层类型后,所有标题统一平移,使顶层标题恰好成为指定类型;
  • 默认情况下,除非满足其他条件,启发式规则会选择 section;
  • 当 documentclass 元数据变量被设置为 report、book 或 memoir(且未指定 article 选项)时,选项会被隐式推断为 chapter;
  • 若输出格式为 beamer,指定 chapter 或 part 会使顶层标题变成 \part{..},而二级标题保持默认类型不变。

这解释了为什么 3968 测试要显式传入 --top-level-division=chapter:如果不传,在默认 section 启发式下,# Chapter 会映射为 section 而非 chapter,测试的期望输出就会完全不同。

源码视角:ConTeXt 输出器如何平移标题层级

理解了选项本身,再看 ConTeXt 输出器 src/Text/Pandoc/Writers/ConTeXt.hs 中标题到章节命令的映射逻辑。核心函数 sectionLevelToText 内部定义了一个 semanticSection 辅助函数,它依据 writerTopLevelDivision 对标题级别施加不同的偏移量(shift):

case writerTopLevelDivision opts of
  TopLevelPart    -> semanticSection (-2)
  TopLevelChapter -> semanticSection (-1)
  TopLevelSection -> semanticSection 0
  TopLevelDefault -> ...

而 semanticSection 的核心是一段按偏移后层级分派的逻辑:

return $ case hdrLevel + shift of
           -1         -> literal "part"
           0          -> chapter
           n | n >= 1 -> text (concat (replicate (n - 1) "sub"))
                         <> section
           _          -> empty -- cannot happen

其中 hdrLevel 是 Markdown 标题级别(一级标题为 1,二级为 2,依此类推)。代入 3968 测试的场景(TopLevelChapter,shift = -1):

  • # Chapter(hdrLevel = 1):1 + (-1) = 0 → 输出 chapter,即 \startchapter;
  • ## Section(hdrLevel = 2):2 + (-1) = 1 → 输出 section,即 \startsection;
  • 若还有 ### Subsection(hdrLevel = 3):3 + (-1) = 2 → 输出 subsection(n - 1 个 sub 前缀拼接 section)。

sectionHeader(ConTeXt.hs)则负责把标题渲染成带选项列表的 \start... 命令:title= 携带标题内容,reference= 携带标识符。同时它区分两种标题形态——出现在 Div(section 容器)中的标题使用 \start 前缀并成对输出 \stop 结尾(见 ConTeXt.hs 的 blockToConTeXt 分支),这就是测试输出中 \startsection ... \stopsection、\startchapter ... \stopchapter 成对出现的原因;而游离于容器外的标题则输出单条命令。sectionFooter 依据同样的 sectionLevelToText 计算结果生成对应的 \stop 命令,确保开合一致。

标题标识符与 unnumbered:reference 与 number 选项从哪来

测试输出中的 reference={chapter}、reference={section} 并非手写,而是源自标题的标识符(identifier)。在 ConTeXt.hs 中,选项列表的构造逻辑为:

let optsList = mconcat . filter (not . null) $
      [ ["title=" <> braces contents | not (isEmpty contents)]
      , ["reference=" <> braces (literal (toLabel ident)) | not (T.null ident)]
      , ["number=no"          | "unnumbered" `elem` classes]
      , ["incrementnumber=no" | "unnumbered" `elem` classes]
      ]

由此可以得出三个规律:

  • reference 取标题的标识符(ident)。未显式指定时,pandoc 会依据标题文本自动生成标识符(# Chapter → chapter),因此测试输出中出现 reference={chapter};若作者在标题上显式指定 {#myref} 这类属性,reference 也会相应变化;
  • 标识符会经过 toLabel 清洗:\ # [ ] " , { } % ( ) | = 等 ConTeXt 特殊字符会被转写为 ux 加十六进制码的形式(见 ConTeXt.hs),避免破坏 ConTeXt 命令语法;
  • 标题带有 unnumbered class(Markdown 中形如 # Title {-} 或 {.unnumbered})时,输出会追加 number=no 与 incrementnumber=no,实现"不编号且不影响后续计数"的效果。

此外 sectionLevelToText 中还处理了 unlisted class:此时 chapter/section 分别被替换为 title/subject(见 ConTeXt.hs),用于生成不出现在目录中的标题。

同一选项在其他输出格式中的影响

--top-level-division 并非 ConTeXt 专属。按 MANUAL.txt 的说明,它同时作用于 LaTeX、ConTeXt、DocBook、TEI 四种输出格式(源码层面,LaTeX、DocBook、TEI、JATS 等 writer 均有对应实现,例如 src/Text/Pandoc/Writers/LaTeX.hs 与 src/Text/Pandoc/Writers/TEI.hs),并额外影响 Docx 输出:选择 chapter 时会在一级标题前插入分节符,选择 part 时则在一级、二级标题前都插入分节符,且除非 reference doc 另行修改,否则每个分节后脚注编号会重新计数。

因此在多格式发布流水线中,--top-level-division=chapter 是让同一份 Markdown 源在 ConTeXt 与 LaTeX 下保持"顶层即章"语义一致的关键开关。

如何复现与验证该测试

test/command/3968.md 属于 pandoc 的命令行 golden 测试体系,同类测试文件均位于 test/command 目录(如 10002.md、11486.md 等),由 test/test-pandoc.hs 驱动执行。你可以用两种方式验证:

方式一:直接手工复现。将测试中的输入通过 stdin 送入 pandoc:

printf '# Chapter\n\n## Section\n' | pandoc --top-level-division=chapter -t context

预期输出与测试规格完全一致,即 \startchapter[title={Chapter},reference={chapter}] 起、\stopchapter 收的完整章节环境。

方式二:运行测试套件。若本地具备构建环境,可执行 cabal test(配置见 cabal.project 与 pandoc.cabal)来跑通包含该用例在内的全部 command 测试;修改 --top-level-division 的取值(如换成 section 或 part)后再对比输出,可以直观观察到 chapter 与 part、section 之间的一级平移差异,从而更深刻地理解 3968 这个最小用例所锁定的行为契约。

【免费下载链接】pandoc Universal markup converter 【免费下载链接】pandoc 项目地址: https://gitcode.com/gh_mirrors/pa/pandoc

更多推荐