惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

W
WeLiveSecurity
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
G
Google Developers Blog
博客园 - 叶小钗
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 三生石上(FineUI控件)
Help Net Security
Help Net Security
Cloudbric
Cloudbric
AI
AI
N
News | PayPal Newsroom
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
SecWiki News
SecWiki News
H
Heimdal Security Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
V
V2EX
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
S
Security Affairs
L
LangChain Blog
The Hacker News
The Hacker News
F
Full Disclosure
aimingoo的专栏
aimingoo的专栏
Hacker News - Newest:
Hacker News - Newest: "LLM"
腾讯CDC
Webroot Blog
Webroot Blog
A
About on SuperTechFans
H
Hacker News: Front Page
Cyberwarzone
Cyberwarzone
WordPress大学
WordPress大学
L
LINUX DO - 热门话题
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
M
MIT News - Artificial intelligence

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
用 ox.el 做你想做的事 —— org-export 高级编程指南
2026-04-24 · via 暗无天日

大多数 Emacs 用户只知道 org-export 能把 Org 文件转成 HTML/PDF,但很少有人意识到它其实是一个高度可扩展的编译框架。你可以继承已有的 backend,覆盖特定元素的渲染逻辑,注入自定义元数据,甚至完全重定义链接的解析方式——所有这些都不需要修改 Org 源码。

本文从 scheatkode 的 ossg 项目1 中提炼出几个实用的 org-export 高级技巧,每个技巧都配有可独立运行的代码。读完之后你会对 ox.el 的架构有一个清晰的认识,知道什么时候该用它、怎么用它。

基础概念:backend 和 transcoder

org-export 用"backend"来定义如何把 Org AST(抽象语法树)转换成目标格式。每个 backend 本质上是一个从"Org 元素类型"到"渲染函数"的映射表。比如 html backend 里, headline 元素对应 org-html-headline 函数, link 元素对应 org-html-link 函数。

这些渲染函数叫 transcoder 。每个 transcoder 接收三个参数:元素本身、可选的描述文字、和一个 info plist(贯穿整个导出过程的通信管道)。

