上面我们提到了可以使用 #+MACRO:
关键字来定义宏,那么 org-macro 是以何种方式和何种顺序来找到文件中的宏定义的呢? org-macro--collect-macros
通过调用 org-collect-keywords
来收集使用 MACRO
关键字的名字和值。这个函数比较有意思的一点是它会将通过 SETUPFILE
引入的文件中的宏定义也包含进去:
;; org.el line 4519
(let* ((keywords (cons "SETUPFILE" (mapcar #'upcase keywords)))
这也就是说如果你通过 #+SETUPFILE:
引入了一些 settings
,那么 setupfile 中的宏定义也会起作用。 org-collect-keywords
中的注释没有说明它的扫描顺序,不过根据位于 org.el 4536 行的 re-search-forward
可以猜测应该是从上往下的顺序。这也就是说宏在文件中出现位置越靠后,它在 org-collect-keywords
的返回表中的位置就越靠后。
org-macro--collect-macros
添加了 4 个预定义的宏,分别是 author
, email
, title
和 date
,它们通过文件或 SETUPFILE
中的 #+AUTHOR
, #+EMAIL
, #+TITLE
和 #+DATE
获取:
;; org-macro line 124
(defun org-macro--collect-macros ()
"Collect macro definitions in current buffer and setup files.
Return an alist containing all macro templates found."
(let ((templates
`(("author" . ,(org-macro--find-keyword-value "AUTHOR" t))
("email" . ,(org-macro--find-keyword-value "EMAIL"))
("title" . ,(org-macro--find-keyword-value "TITLE" t))
("date" . ,(org-macro--find-date)))))
(pcase (org-collect-keywords '("MACRO"))
(`(("MACRO" . ,values))
(dolist (value values)
(when (string-match "^\\(\\S-+\\)[ \t]*" value)
(let ((name (match-string 1 value))
(definition (substring value (match-end 0))))
(push (cons name definition) templates))))))
templates))
根据该函数的中使用的 push
,我们可知在它的返回表中 4 个预定义的宏位于最后,表头是文件中最后出现的宏,第二元素是倒数第二个出现的宏,以此类推。如果说 org-collect-keywords
得到的是 M1 M2 M3
的话,那么它的返回值是 M3 M2 M1 author email title date
。
在 org-macro-initialize-templates
,也就是宏初始化函数中,使用 org-macro--collect-macros
收集到的宏会被 org-macro--set-templates
进行一次转换得到可用的模板,注意这里的 default
在前面,这也就是说文件中定义的宏具有更高的优先级,如果 default
中有宏与文件宏同名,那么它会被替换掉:
;; org-macro.el line 158
;; org-macro-initialize-templates
;; Install user-defined macros. Local macros have higher
;; precedence than global ones.
(org-macro--set-templates (append default (org-macro--collect-macros)))
随后,该函数将 input-file
和 modification-time
这两个宏插入表中,它们分别是输入文件名和修改时间:
;; org-macro.el line 164
`("input-file" . ,(file-name-nondirectory visited-file))
`("modification-time" . ...)
在最后添加 keyword
, n
, property
和 time
这四个宏,可见它们就是函数:
;; org-macro line 174
;; Install generic macros.
'(("keyword" . (lambda (arg1 &rest _)
(org-macro--find-keyword-value arg1 t)))
("n" . (lambda (&optional arg1 arg2 &rest _)
(org-macro--counter-increment arg1 arg2)))
("property" . (lambda (arg1 &optional arg2 &rest _)
(org-macro--get-property arg1 arg2)))
("time" . (lambda (arg1 &rest _)
(format-time-string arg1))))
最后让我们回到 ox.el 中调用 org-macro-initialize-templates 的位置:
(org-macro-initialize-templates org-export-global-macros)
注意函数的参数 org-export-global-macros
,我们可以使用它来定义全局宏,它将作为 default
参加参与 org-macro-initialize-templates
的调用。
以上就是对收集宏阶段的过程分析,现在让我们举个例子,假设 f1
中有定义 M1
和 M2
, f2
中有定义 M3
和 M4
, org-export-global-macros
中有宏 M5
, M6
(它们都是按顺序出现)。那么,对于以下 org 文件:
#+INCLUDE: f1.org
#+MACRO: M7 $1
#+SETUPFILE: f2.org
#+MACRO: M8 Hello world
我们最终由 org-macro-initialize-templates
得到的宏表就是: (--set-templates M5 M6 M8 M4 M3 M7 M2 M1 author email title date)
+ input-file, modification-time, keyword, n, property, time
。在 +
号前的宏定义会被 org-macro--set-templates
整理为模板。
上一节忘了说了,在处理过程中 org-macro--set-templates
会将列表反序,上一节的例子不足以说明这一点,这里补充一下:
(org-macro--set-templates '(("hello" . "world")
("world" . "hello")))
=> (("world" . "hello") ("hello" . "world"))
所以我们最终得到的顺序应该是 date title email author M1 M2 M7 M3 M4 M8 M6 M5 input-file modification-time keyword n property time
。
我在本地试了以下,先将 org-macro-global-macros
设置为 (("M5" . "foooo") ("M6" . "barrrr"))
,然后在 org-macro.el
中 org-macro-initialize-templates
的末尾插入 (print org-macro-templates)
,添加需要的文件 f1
和 f2
后,我通过 C-c C-e h H
调用导出到 html 的 buffer,最后在 *message*
buffer 中看到了如下输出:
实际上这样的输出出现了几次,原因不明…如果你也像我一样修改了 org-macro.el 中的代码,记得测试之后改回来。
根据这样的处理方法会出现一个比较鬼畜的现象,那就是文件中定义的宏如果出现重名,那么最先定义的宏生效,这和我们一般想的最后定义生效不一致,如果你使用 ox-org
(记得 (require 'ox-org)
)导出以下文件,那么最后得到的结果是 1 而不是 2:
#+MACRO: A 1
#+MACRO: A 2
{{{A}}}
得到的宏列表如下,可见没有
A 2
:
宏,很神奇吧(笑)。
整个导出过程还是有点复杂的,不过总结一下也就以下几条规则:
-
- 宏表顺序为
date, title, email, author
加上 文件中按顺序出现的宏
加上 全局宏表倒序
加上剩下的六个宏
-
- 尽量不要使用
author
, email
, title
, date
, input-file
, modification-time
, keyword
, n
, property
, time
作为宏名,它们算是保留关键字
-
- 可以使用
org-macro-templates
定义全局宏,但如果与文件宏同名,它们会被覆盖
-
- 对于同名 文件 宏,以 最先 出现的宏定义为准,其他同名定义会被覆盖
-
- 对于同名 全局 宏,以 最后 出现的宏定义为准,其他同名定义会被覆盖
文档中对宏的顺序没有做任何说明,可见作者也不认为宏应该被非常复杂地使用。