- 文档
- 开发工具
- CLI
【免费下载链接】pandoc
Universal markup converter
导读
本文围绕 pandoc 官方命令测试用例 test/command/8098.md 展开,深入剖析 pandoc 生成 reveal.js 幻灯片时的核心机制:--slide-level如何决定幻灯片切分层级、两级标题如何组织成 reveal.js 的二维嵌套<section>结构、以及::: fragment容器如何被翻译为class="fragment"的分步展示元素。读完本文,你将能够精确掌控 pandoc 将 Markdown 转换为 reveal.js 演示文稿的分节行为,并理解其背后 src/Text/Pandoc/Slides.hs 与 src/Text/Pandoc/Writers/HTML.hs 的源码级实现原理。
一、测试用例全景:一个典型的 revealjs golden test
test/command/8098.md 是 pandoc 仓库中众多命令回归测试(command tests)之一,其格式遵循 pandoc 测试惯例:文件头部的 fenced code block 内写有一条完整的 CLI 调用与输入文档,^D之后是期望的标准输出。该用例完整内容如下:
% pandoc -t revealjs --slide-level=2 # Title 1 ## Slide 1 Text. ::: fragment ### Sub Slide header Text. ::: ## Slide 2 Text. ^D <section> <section id="title-1" class="title-slide slide level1"> <h1>Title 1</h1> </section> <section id="slide-1" class="slide level2"> <h2>Slide 1</h2> <p>Text.</p> <div class="fragment"> <h3 id="sub-slide-header">Sub Slide header</h3> <p>Text.</p> </div> </section> <section id="slide-2" class="slide level2"> <h2>Slide 2</h2> <p>Text.</p> </section></section>该用例验证了三件关键事情:
--slide-level=2的显式指定:配合两级标题结构,将 level-2 标题## Slide 1、## Slide 2各自切分为一张独立幻灯片;- reveal.js 特有的二维嵌套结构:所有 level-2 幻灯片被包裹在一个外层
<section>中,而 level-1 标题# Title 1生成的是带title-slide类的"标题幻灯片"; ::: fragment容器的转换:自定义 Div 容器被输出为<div class="fragment">,从而在 reveal.js 中实现内容的分步(逐步)显示。
值得注意的是,level-3 标题### Sub Slide header没有被切分为新幻灯片,而是作为 level-2 幻灯片内部的内容呈现,这正体现了"标题层级低于 slide level 时,只生成幻灯片内的标题"这一规则。
这些 command tests 由 test/Tests/Command.hs 驱动执行,是 pandoc 保证各输出格式行为稳定、防止回归的重要手段。
二、slide level 的确定:自动检测与手动覆盖
2.1 默认行为:自动推断 slide level
当用户没有显式指定--slide-level时,pandoc 会根据文档结构自动推断。其算法实现在 src/Text/Pandoc/Slides.hs 的getSlideLevel函数中:
getSlideLevel :: [Block] -> Int getSlideLevel = go 6 where go least (Header n _ _ : x : xs) | n < least && nonHOrHR x = go n xs | otherwise = go least (x:xs) go least (Div _ bs : xs) = min (go least bs) (go least xs) go least (_ : xs) = go least xs go least [] = least nonHOrHR Header{} = False nonHOrHR HorizontalRule = False nonHOrHR _ = True其核心逻辑是:slide level 被定义为"层级最高(数字最小)、且其后直接跟有非标题、非分隔线内容的标题层级"。从源码结构可以推断,pandoc 从 level 6 开始向上扫描,一旦发现某个标题后面紧跟的是正文内容(nonHOrHR为真),就将其层级记为候选,最终取最小值作为 slide level。
作为佐证,MANUAL.txt 对这一默认行为给出了权威说明:
By default, theslide levelis the highest heading level in the hierarchy that is followed immediately by content, and not another heading, somewhere in the document.
2.2 手动指定:--slide-level 的取值范围与校验
--slide-level命令行选项在 src/Text/Pandoc/App/CommandLineOptions.hs 中解析,其约束是取值必须在 0 到 6 之间,否则直接报错:
, option "" ["slide-level"] (ReqArg (\arg opt -> case safeStrRead arg of Just t | t >= 0 && t <= 6 -> return opt { optSlideLevel = Just t } _ -> optError $ PandocOptionError "Argument of --slide-level must be a number between 0 and 6") "NUMBER") Files (T.pack "Header level used for slides")- 取值0:表示不按标题切分幻灯片,pandoc 只把水平分隔线(
---)当作幻灯片边界,生成一维布局; - 取值1~6:与 Markdown 的六个标题层级一一对应,该层级的标题将开启新幻灯片。
从 src/Text/Pandoc/Writers/HTML.hs 可以看到两者的结合方式——用户显式指定的值优先,否则回退到自动检测结果:
let slideLevel = fromMaybe (getSlideLevel blocks) $ writerSlideLevel opts modify $ \st -> st{ stSlideLevel = slideLevel }2.3 演示:自动推断如何得出 level 2
在 8098.md 的输入中,# Title 1(level 1)后面紧跟的是## Slide 1(level 2 标题)而非正文,因此# Title 1不满足"紧跟内容"条件;而## Slide 1后面紧跟正文Text.,满足条件,故自动推断出的 slide level 为 2。测试用例仍显式写出--slide-level=2,一方面是为了让测试意图一目了然,另一方面也绕开了文档内容变化对自动推断结果的干扰,使测试更加稳定。
三、切片预处理:prepSlides 如何把文档切成幻灯片块
在确定了 slide level 之后,pandoc 并不会直接拿着原始 block 列表输出,而是先经过 src/Text/Pandoc/Slides.hs 的prepSlides预处理,再由makeSectionsWithOffsets构建分节树(调用点见 HTML.hs)。
prepSlides slideLevel = ensureStartWithH . splitHrule . extractRefsHeader where splitHrule (HorizontalRule : Header n attr xs : ys) | n == slideLevel = Header slideLevel attr xs : splitHrule ys splitHrule (HorizontalRule : xs) = Header slideLevel nullAttr [Str "\0"] : splitHrule xs splitHrule (x : xs) = x : splitHrule xs splitHrule [] = [] ... ensureStartWithH bs = Header slideLevel nullAttr [Str "\0"] : bs从实现可以看出预处理承担了三项职责:
- 把水平分隔线转换成 slide-level 的空标题:
---之后若紧跟同层级标题则合并,否则插入一个内容为\0的标记标题,后面 HTML writer 检测到这个标记时不输出任何标题内容(见 HTML.hs:if ils == [Str "\0"] then return mempty),从而实现"分隔线总是开启新幻灯片"; - 提取参考文献标题:
extractRefsHeader将末尾参考文献 Div 内的标题移出,避免干扰切片; - 确保文档以标题开头:若文档首块不是标题,则补一个空标题,保证切片结构完整。
四、reveal.js 输出的核心:二维嵌套 section 结构
4.1 类名与属性生成规则
每个被切分出的区块在 HTML.hs 中生成 CSS 类名:
let classes' = ["title-slide" | titleSlide] ++ ["slide" | slide] ++ ["section" | (slide || writerSectionDivs opts) && not html5 ] ++ ["level" <> tshow level | slide || writerSectionDivs opts ]据此,8098.md 输出的类名可以一一对应:
| 输出元素 | 类名 | 含义 |
|---|---|---|
<section id="title-1" class="title-slide slide level1"> | title-slide slide level1 | level-1 标题生成的标题幻灯片 |
<section id="slide-1" class="slide level2"> | slide level2 | level-2 标题生成的普通幻灯片 |
<div class="fragment"> | fragment | 分步展示容器 |
注意输出中class="title-slide slide level1"同时包含title-slide与slide,说明标题幻灯片本身也是一张幻灯片,只是语义上承担"章节封面"的角色。
4.2 二维嵌套的实现细节
reveal.js 最显著的特点是支持二维导航:外层<section>构成水平方向(左右键),内层<section>构成垂直方向(上下键)。8098.md 期望输出中的外层<section>包裹全部内容,正是这种二维布局的体现。
这一嵌套逻辑在 HTML.hs 中有明确实现:
if titleSlide then do t <- addAttrs opts attr $ secttag $ nl <> header' <> nl <> titleContents <> nl -- ensure 2D nesting for revealjs, but only for one level; -- revealjs doesn't like more than one level of nesting return $ if slideVariant == RevealJsSlides && not inSection && not (null innerSecs) then H5.section (nl <> t <> nl <> innerContents) else t <> nl <> if null innerSecs then mempty else innerContents <> nl结合 MANUAL.txt 的说明可以总结出 reveal.js 的布局约定:
- slide level 为 2 时:产生二维布局,level-1 标题在水平方向推进,level-2 标题在垂直方向推进;
- 只允许一层嵌套:源码注释明确写道 "revealjs doesn't like more than one level of nesting",因此 pandoc 不会生成更深的多级嵌套;
--slide-level=0时的降级:reveal.js 退化为只按水平分隔线切片的一维布局,规避深层嵌套问题。
这就是 8098.md 中所有 level-2 幻灯片被包进同一个外层<section>、而 level-1 标题幻灯片与其平级并列在其中的根本原因。
五、fragment 分步显示:从 Markdown 容器到 HTML class
5.1 语法与输出
在 8098.md 中,输入使用了 pandoc 的 fenced div 语法:
::: fragment ### Sub Slide header Text. :::输出为:
<div class="fragment"> <h3 id="sub-slide-header">Sub Slide header</h3> <p>Text.</p> </div>即:任意带fragment类的 Div 容器,在 reveal.js 输出中会原样保留为<div class="fragment">,由 reveal.js 的运行时 CSS/JS 将其内容变为"按点击逐步显现"的动画元素。这也是 reveal.js 模板默认开启fragments变量的原因——HTML.hs 中写有defField "fragments" True . defField "fragmentInURL" True,确保模板默认启用分步显示能力。
5.2 源码中的分支处理
fragment 类并非在所有幻灯片格式中都叫fragment。HTML.hs 中有一个针对不同幻灯片变体的类名分支:
let fragmentClass = case slideVariant of RevealJsSlides -> "fragment" _ -> "incremental"也就是说,reveal.js 使用fragment,而 s5、slidy 等其他 HTML 幻灯片格式使用incremental。此外,pandoc 还支持用-i/--incremental让普通列表也逐步显示(CommandLineOptions.hs),且 reveal.js 下列表项的分步显示同样通过class_ "fragment"实现(见 HTML.hs 的listop $ mconcat $ map (! A.class_ "fragment") items)。
六、实战验证与延伸
6.1 复现测试用例
在仓库根目录执行以下命令即可复现 8098.md 的期望输出(文件路径可从 test/command 目录读取):
pandoc -t revealjs --slide-level=2 test/command/8098.md注意该命令仅输出 HTML 片段(fragment),如需生成可在浏览器中播放的完整演示文稿,应追加-s/--standalone选项:
pandoc -t revealjs -s --slide-level=2 test/command/8098.md -o slides.html6.2 结构规则的完整速查
结合 MANUAL.txt 与 Slides.hs 的实现,pandoc 对幻灯片的分节规则可以归纳如下:
| 文档结构 | 对幻灯片的影响 |
|---|---|
水平分隔线--- | 总是开启新幻灯片(经splitHrule转为标记标题) |
| 与 slide level 同级的标题 | 总是开启新幻灯片 |
| 低于 slide level 的标题(数字更大) | 成为幻灯片内部的标题(beamer 中对应 block 环境) |
| 高于 slide level 的标题(数字更小) | 生成title-slide标题幻灯片,把演示分成若干章节 |
文档 YAML 元数据中的title | 自动生成独立的标题页 |
6.3 常见问题排查
- 为什么我的 level-1 标题全部变成了封面页?因为自动推断(或你指定的)slide level 不是 1。若希望每个 level-1 标题都是一张独立幻灯片,显式指定
--slide-level=1即可;若完全不希望标题切分幻灯片,可用--slide-level=0。 - 为什么嵌套 section 只有两层?这是 reveal.js 二维布局的固有限制,pandoc 源码明确只做一层嵌套;更深的多级标题会作为幻灯片内内容而非独立幻灯片输出。
::: fragment没有生效?请确认输出格式确实是revealjs(s5/slidy 等其他格式会输出incremental类),且使用支持 fragments 的 reveal.js 主题;同时可通过-V fragments=false关闭该特性。
七、总结
test/command/8098.md 虽然只有三十余行,却精准覆盖了 pandoc reveal.js 输出的三大支柱:slide level 的自动推断与手动覆盖(Slides.hs 的getSlideLevel与 CommandLineOptions.hs 的参数校验)、二维嵌套 section 结构的生成(HTML.hs 的嵌套逻辑与类名规则)、以及fragment 分步显示(HTML.hs 的类名分支)。理解这三者,你就能完全掌控 pandoc 到 reveal.js 的转换行为,无论是默认推断还是显式指定 slide level,都能准确预测每一级标题最终在演示文稿中的位置与形态。对于更深入的配置项(如revealjs-url等模板变量),可继续查阅 MANUAL.txt 中 "Variables for HTML slides" 一节以及 data/templates/default.revealjs 模板。
- 文档
- 开发工具
- CLI
【免费下载链接】pandoc
Universal markup converter
相关推荐
Pandoc 演示文稿幻灯片层级控制:--slide-level 深度解析与 revealjs/beamer 输出验证
Pandoc 演示文稿幻灯片层级控制: slide level 深度解析与 revealjs/beamer 输出验证 导读 本文以 pandoc 仓库中的回归测
文档开发工具CLIPandoc Beamer 幻灯片实战:用 --slide-level 与 columns 分栏精准控制帧结构
Pandoc Beamer 幻灯片实战:用 slide level 与 columns 分栏精准控制帧结构 导读 本文以 pandoc 官方测试用例 test/
文档开发工具CLIPandoc 幻灯片分栏输出指南:从 HTML/Reveal.js 到 LaTeX/Beamer 的 columns 布局深入解析
Pandoc 幻灯片分栏输出指南:从 HTML/Reveal.js 到 LaTeX/Beamer 的 columns 布局深入解析 本指南以 pandoc 命令
文档开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考