pandoc revealjs 幻灯片输出的 slide-level 机制与嵌套分节结构实战解析
2026/9/23 4:33:02 网站建设 项目流程
  • 文档
  • 开发工具
  • CLI

【免费下载链接】pandoc

Universal markup converter

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

导读

本文围绕 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>

该用例验证了三件关键事情:

  1. --slide-level=2的显式指定:配合两级标题结构,将 level-2 标题## Slide 1## Slide 2各自切分为一张独立幻灯片;
  2. reveal.js 特有的二维嵌套结构:所有 level-2 幻灯片被包裹在一个外层<section>中,而 level-1 标题# Title 1生成的是带title-slide类的"标题幻灯片";
  3. ::: 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

从实现可以看出预处理承担了三项职责:

  1. 把水平分隔线转换成 slide-level 的空标题---之后若紧跟同层级标题则合并,否则插入一个内容为\0的标记标题,后面 HTML writer 检测到这个标记时不输出任何标题内容(见 HTML.hs:if ils == [Str "\0"] then return mempty),从而实现"分隔线总是开启新幻灯片";
  2. 提取参考文献标题extractRefsHeader将末尾参考文献 Div 内的标题移出,避免干扰切片;
  3. 确保文档以标题开头:若文档首块不是标题,则补一个空标题,保证切片结构完整。

四、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 level1level-1 标题生成的标题幻灯片
<section id="slide-1" class="slide level2">slide level2level-2 标题生成的普通幻灯片
<div class="fragment">fragment分步展示容器

注意输出中class="title-slide slide level1"同时包含title-slideslide,说明标题幻灯片本身也是一张幻灯片,只是语义上承担"章节封面"的角色。

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.html

6.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

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

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

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

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

立即咨询