no title found
1 kaijuan
格式转换,
典故:“开卷有益”,陶渊明“好读书,不求甚解;每有会意,便欣然忘食”。
体验隐喻:你的工具做的所有事情,最终都指向同一个动作——“开卷”。
用户打开生成的 HTML/PDF,就像翻开一本排印精良的书。这个名字把复杂的格式转换,浓缩成了一个读者最本能的动作,极其有共鸣。
1.1 Arch Design
1.1.1 flatten heading or nested heading?
Markdown的Heading 是“扁平的(Flat)”,而 Org-mode 的 Heading 是“嵌套的树状结构(Tree)”
- Markdown (GFM/CommonMark) 中的 Heading
在标准的 Markdown AST 中,Heading 仅仅指的是标题本身,它直接映射为 HTML 中的 <h1> 到 <h6> 标签。标题下方的正文(段落、列表等)与标题在 AST 中是平级的兄弟节点(Siblings),它们之间没有任何包含关系。
# 标题
这里是正文段落。 Markdown AST 视角(扁平序列):
[
Heading(level: 1, content: "标题"),
Paragraph(content: "这里是正文段落。"), // 与 Heading 是平级的兄弟
] - Org-mode 中的 Heading (Headline)
Org-mode 本质上是一个大纲(Outline)系统。在它的 AST 中,一个 Heading(在 Org 术语中常称为 Node 或 Headline)是一个容器。它不仅包含这行标题文本,还包含该标题下的所有正文内容(Section),甚至包含它下面所有低级别的子 Heading。
#+begin_src org-mode
2 格式转换
转为typst,
- 导出到pdf
- 导出到md
- 导出到word
pandoc /home/touch/Downloads/水浒_第一回_-_DeepSeek_20260516T142649.md -o output.pdf --pdf-engine=typst -V mainfont="Sarasa Gothic SC"
cargo install --locked typst-cli
- md导出
- 精美的pdf导出
md规范
- AST: GFM
- org-mode 统一
通过org-mode/md-mode定义内容、结构 通过IR定义中间结构 IR转typst,配置模板转pdf/html/docx, 是否是最佳实践?
- 有org-mode导出html, 对于自定义code block, 比如begin_zongpi, 输出<div class="zongpi">, 通过css可配置样式。上面的架构,如何支持?
- typst模板,和css的关系。
3 标题
这里是正文段落。 #+end_src
[
Headline {
level: 1,
title: "标题",
section: [
Paragraph("这里是正文段落。") // 正文是 Headline 的子节点
],
children: [] // 如果有 `** 子标题`,会放在这里
}
] Markdown/HTML/Typst天然扁平,org-mode天然嵌套 (树状大纲)
在通用文档转换工程领域(如业界黄金标准 Pandoc),方案 A(扁平 IR)是绝对的行业最佳实践。 在这四种格式(Markdown, HTML, Typst, Org)的交集中,方案 A 胜出的核心原因在于“少数服从多数”以及“复杂度隔离”原则。
如果选择方案 B,为了照顾 Org-mode 一种格式,你必须让 Markdown、HTML 和 Typst 的解析器全部承担“强行变树”的额外复杂度。
方案 A 下,如何优雅地处理 Org-mode? 选择方案 A 并不意味着放弃 Org-mode 的结构,而是把“树状与扁平相互转换”的复杂度,隔离在 Org-mode 自己的 Parser 和 Renderer 中,保持统一 IR 的纯净。
- Org 文本 -> 扁平 IR (Parser 阶段)
在解析 Org-mode 时,采用深度优先遍历(DFS),遍历到 Headline 时先输出一个 Block::Heading,然后递归遍历它的 section 内容和子 Headline,把它们平铺吐进一个扁平的 Vec<Block> 中。
- 扁平 IR -> Org 文本 (Renderer 阶段)
当你要把扁平的 IR 重新写回 Org-mode 时,Renderer 内部使用一个状态机(或者栈)来根据 Heading 的 level 隐式还原嵌套关系。
在 Rust 中,伪代码逻辑非常清晰:
struct OrgRenderer {
// 可以在这里记录当前处于第几级标题,用来处理一些特殊的缩进或缩进抽屉
current_level: u8,
}
impl OrgRenderer {
pub fn render(&mut self, blocks: &[Block]) -> String {
let mut output = String::new();
for block in blocks {
match block {
Block::Heading { level, content, meta } => {
self.current_level = *level;
// 根据元数据还原 Org 特有的 TODO 状态和 Tags
let todo = meta.extensions.get("org_keyword").map(|s| format!("{} ", s)).unwrap_or_default();
let tags = meta.extensions.get("org_tags").map(|s| format!(" :{}─", s)).unwrap_or_default();
let stars = "*".repeat(*level as usize);
output.push_str(&format!("{} {}{}{}\n", stars, todo, self.render_inlines(content), tags));
}
Block::Paragraph { content, .. } => {
// 段落直接渲染,它在 Org 中自然属于上一个 Heading 之下(直到遇到下一个 Heading)
output.push_str(&format!("{}\n\n", self.render_inlines(content)));
}
// ... 处理其他块
_ => {}
}
}
output
}
} 工程结论
- 选方案 A:你的核心 IR 极其轻量、扁平,编写 AST 转换插件(比如统计字数、替换所有链接)会变得无比简单,只需要一个简单的 for 循环或 map。
- 隔离复杂度:让 Org-mode 的适配器自己去处理“拍平”和“复原”的逻辑。这是目前诸如 Pandoc 等成熟编译器项目的标准解法。