从0到1理解OmniOperator:大数据算子加速的完整学习路径
pandoc 的 --top-level-division=chapter 实战:从命令行测试用例看 Markdown 标题到 ConTeXt 章节命令的映射
【免费下载链接】pandoc Universal markup converter 项目地址: 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 命令语法; - 标题带有
unnumberedclass(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 项目地址: https://gitcode.com/gh_mirrors/pa/pandoc
更多推荐

所有评论(0)