
文档开发工具CLI【免费下载链接】pandocUniversal markup converter项目地址https://gitcode.com/gh_mirrors/pa/pandoc点击查看免费下载pandoc 的--shift-heading-level-by允许在文档转换时将标题heading层级整体上移或下移常用于把章节片段合并进更大文档、或将整本书拆分成分章。本文以命令测试用例 10459.md 为切入点结合 CLI 选项解析、全局 AST 变换transform与 Djot 阅读器的源码实现讲清该选项的正负号语义、标题与 title 元数据的交互规则以及--shift-heading-level-by-1配合 Djot 输入时的一个关键边界行为。测试用例 10459 原文一次 Djot 输入的负向迁移仓库的命令测试golden test位于 test/command/10459.md内容是一段可在终端直接复现的完整会话% pandoc -s --shift-heading-level-by-1 -f djot -t native # hi ^D Pandoc Meta { unMeta fromList [ ( title , MetaInlines [ Str hi ] ) ] } [ Div ( hi , [ section ] , [] ) [] ]逐行解读% pandoc后的命令以-s--standalone开启独立文档模式--shift-heading-level-by-1把所有标题层级减 1-f djot指定输入格式为 Djot-t native把内部 Pandoc AST 以 native 表示打印出来输入正文只有一行# hi即一个 1 级标题内容为纯文本hi转换结果中Meta.unMeta出现( title , MetaInlines [ Str hi ] )即标题文字被提升为文档 title 元数据正文部分只有一个空的Div ( hi , [ section ] , [] ) []不再有任何 Header 节点。可见--shift-heading-level-by-1作用于一个 1 级标题时pandoc 并没有把它降为 0 级不存在 0 级标题而是把它的内容“塞”进了title元数据若标题原本位于 section 包裹内Djot 阅读器的典型产物则 section Div 会被保留、标题本身被移除。这正是本次测试用例想固化的行为。选项的解析与校验从命令行到 Opt 记录--shift-heading-level-by的解析逻辑位于 src/Text/Pandoc/App/CommandLineOptions.hs选项接受一个必填参数NUMBER参数通过safeStrRead尝试解析为整数Int解析失败时直接抛出PandocOptionError错误消息为 “Argument of --shift-heading-level-by must be an integer”。换言之--shift-heading-level-by1.5或非数字参数都会在启动阶段即被拒绝。解析成功后的整数值存入optShiftHeadingLevelBy字段该字段定义于 src/Text/Pandoc/App/Opt.hs默认值为0见同文件 L838表示不做任何迁移。该字段同时支持通过 YAML 配置Opt.hs和 JSONOpt.hs注入因此同样的行为在程序化调用 pandoc 库时也可复现。另一个值得注意的细节是旧选项--base-header-level已被标记为 deprecated其实现CommandLineOptions.hs会提示 “Use --shift-heading-level-by instead.”并把用户给出的基础层级换算为t - 1存入同一个optShiftHeadingLevelBy字段例如旧写法--base-header-level3等价于--shift-heading-level-by2。全局变换的接入点阅读后、写出前的 AST 处理迁移并不是在某个阅读器内部完成的而是由 CLI 主流程在读取并解析输入后统一施加的 AST 变换。相关代码在 src/Text/Pandoc/App.hslet transforms (case optShiftHeadingLevelBy opts of 0 - id x - (headerShift x :)) ...当optShiftHeadingLevelBy为0时变换为恒等函数id否则把headerShift x前置到 transform 列表。由于该变换作用在 Pandoc 文档整体上它天然地同时作用于标题节点Header和文档元数据Meta这也解释了为什么 title 迁移逻辑能在此处实现。headerShift 的实现标题降级与 title 提升规则核心函数headerShift定义于 src/Text/Pandoc/Transforms.hsheaderShift :: Int - Pandoc - Pandoc headerShift n (Pandoc meta (Header m _ ils : bs)) | n 0 , m n 0 headerShift n $ B.setTitle (B.fromList ils) $ Pandoc meta bs -- for this case, see #10459: headerShift n (Pandoc meta (Div attr(_,section:_,_) (Header m _ ils : as) : bs)) | n 0 , m n 0 headerShift n $ B.setTitle (B.fromList ils) $ Pandoc meta (Div attr as : bs) headerShift n (Pandoc meta bs) Pandoc meta (walk shift bs) where shift :: Block - Block shift (Header level attr inner) | level n 0 Header (level n) attr inner | otherwise Para inner shift x x拆解其语义普通标题的降级越界当迁移量n 0且首个块是Header m若m n 0例如 1 级标题减 1则把该标题的 inline 内容通过B.setTitle写入文档Meta的title字段并从正文中移除这个标题section 包裹内的标题本用例的关键分支当首个块是Div且其 class 列表包含section(section:_,_)模式匹配且该 Div 的第一个子块是Header m、m n 0时同样把标题内容提升为 title但保留 Div 本身Div attr as作为正文容器。源码注释-- for this case, see #10459明确指出该分支就是为 issue #10459 的场景而加即 Djot 阅读器输出的 section Div 结构一般情况的遍历其余情况用walk shift遍历所有块Header level在level n 0时平移到level n否则迁移后层级 ≤ 0退化为段落Para inner。也就是说--shift-heading-level-by-1对 1 级标题有两种结局不在 section 内的直接移除并提升为 title在 section 内的则移除标题但保留 section 包裹二者都会把标题内容写进 title 元数据。这正是测试 10459 输出title Str hi、正文只剩Div ( hi , [ section ] , [] ) []的原因。为什么 Djot 输入会产生 section Div阅读器侧的证据本用例特意选择-f djot输入原因在于 Djot 阅读器会为标题生成 section 包裹结构。在 src/Text/Pandoc/Readers/Djot.hs 中Djot 的Section节点被转换为D.Section bls - divWith (,[section],[]) $ convertBlocks bls即一个 class 为section的Div且其 id 来自 section 的标题测试输出中的Div ( hi , [ section ] , [] )中第一个元素hi正是标题# hi的 slug 标识。因此经过 Djot 阅读器后顶层 1 级标题# hi实际处于Div(hi,[section],[])内部触发的是headerShift的第二个分支——保留 section Div、标题内容进入 title 元数据。仓库 test/command/10459.md 正是这一分支的回归测试一旦未来修改破坏该行为例如误删 section Div 或未正确设置 titlegolden test 比对就会失败。正负迁移量的完整行为对照--shift-heading-level-by的整数参数允许正负两个方向可结合测试用例 shift-heading-level-by.md 完整对照参数对# First heading1 级的效果依据1变为 2 级标题## First heading## Second变为 3 级title 保持 YAML 中的My titleshift-heading-level-by.md 第一个用例-1普通 Markdown 输入顶层# First heading提升为 title正文中## Second变为 1 级标题后续的# Another top-level heading因层级越界被降级为普通段落shift-heading-level-by.md 第二个用例-1Djot 输入顶层# hi提升为 title正文保留空Div(hi,[section],[])10459.md对比 shift-heading-level-by.md 第二个用例可看到普通非 Djot输入下标题被提升为 title 后正文中不再保留任何包裹节点而 Djot 输入下 section Div 会被保留。两种输入在--shift-heading-level-by-1下都会发生“标题内容 → title 元数据”的迁移区别只在于正文结构是否保留 section 容器。实战如何复现与验证在任意安装了当前仓库所构建的 pandoc 可执行文件的环境中按以下步骤可完整复现测试 10459运行pandoc -s --shift-heading-level-by-1 -f djot -t native输入一行# hi然后按下^DCtrlD结束输入观察输出Meta.unMeta中出现title hi正文只剩一个空的 section Div。若机器上没有 Djot 输入支持可改用等价的普通 Markdown 输入观察标题提升为 title 的效果% pandoc -s --shift-heading-level-by-1 -t native # hi ^D此时正文中# hi会被整体移除title 元数据为hi。两个命令的输出差异恰好演示了headerShift第一、二两个分支的区别。golden test 由 test/command/10459.md 以「输入 → 期望输出」成对形式固化修改相关行为时可直接运行对应命令测试套件验证回归。小结--shift-heading-level-byNUMBER接受整数参数解析与校验在 CommandLineOptions.hs默认0不迁移旧选项--base-header-level已弃用迁移是全局 AST 变换接入点在 App.hs核心算法在 Transforms.hs负向迁移遇到 1 级标题时标题内容被提升进title元数据Djot 输入因阅读器生成Div(...,[section],[])结构触发专门为 #10459 增加的分支正文保留空 section Div测试用例 10459.md 是对该边界行为的回归保护可与 shift-heading-level-by.md 组合作为理解标题层级迁移语义的完整参考。赞分享文档开发工具CLI【免费下载链接】pandocUniversal markup converter项目地址https://gitcode.com/gh_mirrors/pa/pandoc点击查看免费下载相关推荐Pandoc Org 阅读器等宽标记...解析原理与命令测试用例深度解读Pandoc Org 阅读器等宽标记...解析原理与命令测试用例深度解读 导读 本文围绕 Pandoc 仓库中的命令测试用例 test/command/文档开发工具CLIPandoc Markdown 标题解析的换行边界规则以 test/command/5714.md 测试用例为入口Pandoc Markdown 标题解析的换行边界规则以 test/command/5714.md 测试用例为入口 本篇文章以 pandoc 仓库中的命令测试文档开发工具CLIpandoc 的 RST 阅读器如何解析反斜杠转义与内联标记以测试用例 11309 为例pandoc 的 RST 阅读器如何解析反斜杠转义与内联标记以测试用例 11309 为例 本篇文章围绕 pandoc 仓库中的命令测试用例 test/comm文档开发工具CLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考