Pandoc 的 Org 模式 Example 块解析:缩进保留、-i 开关与嵌套列表边界判定
Pandoc 的 Org 模式 Example 块解析缩进保留、-i 开关与嵌套列表边界判定【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc导读Org-mode 的#begin_example块用于展示需要原样保留的示例文本但它在列表、#name属性和缩进处理上的行为相当微妙稍不留神就会产生与预期不符的输出。本篇文章以 pandoc 仓库的回归测试 test/command/4186.md 为主线结合 Org 读取器源码 逐层拆解 pandoc 解析 example 块的完整规则-i开关如何禁用首行缩进修剪、块内容缩进如何计算、#name与块归属如何决定块的层级以及嵌套列表中“块从属于哪个列表项”的判定逻辑。读完本文你将能够精确预判任意 Org 示例块在 pandoc 转换后的 AST 与 HTML 结构。测试案例 4186一个针对 Org Example 块的回归测试test/command/4186.md 是 pandoc 的命令行回归测试command test。这类测试文件的格式约定是以 包裹的代码块内%开头的行为待执行的 pandoc 命令^D之前是标准输入内容之后为预期输出。测试通过比对命令真实输出与文件中的预期输出来验证解析器行为是否回归。4186 号测试包含两个用例分别针对 Org example 块的两个独立维度-i开关对缩进保留的影响pandoc -f org -t native下#begin_example -i中的前导空格必须完整保留。嵌套列表中的块归属与缩进对齐pandoc -f org -t html下多层列表内多个 example 块的归属属于外层还是内层列表项、#name生成的id属性、以及块对列表结构的影响。两个用例缺一不可第一个验证“缩进保留”这一基础语义第二个验证“缩进如何决定块归属”这一进阶语义二者共同构成对 example 块解析的完整约束。Org Example 块的解析模型从 Org 块到 Pandoc CodeBlock 的映射在 pandoc 的 Org 读取器中#begin_example与#end_example之间包裹的内容被解析为带属性的代码块CodeBlock。入口处Org.hs 的readOrg通过parseOrg调用blockList进入块级解析在 Blocks.hs 的block组合子列表中orgBlock处理#begin_*系列块和example处理冒号开头的行内示例都位列其中。orgBlock读取块类型后按类型分发见 Blocks.hsorgBlock :: PandocMonad m OrgParser m (F Blocks) orgBlock try $ do blockAttrs - blockAttributes blkType - blockHeaderStart ($ blkType) $ case T.toLower blkType of export - exportBlock comment - rawBlockLines (const mempty) html - rawBlockLines (return . B.rawBlock (T.toLower blkType)) latex - rawBlockLines (return . B.rawBlock (T.toLower blkType)) ascii - rawBlockLines (return . B.rawBlock (T.toLower blkType)) example - exampleBlock blockAttrs ... src - codeBlock blockAttrsexampleBlock的具体实现位于 Blocks.hsexampleBlock :: PandocMonad m BlockAttributes - Text - OrgParser m (F Blocks) exampleBlock blockAttrs _label do skipSpaces (classes, kv) - switchesAsAttributes newline content - rawBlockContent example let id fromMaybe mempty $ blockAttrName blockAttrs let codeBlck B.codeBlockWith (id, classes, kv) content return . return $ codeBlck关键点有三块属性先行解析blockAttributes在块头之前读取#name、#caption、#attr_html等属性行支持的行见 Blocks.hs其中name/label会映射为最终CodeBlock的 id开关参数解析switchesAsAttributes解析块头中-i、-n、n等开关转成 classes 与 key-value 属性内容原样读取rawBlockContent example读取直到#end_example大小写不敏感见 Blocks.hs为止的原始文本。这就是 4186 第一个用例预期输出CodeBlock (, [], []) This should retain the four leading spaces\n的来源属性为空、classes 为空、key-value 为空但内容中的四个前导空格被原样保留。缩进修剪机制与 -i 开关example 块的缩进处理是整个语义的核心。rawBlockContentBlocks.hs实现了一套“按最短缩进去公共前缀”的算法rawBlockContent :: Monad m OrgParser m Text - OrgParser m Text rawBlockContent blockEnder try $ do blkLines - manyTill rawLine (try $ skipSpaces * blockEnder) tabStop - getOption readerTabStop trimP - orgStateTrimLeadBlkIndent $ getState -- split lines into indentation/contents tuples let splitLines map (T.span (\c - c || c \t)) blkLines let countSpaces T.foldr (\case {\t - (tabStop ); _ - (1 )}) 0 let shortestIndent foldr (min . countSpaces . fst) maxBound . filter (not . T.null . snd) -- ignore empty lines $ splitLines let tabsToSpaces T.replace \t (T.replicate tabStop ) let reIndent if trimP then (T.drop shortestIndent . tabsToSpaces) else id ...算法要点每行先被拆分为“前导空白”与“内容”两部分制表符按readerTabStop默认 4 列折算求出非空行中最短的前导空白宽度shortestIndent若启用修剪trimP为真则所有行统一去掉这shortestIndent个字符的前缀空行不参与最短缩进计算因此不会因空行而把缩进裁掉。而-i开关的作用正是关闭这一修剪行为。在 Blocks.hs 中whitespaceSwitch :: Monad m OrgParser m (Char, Maybe Text, SwitchPolarity) whitespaceSwitch do string -i updateState $ \s - s { orgStateTrimLeadBlkIndent False } return (i, Nothing, SwitchMinus)-iignore indentation将解析器状态orgStateTrimLeadBlkIndent置为False状态字段定义见 ParserState.hs默认值为True见 ParserState.hs。此时reIndent id每一行内容原样保留。这解释了 4186 第一个用例% pandoc -f org -t native #begin_example -i This should retain the four leading spaces #end_example ^D [ CodeBlock ( , [] , [] ) This should retain the four leading spaces\n ]若去掉-i由于整块内容只有一行且该行前导缩进为 4shortestIndent即为 4四个空格会被全部裁掉加上-i后修剪被禁用四个空格完整进入 CodeBlock 内容。块级状态的一次性特性值得注意的一个实现细节rawBlockContent在读取结束后会把orgStateTrimLeadBlkIndent重新置回TrueBlocks.hsT.unlines (map (uncurry T.append . bimap reIndent commaEscaped) splitLines) $ updateState (\s - s { orgStateTrimLeadBlkIndent True })这意味着-i只对当前这一个块生效而不会泄漏到后续的 example 块。从源码结构可以推断这是为了隔离开关作用域避免一个块的-i意外影响文档后面其他块的缩进修剪保证每个块默认行为的一致性。逗号转义块内内容的安全处理rawBlockContent中的commaEscaped函数Blocks.hs处理了 Org 块内以逗号开头的转义行commaEscaped suff case T.uncons suff of Just (,, cs) | * - T.take 1 cs - cs | # - T.take 2 cs - cs _ - suff在 Org-mode 中块内若要书写* 标题或#begin_*之类的行需要加逗号转义pandoc 在读取块内容时会剥掉这一层逗号恢复真实内容。例如 test/command/9218.md 中就用到了,#begin_src py这样的转义写法。嵌套列表中的 Example 块归属判定4186 的第二个用例远比第一个复杂它展示了 pandoc 解析器对“块属于哪个列表项”的判定规则。先看完整输入% pandoc -f org -t html - depth 1 #name: bob #begin_example -i Vertical alignment is four spaces beyond the appearance of the word depth. #end_example - depth 2 #begin_example Vertically aligned with the second appearance of the word depth. #end_example #begin_example -i Vertical alignment is four spaces beyond the second appearance of the word depth. The begin portion is a component of this deeper list element, so that guarantees that the entire block must be a component of the inner list element. #end_example Still inside the inner list element #name: carrie #begin_example This belongs to the outer list element, and is aligned accordingly, since the NAME attribute is not indented deeply enough. It is not enough for the BEGIN alone to be aligned deeply if the block is meant to have a NAME. #end_example Still in the shallower list element since the preceding example block forced the deeper list element to terminate. Outside all lists. ^D对应的预期 HTML 输出见 test/command/4186.md结构为depth 1列表项内pre idbob#name: bob映射为 id内容行前导缩进 4 被保留-i生效其子列表depth 2项内第一个无-i的 example 块内容行因公共缩进被修剪到列 0depth 2项内第二个#begin_example -i块缩进对齐于外层内容完整保留缩进紧随其后的#name: carrie块虽然#begin_example本身缩进很深但归属于外层列表项id 为carrie的pre出现在depth 1的li下、depth 2的/ul之后两个列表项之间的文本行归属于各自所在层级最后Outside all lists.在/ul之外。从该用例可以提炼出三条判定规则#name的缩进决定块归属而非#begin本身。carrie块中#begin_example的缩进超过 26 列但#name: carrie仅缩进 3 列解析器据此把块判给外层depth 1列表项——正如用例注释所言“It is not enough for the BEGIN alone to be aligned deeply if the block is meant to have a NAME”仅靠 BEGIN 对齐深不够块若带 NAME 则归属由 NAME 决定。块体内容对齐决定了块属于哪个列表项。第二个-i块中#begin_example -i缩进很深与depth 2的内容列对齐因此它“must be a component of the inner list element”必然属于内层列表项其后续文本Still inside the inner list element也继续停留在内层。一个块会终结其所在列表项。carrie块属于外层列表项它迫使更深的depth 2列表项终止之后的Still in the shallower list element...回到浅层列表项中继续。这三条规则共同保证了 Org 文档中“块嵌套在列表里”这种常见结构能够被稳定还原为正确的 HTML 嵌套关系。关联机制块属性#name 与 #caption如何进入 AST#name: bob能成为pre idbob背后是blockAttributes与attrFromBlockAttributes的配合。blockAttributesBlocks.hs解析#name/#label为blockAttrName同时解析#caption为标题、#attr_html为 HTML 属性键值对。attrFromBlockAttributesBlocks.hs进一步把键值对中的id、class提取为 pandoc 属性的 identifier 与 classesattrFromBlockAttributes :: BlockAttributes - Attr attrFromBlockAttributes BlockAttributes{..} let ident fromMaybe mempty $ lookup id blockAttrKeyValues classes maybe [] T.words $ lookup class blockAttrKeyValues kv filter ((notElem [id, class]) . fst) blockAttrKeyValues in (ident, classes, kv)在exampleBlock中blockAttrName即#name被直接用作B.codeBlockWith的第一个参数 idlet id fromMaybe mempty $ blockAttrName blockAttrs let codeBlck B.codeBlockWith (id, classes, kv) content于是#name: bob最终变成 HTML 输出中的pre idbob#name: carrie变成pre idcarrie。这与单元测试 test/Tests/Readers/Org/Block/CodeBlock.hs 中“Code block with caption”用例的断言一致#name: functor-laws产生codeBlockWith (functor-laws, [haskell], [])。Example 块与 Source 块的异同同为#begin_*块example 与 source 块共享块头解析、属性解析与rawBlockContent内容读取机制但存在关键差异维度#begin_example#begin_src内容语义原样文本不做语法标注带语言标识的代码语言参数无只有开关第一个词作为语言映射为 classes如haskell结果块无可跟随#RESULTS:结果块受:exports控制常见开关-i保留缩进、-n/n行号同上另有:exports、:tangle等 header 参数codeHeaderArgsBlocks.hs会解析 src 块的语言词与 babel 参数例如#begin_src emacs-lisp :exports both生成 classes[commonlisp]与 key-value[(org-language,emacs-lisp),(exports,both)]详见 test/Tests/Readers/Org/Block/CodeBlock.hs。而 example 块不存在语言概念switchesAsAttributes只会消费-i、-n等开关。行号开关-n/n由lineNumberSwitchBlocks.hs解析-n 10生成numberLinesclass 与startFrom10属性n生成continuedSourceBlockclass见 Blocks.hs。尽管 4186 未涉及这套开关机制与-i共享switch组合子Blocks.hs行为一致。实战验证与调试建议用 native 输出观察 AST排查 example 块缩进问题时优先使用-t native查看中间 AST它比 HTML 更直接地暴露 CodeBlock 的内容与属性pandoc -f org -t native input.org4186 第一个用例正是这种调试手法的产物。当内容与预期不符时重点检查CodeBlock 的 classes 中是否出现numberLines、continuedSourceBlock等意外 class内容首行的前导空格数量是否符合预期-i缺失时会被按最短缩进裁掉属性三元组(id, classes, kv)中的 id 是否来自#name。用 HTML 输出观察块归属嵌套列表场景下直接检查输出 HTML 中pre与ul/li的嵌套层级。规则是pre出现在哪个li之下就说明块被判定属于哪个列表项。4186 第二个用例中idcarrie的pre位于depth 1的li内、depth 2的/ul之后即为归属外层的铁证。对照既有回归测试仓库中的相关测试可作为行为基准test/command/4186.md本文主题缩进保留 嵌套列表归属test/command/4748.mdexample 块在 reStructuredText 输出下渲染为::字面块test/command/7810.md列表内 example 块的 org 往返输出保持嵌套test/Tests/Readers/Org/Block/CodeBlock.hs单元测试“Example block”断言基础内容与空属性。运行命令测试的方式是make test或直接执行测试套件见 test-pandoc.hs 与 Command.hs其中 4186 用例由命令测试框架驱动pandoc -f org -t native/html完成比对。总结通过 test/command/4186.md 这一个回归测试可以完整还原 pandoc Org 读取器对 example 块的解析语义#begin_example的内容映射为带属性的CodeBlock#name决定 id默认按非空行的最短缩进去除公共前缀-i开关禁用该修剪以完整保留前导空格且-i只作用于当前块在嵌套列表中块的归属由#name与块头/内容的对齐深度共同决定一个深层块会终结其所在列表项块内容支持逗号转义,前缀结束标记#end_example大小写不敏感。理解这些规则即可在编写 Org 文档时准确预判 pandoc 的转换结果也能在遇到缩进或层级异常时快速定位问题根源。参考实现与测试文件Org 读取器入口src/Text/Pandoc/Readers/Org.hs块级解析与 example 块实现src/Text/Pandoc/Readers/Org/Blocks.hs解析器状态字段定义src/Text/Pandoc/Readers/Org/ParserState.hs回归测试本文主题test/command/4186.md相关回归测试test/command/4748.md、test/command/7810.md、test/command/5178.md单元测试test/Tests/Readers/Org/Block/CodeBlock.hsOrg 写出器#begin_example的生成侧src/Text/Pandoc/Writers/Org.hs【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考