
Pandoc 源码级解析--preserve-tabs与 Org-mode-i源块如何保留前导制表符【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc本文围绕 pandoc 官方命令测试用例 test/command/10071.md 展开当使用-p/--preserve-tabs读取 Org 文档时#begin_src源块中带有-i标志的行首制表符Tab会被原样保留进最终的 CodeBlock AST 节点。读完本文你将掌握 pandoc 的制表符预处理管线tabFilter、--tab-stop的作用边界以及 Org 读取器-i开关的底层实现并能据此复现、验证与排查同类问题。一、问题场景pandoc 默认会吃掉制表符pandoc 在解析输入之前默认会先把制表符转换为空格这一点在官方手册中有明确说明见 MANUAL.txtBy default, pandoc converts tabs to spaces before parsing its input.换句话说如果一段 Makefile 或其它依赖真实 Tab 字符的代码被写进代码块默认情况下经过 pandoc 转换后行内缩进会变成空格制表符从此消失。对%.o: %.cpp这类 Makefile 规则而言Tab 与空格语义截然不同这种默认行为可能导致转换结果无法直接使用。测试用例 test/command/10071.md 正是针对这一痛点设计的回归测试它验证在--preserve-tabs开启、且 Org 源块声明-i保留缩进时前导制表符必须完整保留。二、测试用例 10071 全解析从命令行到 AST 输出用例全文如下原样复现Leading tabs must be preserved in org mode *src* blocks with the -i flag when pandoc is called with -p/--preserve-tabs. % pandoc -f org -t native --preserve-tabs #begin_src makefile -i %.o: %.cpp $(CXX) -o $ $ #end_src ^D [ CodeBlock ( , [ makefile ] , [] ) %.o: %.cpp\n\t$(CXX)\t-o\t$\t$\n ]逐项拆解命令pandoc -f org -t native --preserve-tabs。输入格式为 Org输出格式为native即 pandoc 内部 AST 的 Haskell 表示并显式开启--preserve-tabs。输入一个 Org 源块#begin_src makefile -i语言为makefile开关为-i块内第二、三行的行首以及$(CXX)与-o、$之间都是真实 Tab 字符^I。输出pandoc 将源块解析为CodeBlock节点类名为[makefile]内容为字符串%.o: %.cpp\n\t$(CXX)\t-o\t$\t$\n——其中的\t证明制表符被原样保留既没有变成空格也没有被丢弃。若去掉--preserve-tabs同一输入中行首的 Tab 会在预处理阶段按制表位换算成空格默认每 4 个空格一档输出中就不会再出现\t。这正是本用例要锁定的行为差异。同类用例还有 test/command/7573.md它以 HTML 片段验证--preserve-tabs对tab字符的保留效果可与本用例互相印证。三、参数入口-p/--preserve-tabs[true|false]是如何被解析的--preserve-tabs的命令行定义位于 src/Text/Pandoc/App/CommandLineOptions.hs, option p [preserve-tabs] (OptArg (\arg opt - do boolValue - readBoolFromOptArg --preserve-tabs/-p arg return opt { optPreserveTabs boolValue }) true|false) OptFlag (T.pack Preserve tabs)关键细节短选项为-p长选项为--preserve-tabs采用OptArg可选参数因此--preserve-tabs、--preserve-tabstrue、--preserve-tabsfalse三种写法都合法裸写-p时等价于true解析结果存入optPreserveTabs字段其类型定义与默认值位于 src/Text/Pandoc/App/Opt.hs 和 src/Text/Pandoc/App/Opt.hsoptPreserveTabs False即默认关闭。在配置层preserve-tabs同样可以作为 defaults 文件的顶层键使用。官方手册的选项与默认配置文件对照表MANUAL.txt给出了等价写法preserve-tabs: true四、核心管线inputSpacesPerTab与tabFilter的分工optPreserveTabs并不会直接进入读取器而是通过制表符预处理这一环生效。在 src/Text/Pandoc/App.hs 中构造输入参数时做了如下映射, inputSpacesPerTab if optPreserveTabs opts then Nothing else Just (optTabStop opts)即开启--preserve-tabs→inputSpacesPerTab Nothing→ 不换算制表符关闭默认→inputSpacesPerTab Just (optTabStop opts)→ 按--tab-stop默认 4见 MANUAL.txt换算。随后src/Text/Pandoc/App/Input.hs 中的readInput将这一参数转化为对tabFilter的调用let convertTabs :: Text - Text convertTabs tabFilter $ case inputSpacesPerTab params of Nothing - 0 Just ts - if readerName elem [t2t, man, tsv] then 0 else ts这里有两处值得注意的特殊分支Nothing - 0tabFilter 0是恒等函数制表符原样通过readerName为t2t、man、tsv时无条件使用0——即这三类读取器始终保留制表符不受--preserve-tabs影响这是由其格式语义决定的。而tabFilter本身的实现位于 src/Text/Pandoc/Shared.hs-- | Convert tabs to spaces. Tabs will be preserved if tab stop is set to 0. tabFilter :: Int -- ^ Tab stop - T.Text -- ^ Input - T.Text tabFilter 0 id tabFilter tabStop T.unlines . map go . T.lines where go s let (s1, s2) T.break ( \t) s in if T.null s2 then s1 else s1 T.replicate (tabStop - (T.length s1 mod tabStop)) go (T.drop 1 s2)其换算规则与常见编辑器的制表位一致遇到\t时补足到下一个tabStop整数倍的列位置tabStop - (当前列 mod tabStop)个空格然后继续处理行内剩余部分。由此可见--tab-stop只决定换算成几个空格而--preserve-tabs决定要不要换算。五、Org 读取器侧的配合-i开关与orgStateTrimLeadBlkIndent--preserve-tabs只是保证制表符不被换算但 Org 源块内容是否保留行首缩进还取决于 Org 读取器对块头开关的处理。测试用例中的-i正是关键。在 src/Text/Pandoc/Readers/Org/Blocks.hs 中-i开关由whitespaceSwitch解析whitespaceSwitch :: Monad m OrgParser m (Char, Maybe Text, SwitchPolarity) whitespaceSwitch do string -i updateState $ \s - s { orgStateTrimLeadBlkIndent False } return (i, Nothing, SwitchMinus)也就是说读到-i后读取器会将状态中的orgStateTrimLeadBlkIndent置为False不再裁剪代码块的公共前导缩进。反之若源块没有-i或明确写了i读取器会先按公共缩进裁剪内容即使制表符被保留行首的 Tab 也会随缩进裁剪一起被移除——这正是本用例必须同时使用-i与--preserve-tabs的原因前者保住缩进后者保住Tab 字符。两者配合后的完整链路为--preserve-tabs │ (App.hs: inputSpacesPerTab Nothing) ▼ readInput (Input.hs: tabFilter 0 id制表符不换算) │ ▼ Org Reader 解析 #begin_src makefile -i │ (Blocks.hs: orgStateTrimLeadBlkIndent False不裁剪行首缩进) ▼ CodeBlock (, [makefile], []) %.o: %.cpp\n\t$(CXX)\t-o\t$\t$\n六、作用边界哪些制表符会受影响官方手册MANUAL.txt对--preserve-tabs的适用范围给出两条重要限制Note that this will only affect tabs in literal code spans and code blocks. Tabs in regular text are always treated as spaces.即只影响字面代码literal code spans与代码块code blocks内的制表符普通段落文本中的制表符始终按空格处理--preserve-tabs对它们无效。另外这一行为同样会传导到 ipynb 场景手册在 ipynb 一节MANUAL.txt明确指出影响 Markdown 读写行为的选项同样作用于 ipynb 的 Markdown 单元格例如--preserve-tabs会阻止单元格中的 Tab 被转换为空格。如果你用 pandoc 在 ipynb 与 Markdown 之间往返转换并依赖 Tab 缩进应保持该选项的一致性。七、本地验证复现该测试用例无需修改仓库直接在命令行复现即可验证pandoc -f org -t native --preserve-tabs #begin_src makefile -i %.o: %.cpp $(CXX) -o $ $ #end_src # 输入完毕后按 Ctrl-D 结束预期输出\t保留[ CodeBlock ( , [ makefile ] , [] ) %.o: %.cpp\n\t$(CXX)\t-o\t$\t$\n ]可以对照做两组实验加深理解去掉--preserve-tabs行首 Tab 会按--tab-stop默认 4换算为空格输出中的\t消失保留--preserve-tabs但去掉-i制表符虽然不被换算但行首缩进会被 Org 读取器按公共缩进裁剪前导 Tab 同样不会出现在结果中。这两个对照实验即可验证本文第三节到第五节所述的参数传递与解析逻辑。八、小结-p/--preserve-tabs[true|false]控制 pandoc 输入预处理阶段的制表符换算开关由 CommandLineOptions.hs 解析、App.hs 映射为inputSpacesPerTab Nothing换算逻辑集中在 Shared.hs 的tabFiltertabFilter 0即恒等函数--tab-stop只影响换算宽度默认 4Org 读取器中-i开关Blocks.hs负责关闭公共缩进裁剪是前导制表符得以保留的另一半前提该选项仅作用于代码 span 与代码块普通文本中的 Tab 始终视为空格测试用例 test/command/10071.md 与 test/command/7573.md 分别从 Org 与 HTML 两个方向锁定了该行为可作为后续修改时的回归基线。【免费下载链接】pandocUniversal markup converter项目地址: https://gitcode.com/gh_mirrors/pa/pandoc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考