【存档】Org所指属性


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-getorg-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

3 拥有ID的Org节点