(org-export-backend-transcoders (org-export-get-backend 'html))
((bold . org-html-bold)
 (center-block . org-html-center-block)
 (clock . org-html-clock)
 (code . org-html-code)
 (drawer . org-html-drawer)
 (dynamic-block . org-html-dynamic-block)
 (entity . org-html-entity)
 (example-block . org-html-example-block)
 (export-block . org-html-export-block)
 (export-snippet . org-html-export-snippet)
 (fixed-width . org-html-fixed-width)
 (footnote-reference . org-html-footnote-reference)
 (headline . org-html-headline)
 (horizontal-rule . org-html-horizontal-rule)
 (inline-src-block . org-html-inline-src-block)
 (inlinetask . org-html-inlinetask)
 (inner-template . org-html-inner-template)
 (italic . org-html-italic)
 (item . org-html-item)
 (keyword . org-html-keyword)
 (latex-environment . org-html-latex-environment)
 (latex-fragment . org-html-latex-fragment)
 (line-break . org-html-line-break)
 (link . org-html-link)
 (node-property . org-html-node-property)
 (paragraph . org-html-paragraph)
 (plain-list . org-html-plain-list)
 (plain-text . org-html-plain-text)
 (planning . org-html-planning)
 (property-drawer . org-html-property-drawer)
 (quote-block . org-html-quote-block)
 (radio-target . org-html-radio-target)
 (section . org-html-section)
 (special-block . org-html-special-block)
 (src-block . org-html-src-block)
 (statistics-cookie . org-html-statistics-cookie)
 (strike-through . org-html-strike-through)
 (subscript . org-html-subscript)
 (superscript . org-html-superscript)
 (table . org-html-table)
 (table-cell . org-html-table-cell)
 (table-row . org-html-table-row)
 (target . org-html-target)
 (template . org-html-template)
 (timestamp . org-html-timestamp)
 (underline . org-html-underline)
 (verbatim . org-html-verbatim)
 (verse-block . org-html-verse-block))

这意味着只要覆盖其中任何一个 transcoder,就能改变对应元素的输出方式。

技巧一:继承 backend 并覆盖 transcoder

org-export-define-derived-backend 是整个扩展机制的入口。它从父 backend 继承所有默认行为,然后你只需覆盖想要改动的部分。

下面是一个最小示例:继承 html backend,把所有 verbatim (用 = 包裹的行内代码)渲染成带有 CSS class 的 =<code> 标签,方便自定义样式:

(org-export-define-derived-backend 'my-html 'html
  :translate-alist
  '((verbatim . my-html-verbatim)))

(defun my-html-verbatim (verbatim _contents _info)
  "Render VERBATIM with a custom CSS class."
  (format "<code class=\"inline-code\">%s</code>"
          (org-element-property :value verbatim)))

验证一下效果:

(with-temp-buffer
  (insert "=hello world=\n")
  (org-mode)
  (org-export-as 'my-html))
<p>
<code class="inline-code">hello world</code>
</p>

对比默认的 html backend 输出:

(with-temp-buffer
  (insert "=hello world=\n")
  (org-mode)
  (org-export-as 'html))
<p>
<code>hello world</code>
</p>

区别在于默认输出没有任何 CSS class,而我们的自定义版本加上了 inline-code class,方便统一设置样式。

`★ Insight ─────────────────────────────────────` org-export-define-derived-backend:translate-alist 只需要列出你要覆盖的 transcoder。没有列出的元素类型自动继承父 backend 的行为。这意味着你的自定义 backend 可以只改一个元素(比如只改 verbatim 的渲染),其他所有元素都跟 标准 HTML 导出一模一样。 `─────────────────────────────────────────────────`

技巧二:通过 #+KEYWORD 注入自定义数据到导出管线

ossg 用了一个巧妙的技巧:往 Org 文件里插入 #+OPD_BASE_URL: 这样的自定义 keyword,Org 解析器会自动把它解析进 info plist,transcoder 就能读取到。

org-export-define-derived-backend:options-alist 参数定义了 keyword 到 plist key 的映射:

(org-export-define-derived-backend 'my-html 'html
  :options-alist
  '((:my-color "MY_COLOR"))       :translate-alist
  '((verbatim . my-html-verbatim)))

这样,如果 Org 文件里有 #+MY_COLOR: blue ,transcoder 就能通过 (plist-get info :my-color) 拿到 blue

来做一个完整的例子:根据文件中的 #+MY_COLOR keyword 来改变 verbatim 的输出颜色。

(org-export-define-derived-backend 'my-html 'html
  :options-alist
  '((:my-color "MY_COLOR"))
  :translate-alist
  '((verbatim . my-html-colored-verbatim)))

(defun my-html-colored-verbatim (verbatim _contents info)
  "Render VERBATIM with color from MY_COLOR keyword."
  (let ((color (or (plist-get info :my-color) "inherit")))
    (format "<code style=\"color: %s;\">%s</code>"
            color
            (org-element-property :value verbatim))))

测试:

(with-temp-buffer
  (insert "#+MY_COLOR: red\n\n=colorized text=\n")
  (org-mode)
  (org-export-as 'my-html))
<p>
<code style="color: red;">colorized text</code>
</p>

如果没有设置 #+MY_COLOR ,则回退到 inherit (继承父元素颜色)。

这个技巧的威力在于:你可以在不修改 Org 文件内容的情况下,在构建脚本里动态插入 #+KEYWORD 行,把外部上下文"走私"进导出管线。ossg 就是这样把站点 URL 注入进去的——在渲染前往临时 buffer 里插入一行 #+OPD_BASE_URL: ,org-export 自动解析它,link transcoder 就能读到。

`★ Insight ─────────────────────────────────────` info plist 是贯穿整个导出过程的"通信管道"。它包含文件的所有元数据 (标题、作者、日期、选项等),加上你在 :options-alist 里声明的自定义字段。 每个 transcoder 的第三个参数就是这个 plist。通过往里面注入自定义数据, 你可以实现 transcoder 和外部世界的通信,而不需要全局变量。 `─────────────────────────────────────────────────`

技巧三:自定义 link transcoder

ossg 最核心的技巧之一是自定义 link transcoder,把 Org 文件间的 [[file:xxx.org]] 链接自动替换成最终的 permalink。

link transcoder 接收的 link 参数是一个 org-element 对象,你可以用 org-element-property 读取它的属性:

(defun my-link-info (link desc info)
  "Demonstrate link element properties."
  (let ((type (org-element-property :type link))
        (path (org-element-property :path link))
        (raw-link (org-element-property :raw-link link)))
    (format "<!-- type=%s path=%s raw=%s -->%s"
            type path raw-link
            (org-html-link link desc info))))

先看看 Org 是怎么解析不同类型的链接的:

(org-export-define-derived-backend 'debug-html 'html
  :translate-alist
  '((link . my-link-info)))

(with-temp-buffer
  (insert "* Test links
- [[https://example.com][external link]]
- [[file:other.org][file link]]
- [[file:images/photo.png][image link]]
- [[#my-heading][internal heading link]]

* My Heading
:PROPERTIES:
:CUSTOM_ID: my-heading
:END:
")
  (org-mode)
  (org-export-as 'debug-html))
<ul>
<li><!-- type=https path=//example.com raw=https://example.com --><a href="https://example.com">external link</a></li>
<li><!-- type=file path=other.org raw=file:other.org --><a href="other.html">file link</a></li>
<li><!-- type=file path=images/photo.png raw=file:images/photo.png --><a href="images/photo.png">image link</a></li>
<li><!-- type=custom-id path=my-heading raw=#my-heading --><a href="#my-heading">internal heading link</a></li>
</ul>

可以看到:

  • 外部 URL 的 typehttpspath// 开头的地址部分(不含 scheme)
  • 文件链接的 typefilepath 是文件路径
  • 内部标题链接的 typecustom-id

有了这些信息,就可以实现 ossg 那样的链接重写——检测到 typefilepath.org 结尾时,查表替换成 permalink:

(defvar my-url-registry (make-hash-table :test 'equal)
  "Maps absolute file paths to their final URLs.")

(puthash "/home/user/blog/posts/hello.org" "/posts/hello.html" my-url-registry)

(defun my-translate-link (link desc info)
  "Resolve file: links to .org files using the URL registry."
  (let ((type (org-element-property :type link))
        (path (org-element-property :path link)))
    (if (and (string= type "file")
             (string-suffix-p ".org" path))
                (let* ((abspath (expand-file-name path))
               (url (gethash abspath my-url-registry)))
          (if url
              (format "<a href=\"%s\">%s</a>" url (or desc url))
                        (org-html-link link desc info)))
            (org-html-link link desc info))))

`★ Insight ─────────────────────────────────────` link transcoder 是 org-export 最强大的扩展点之一。 通过检查 typepath 属性,你可以精确控制不同类型链接的输出。 ossg 用它实现了跨路由链接解析,但同样的技巧也可以用于: 把 [[file:*.pdf]] 链接渲染成嵌入式的 PDF 查看器、 把 [[doi:xxx]] 链接渲染成学术引用格式、 或者把特定路径的文件链接渲染成缩略图。 `─────────────────────────────────────────────────`

技巧四:用 cl-progv 做动态变量绑定

ossg 需要在不同路由中覆盖不同的 org-export 全局变量(比如 org-export-with-toc ),但普通的 let 绑定在延迟执行的场景下会失效——因为 let 在求值时就确定了绑定,而实际导出可能在很久之后才发生。

cl-progv 解决了这个问题:它接受一个 运行时 生成的变量名列表和值列表,动态绑定它们。

(cl-progv
    '(org-export-with-toc org-export-with-section-numbers)
    '(nil nil)
      (list org-export-with-toc org-export-with-section-numbers))
(nil nil)

这和 let 的区别在哪?关键在于变量名可以是 数据驱动的

(let ((org-export-with-toc nil))
  org-export-with-toc)  
(let ((env '((org-export-with-toc . nil)
             (org-html-link-home . "https://example.com"))))
  (cl-progv
      (mapcar #'car env)          (mapcar #'cdr env)        (list org-export-with-toc org-html-link-home)))
(nil "https://example.com")

实际应用中, env 可以来自配置数据(比如路由的 :env 属性),不同的路由覆盖不同的变量组合。这让每个路由有自己独立的导出环境,互不污染。

技巧五:org-element-parse-buffer 的 granularity 控制

ossg 在性能优化中发现了一个关键细节: org-element-parse-buffer 的第一个参数控制解析深度。

(org-element-parse-buffer)              
(org-element-parse-buffer 'element)

(org-element-parse-buffer 'greater-element)

(org-element-parse-buffer 'headline)

来看一个具体的例子,感受不同粒度返回的 AST 结构差异:

(with-temp-buffer
  (insert "* My Title
#+DATE: 2026-04-24

Some paragraph with *bold text* and =code=.

** Sub heading
Another paragraph.
")
  (org-mode)
  (let ((ast (org-element-parse-buffer 'greater-element)))
        (format "paragraphs: %d, bold objects: %d"
            (length (org-element-map ast 'paragraph #'identity))
            (length (org-element-map ast 'bold #'identity)))))
"paragraphs: 2, bold objects: 0"

注意 *bold text* 没有被解析成独立的 bold 对象——在 greater-element 粒度下段落内部不会被递归解析。这比完整解析快得多,当你只需要提取标题、日期、标签等顶层元数据时完全够用。

对比完整解析的结果:

(with-temp-buffer
  (insert "* My Title
#+DATE: 2026-04-24

Some paragraph with *bold text* and =code=.

** Sub heading
Another paragraph.
")
  (org-mode)
  (let ((ast (org-element-parse-buffer)))      (format "paragraphs: %d, bold objects: %d"
            (length (org-element-map ast 'paragraph #'identity))
            (length (org-element-map ast 'bold #'identity))))
"paragraphs: 2, bold objects: 1"

完整解析时, *bold text* 被识别为一个独立的 bold 对象。这种精度在你需要精确控制行内标记的 HTML 输出时是必要的,但在只需要元数据的场景下就是浪费。

`★ Insight ─────────────────────────────────────` 选择合适的 granularityorg-element 性能优化的关键。 规则很简单:如果你只需要标题、属性、keyword 等结构性数据, 用 greater-element ;如果你需要操作段落内的行内标记(粗体、链接、代码等), 用 element 或默认的 object 。对于大文件的元数据提取, 这个选择可能带来数倍的性能差异。 `─────────────────────────────────────────────────`

组合起来:一个自定义导出 pipeline

把上面几个技巧组合起来,你可以构建一个完整的自定义导出 pipeline。下面是一个简化的示例,演示了 ossg 的核心思路:

(defvar my-url-registry (make-hash-table :test 'equal))

(org-export-define-derived-backend 'site-html 'html
  :options-alist
  '((:site-base-url "SITE_BASE_URL"))
  :translate-alist
  '((link . my-site-translate-link)))

(defun my-site-translate-link (link desc info)
  "Resolve .org file links using the URL registry."
  (let ((type (org-element-property :type link))
        (path (org-element-property :path link)))
    (if (and (string= type "file")
             (string-suffix-p ".org" path))
        (let ((url (gethash (expand-file-name path) my-url-registry)))
          (if url
              (format "<a href=\"%s\">%s</a>" url (or desc url))
            (org-html-link link desc info)))
      (org-html-link link desc info))))

(defun my-site-export-file (filepath output-path base-url)
  "Export FILEPATH to OUTPUT-PATH with BASE-URL injected."
  (with-temp-buffer
    (insert-file-contents filepath)
    (org-mode)
        (goto-char (point-min))
    (forward-line)
    (insert (format "#+SITE_BASE_URL: %s\n" base-url))
    (let ((html (org-export-as 'site-html)))
      (with-temp-file output-path
        (insert html)))))

这就是 ossg 导出管线的最小核心:一个自定义 backend + 一个 link transcoder + 一个注入元数据的导出函数。ossg 在此基础上加了两遍编译(第一遍注册 URL、第二遍渲染)、路由系统、增量缓存等,但骨架就是这些。

小结

org-export 的可扩展性来自三个核心机制:

  1. Backend 继承 :通过 org-export-define-derived-backend 继承已有 backend,只覆盖需要的部分
  2. Keyword 注入 :通过 :options-alist 声明自定义 keyword,让 Org 解析器自动把文件元数据传进 transcoder 的 info plist
  3. Transcoder 覆盖 :在 :translate-alist 里指定自定义渲染函数,精确控制特定元素类型的输出

这三个机制组合起来,足以在不修改 Org 源码的前提下实现几乎所有自定义导出需求。下次你需要定制 HTML 输出时,不妨先看看能不能用这些机制解决,而不是写后处理脚本。