Org节点属性(实现)
概述见应用一: Org节点属性。
1 实现org-N
org-N 实质只是对 org-referent-get 的封装,出于简写的考虑。如前所述, org-referent-get 的 PROPERTY 比 Org Properties 更加抽象,原因在于 PROPERTY 除了可以是 Org Properties 之外,还可以是一个符号,该符号通常与某个函数关联,该函数被调用于节点所处位置 (某 buffer 某 point), 负责抓取节点的属性值。用代码来描述的话,即:
(org-with-point-at (org-link-find-location LINK)
(funcall property))
由此,我们便可通过自定义的(函数)符号,为 Org节点 定义任意的节点属性,比如定义 #'org-N.location 返回节点所处位置的 marker, 又比如定义 #'org-N.element-type 返回节点的 org-element 类型,最后再借 org-referent-get 或 org-N 获取属性的值。
另外,为了尽可能让接口符合直观,我们还可以对符号进行一定的预拼接工作,使得:
(org-N 'location) 等价于
(org-N #'org-N.location) 等价于
#'org-N.location
org-N 实现:
#+name: 2025-08-20-21-07
#+begin_src emacs-lisp :lexical t :results silent
(defun org-N (property &optional link &rest kargs)
"除 :type 外,所有参数均与 ‘org-referent-get’ 一致。"
(declare (indent 2))
(let ((at-point? (null link))
(type (or (plist-get kargs :type) "N")))
(setq kargs (org-plist-delete kargs :type))
;; 试寻 PROPERTY 完整定义。
;; (org-N 'P) -> (org-N 'org-TYPE.P)
(and-let*
((_ (and type (symbolp property)))
(fn (format "org-%s.%s" type property))
(fn (intern-soft fn))
(_ (fboundp fn)))
(setq property fn))
;; 如 LINK nil, 于当前位置生成 LINK.
(when (and at-point?
;; 以下这些情况无需提取 LINK:
(not (or (stringp property)
(symbolp property)))
(null (plist-get
kargs :no-cache))
(null (plist-get kargs :epom)))
(setq link (org-store-link nil nil))
(unless link
(user-error
"Not link refer to position %S %S"
(current-buffer) (point)))
;; (message "org-N at point: %s" link)
;; 截掉 LINK 的中括号。
(with-temp-buffer
(save-excursion (insert link))
(setq link (org-element-property
:raw-link
(org-element-link-parser)))))
(apply #'org-referent-get link property kargs)))
#+end_src
2 定义节点属性
至此,我们有了 #'org-N.location, #'org-N.element-type 等属性,现进一步展开其实现细节。
当前, org-referent-get 基于“大部分笔记的属性不会频繁更新”的假设,并无缓存更新机制,而是借由 org-N 引入一种手动更新缓存的办法:通过 Emacs命令 获取属性的值并更新其缓存。
org-N 将代表属性的符号与上述手动更新缓存的策略相结合:对于某个属性 PROPERTY, 定义其符号为 #'org-N.PROPERTY, 当 point 于某节点时, 该节点的 PROPERTY 属性的值可通过 M-x org-N.PROPERTY 获取 (PROPERTY 的值将拷贝至 kill-ring), 同时更新其缓存。
为此, org-N 提供一个定义属性的工具——org-N-defprop:
(org-N-defprop PROPERTY BASE ...).
其中, BASE 为属性提取函数——在节点位置上被无参调用的函数。
比如,通过如下方式定义的属性 #'org-N.element-type:
#+name: 2025-08-20-21-08
#+begin_src emacs-lisp :eval no
(org-N-defprop element-type
(lambda nil
(org-element-type
(org-element-at-point-no-context))))
#+end_src
2.1 文档
(org-N-defprop property base &rest kargs)
#+name: 2025-08-20-21-09
#+begin: elisp-docstring
此宏会定义一个名为 ‘org-TYPE.PROPERTY’ 的 Emacs 命令。
‘org-TYPE.PROPERTY’ 以 BASE 为基础,获取并返回属性值。
作为命令调用时,更新属性缓存,并拷贝属性值至 kill-ring.
PROPERTY 为 unquoted symbol. BASE 可以是 string,
quoted symbol, 无参 lambda, 用于从节点中提取属性。详见
‘org-referent-get’.
KARGS:
:type, 指定 TYPE, 可选,默认 “N”.
(org-N-defprop id "ID") => org-N.id
(org-N-defprop id "ID" :type 'id) => org-id.id
:get, ‘org-TYPE.PROPERTY’的实现,可选。
get 为一个可零参调用的 lambda, 调用后返回 PROPERTY
的属性值。其执行环境中存在一个绑定变量 fn, 其值为
‘org-TYPE.PROPERTY’. 该 lambda可借
(eq this-command fn) 判断是否为交互式调用。
‘org-N-defprop’ 对 get 有一个要求:当交互式调用时,
强制更新属性缓存。用例:
(org-N-defprop ...
:get
(lambda (&optional any variables)
(if (eq this-command fn)
(get-value-update)
(get-value))))
:apply, :depends 同 ‘org-referent-get’.
#+end:
2.2 实现
org-N-defprop 实现:
#+name: 2025-08-20-21-10
#+begin_src emacs-lisp :lexical t :results silent
(defmacro org-N-defprop (property base &rest kargs)
"定义 Org节点 属性。
<<@([[id:org-N::doc:org-N-defprop]])>>"
(declare (indent defun))
(let* ((type (or (plist-get kargs :type) "N"))
;; fn 名称
(fn (format "org-%s.%s" type property))
(fn (intern fn))
;; fn 默认实现
(fn-impl
`(lambda (&optional link use-cache)
"LINK: Org 链接。
USE-CACHE: nil 禁缓存, t 用缓存, \\='update 更新缓存。"
;; variables from env: fn, apply.
(let ((use-cache
(if (eq this-command fn)
'update use-cache))
kargs)
(pcase use-cache
(`nil (setq kargs `(:no-cache t)))
(`update (setq kargs `(:force t))))
(apply #'org-N fn link kargs))))
(fn-impl (or (plist-get kargs :get) fn-impl))
(fn-arglist (help-function-arglist fn-impl))
(fn-args (seq-difference
fn-arglist '(&optional &rest)))
(fn-doc (or (documentation fn-impl) "")))
;; 根据 BASE 及 fn-impl 生成 docstring.
(if (or (stringp base)
(and (cadr base) (symbolp (cadr base))))
;; string 或 quoted symbol.
(setq fn-doc (format
"返回基于 %S 的节点属性。
交互式调用时,强制更新属性缓存,并拷贝属性值至 kill-ring.
%s"
base fn-doc))
;; lambda
(when (functionp base)
(setq fn-doc
(format "%s\n\n%s"
(or (documentation base) "")
fn-doc))))
(setq fn-doc (string-trim fn-doc))
;; 定义属性函数
`(!let* ((fn ',fn) (fn-impl ,fn-impl))
(org-referent-get 'set fn :base ,base)
(org-referent-get 'set fn
:apply ,(plist-get kargs :apply))
(org-referent-get 'set fn
:depends ,(plist-get kargs :depends))
(!def fn
(lambda ,fn-arglist
,fn-doc
(interactive)
(let* ((v (fn-impl ,@fn-args)))
(when (eq this-command fn)
(kill-new (format "%s" v))
(message
"Node's %s copied, value: %.50s."
',property v))
v))))))
#+end_src