【存档】Org所指属性

Org所指属性及其缓存


背景

在基于 Org ID 的笔记系统中常常有这样的需求:根据 ID 获取某些属性,或根据某些属性查询某些 ID. 为此, Org mode 为我们提供了诸如 org-entry-get, match string 等接口及机制。但这些接口只限于特定的对象,比如 point, marker, org-element 等。当我们希望以 ID, 甚至更抽象的 Org链接 为对象时, Org mode 尚未具备这类接口。另外,对于大部分笔记,或者说节点(或 headline, 或 src-block, dynamic-block 等所有可被 Org链接 引用的对象),它们大多变更频率低,比如笔记的创建时间属性,几乎不会变更,又比如,某天的日记的内容。为此,为略去“链接定位与跳转”的耗时,我们希望引入一层缓存加速对节点属性的获取。

换言之,我们希望有一个接口:

(org-referent-get LINK PROPERTY ...)

能够跳转到 LINK 所指位置,并获取 PROPERTY 属性的值。

其中,PROPERTY 是一个比 Org Properties 更抽象的概念。

先不谈 org-referent-get 的实现细节。


应用一:Org节点属性

Org节点在本文中指诸如 headline, src-block, dynamic-block 等所有可被 Org链接 引用,并具备某种特性以实现 节点属性 的元素。Org节点的属性值可通过如下接口获取:

(org-N PROPERTY &optional LINK ...)

比如:

(org-N "ITEM" ID-LINK) 等价于
(org-entry-get (org-id-find ID t) "ITEM")

(org-N #'org-get-tags ID-LINK) 等价于
(org-get-tags (org-id-find ID t))

尽管这里以 ID-LINK 为例,但别忘了,我们定义的 Org节点 不止 ID所指 的 Org entry.

实现:见Org节点属性


应用二:Org ID查询

(org-id-select &rest kargs): 查询某些满足特定条件的 IDs.

#+name: 2025-08-17-23-41
#+begin: elisp-docstring

查询匹配中的 Org ID, 返回 ID 集合。

关键字参数:

候选 ID 集

:id-set  候选 ID 集,可选。非 nil 时 ‘org-id-select’
         将从该集合中查询; nil 时从 ‘org-id-locations’
         获取。

用例:

  (org-id-select
    :id-set '(a b c)
    :match "+TODO=\"DONE\"")

  (org-id-select
    :id-set '("a" "b" "c")
    :match "+TODO=\"DONE\"")

Match 字符串

:match  Org Match 字符串,详情见 Info node
        ‘(org)Matching tags and properties’.

用例:

  (org-id-select
    :match "+CREATE_TIME>\"[2025-08-10]\"")

时间范围查询

:tprop   Org 属性,值为 Org 时间戳;
:block   见 Info node ‘(org)The clock table’;
:tstart  见 Info node ‘(org)The clock table’;
:tend    见 Info node ‘(org)The clock table’.

用例:

  (org-id-select
    :tprop "CREATE_TIME" :block '2025-08)

排序

:sort-by  id 到 key 的映射;
:<        key 比较函数。

用例:

  (org-id-select
   :match "+ITEM={Task.*}"
   :tprop "CREATE_TIME" :block 'today
   :sort-by #'org-id->time :< #'time-less-p)

#+end:


Org ID查询(续)

1 根据 MATCH 字符串查询 IDs

为尽可能复用 Org mode 提供的功能, org-id-select 建基于 Org Match String 之上. Org mode 通过 org-make-tags-matcher 将 MATCH 字符串转化为一个 lambda——一个判断节点是否符合要求的谓词。我们直接重构该 lambda, 使其以 ID 为入参,并用 org-N 获取属性。

#+name: 2025-08-17-23-58
#+begin_src emacs-lisp :eval no
(lambda (id-set mstr &optional ref-mode)
  ;; org 会使用 match 字符串构造一个 lambda (具体见
  ;; `org-make-tags-matcher'), 我们直接重构该 lambda,
  ;; 使其以 id 为入参,并用 `org-N' 获取属性。
  (!let* ((fn
           (cl-letf
               ;; 防止 org-make-tags-matcher 编译
               ;; lambda.
               (((symbol-function #'byte-compile)
                 #'identity))
             (seq-remove
              (lambda (s)
                (eq (car-safe s) 'ignore))
              (cdr (org-make-tags-matcher mstr)))))
          (body (flatten-list (car (last fn))))
          ;; 于此 hack 掉原本的 `org-entry-get'.
          (bindings
           `((link id)
             ,(unless ref-mode
                '(link (format "id:%s" id)))
             (org-entry-get
              (lambda (_ P &rest _)
                (org-N P link)))
             ,(when (memq 'todo body)
                '(todo
                  (org-N "TODO" link)))
             ,(when (memq 'tags-list body)
                '(tags-list
                  (string-split
                   (or (org-N "TAGS" link)
                       "")
                   ":" t)))
             ,(when (memq 'level body)
                '(level
                  (org-N "LEVEL" link)))))
          (bindings (seq-remove #'null bindings)))
   ;; 改 lambda 参数列表及变量绑定。
   (setf (nth 1 fn) '(id))
   (setf (car (last fn))
         `(!let* (,@bindings) ,(car (last fn))))
   (seq-filter (byte-compile fn) id-set)))
#+end_src

2 构造 MATCH 字符串

根据 org-id-select 入参构造 Org Match 字符串。

#+name: 2025-08-18-00-01
#+begin_src emacs-lisp :eval no
(lambda (kargs)
  (let* ((match (plist-get kargs :match))
         (block (plist-get kargs :block))
         (tstart (plist-get kargs :tstart))
         (tend (plist-get kargs :tend))
         (tprop0 (plist-get kargs :tprop))
         (tprop1 (or (plist-get kargs :tprop1)
                     tprop0))
         (range
          (when block
            (org-clock-special-range
             block nil t)))
         (tstart (or tstart (car range)))
         (tend (or tend (cadr range)))
         (match
          (if (null tprop0) match
            (concat
             match
             "+" tprop0 ">=\"" tstart "\""
             "+" tprop1 "<\"" tend "\""))))
    match))
#+end_src

3 org-id-select 入口

#+name: 2025-08-18-00-02
#+begin_src emacs-lisp :eval no
(lambda (&rest kargs)
  "Org ID select.

<<@([[id:org-id-select::doc:org-id-select]])>>"
  (let* ((match (make-match-string kargs))
         (ref-mode (plist-get kargs :ref-mode))
         (id-set
          (or (plist-get kargs :id-set)
              (unless ref-mode
                (unless (hash-table-p
                         org-id-locations)
                  (org-id-update-id-locations)
                  nil)
                (when (hash-table-p
                       org-id-locations)
                  (hash-table-keys
                   org-id-locations)))))
         (ids (select-by-match
               id-set match ref-mode))
         (sort-by (plist-get kargs :sort-by))
         (< (or (plist-get kargs :<) #'<))
         (ids (cond
               (sort-by
                (seq-sort-by sort-by < ids))
               ((plist-get kargs :<)
                (seq-sort < ids))
               (t ids))))
    ids))
#+end_src

4 整体结构

#+name: 2025-08-17-23-45
#+begin_src emacs-lisp :eval no :noweb yes
;;; org-id-select  -*- lexical-binding: t; -*-
;; (require 'seq)
;; (require 'org)
;; (require 'org-id)
;; (require 'org-clock)
(!def 'org-id-select
 (!let (org-id-select
        make-match-string select-by-match)

;;;; org-id-select
  (!def org-id-select
   <<@([[id:org-id-select::org-id-select]])>>)

;;;; make-match-string
  (!def make-match-string
   <<@([[id:org-id-select::make-match-string]])>>)

;;;; select-by-match
  (!def select-by-match
   <<@([[id:org-id-select::select-by-match]])>>)

;;;; end
  org-id-select))
#+end_src

构建:Org ID查询

映射表

#+name: 2025-08-23-10-36
#+begin_src emacs-lisp :eval no
"org-id-select"
"https://emacs-china.org/t/org/29965::2025-08-17-23-45"
"org-id-select::org-id-select"
"https://emacs-china.org/t/org/29965::2025-08-18-00-02"
"org-id-select::doc:org-id-select"
"https://emacs-china.org/t/org/29965::2025-08-17-23-41"
"org-id-select::make-match-string"
"https://emacs-china.org/t/org/29965::2025-08-18-00-01"
"org-id-select::select-by-match"
"https://emacs-china.org/t/org/29965::2025-08-17-23-58"
#+end_src

构建目标

#+name: 2025-08-23-10-34
#+begin_src emacs-lisp :eval no :noweb yes
<<@([[id:org-id-select]])>>
#+end_src

构建入口

#+name: 2025-08-23-10-35
#+header: :var tangle=(ignore) load=(ignore) conf-only="no"
#+begin_src emacs-lisp :results silent :noweb yes
(org-id-remap 'reset)
(org-id-remap t)
(org-exec
  "[[https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-27]]" nil
  :eval "yes"
  'target "[[https://emacs-china.org/t/org/29965::2025-08-23-10-34]]"
  'map-table ''("[[https://emacs-china.org/t/org/29965::2025-08-23-10-36]]")
  'tangle (or tangle "~/org/org-id-select.el")
  'load (or load "no")
  'conf-only conf-only)
#+end_src

关于org笔记id的问题, 怎么实现我也想了好久, 最后用的办法是: 时间戳-笔记内容hash值64位, 时间戳也像hash一样转16进制, 然后感觉这个还是太长了, 然后我效仿 比特币 钱包地址用的base58, 写了base60, 16进制转60进制, 这回id终于短了.

不过我的代码是php的,只是拿org-mode做前端,与elisp基本没多少关系就不发了

好奇,为什么你会在意 id 的形式呢?我现在还只用 org 默认的配置,不太想自己改 id 的语义,想尽可能减少开发负担。

确实很多人不在意id的形式, 我以前一直用的是非自动生成id, 手写一个有语义的词来作为id, 后面发现确实没什么意义…… 至于为什么会在意id的形式,可能只是因为一长串内容感觉看起来不好看,但如果不是在org-mode里,在其它地方,我却又不在意id的形式了

1 个赞

了解了。如果不是在 org 里,一大串 hash 看起来确实有点难看。如果是自己 做 UI, 感觉可以仿 org 把 link 隐藏,只显示 description 会好些?

把link隐藏,只显示description在其它地方感觉都挺好的, 但在org-mode, 并排显示较多个link的description, link中url很长很长时, 除了有性能问题还会有些奇怪的bug(也不知如何复现),所以显示在org-mode里的link我都能缩短就缩短

这问题我还真没遇到过,有具体上下文吗?具体并排多少个会有这种问题?我的使用场景里并排的链接最多不过十个,再多我就折行排版了。

没有保留上下文,原因可能是url或description里包含某个特殊字符,然后这个link并排显示时就会打不开,点击还会有错误提示,根据那个错误提示去搜索还能找到相关问题的解决办法。

奇怪之处就在这里了,根据那个错误提示在搜索引擎中搜出来的问题都不是我遇到的问题,我并没有遇到那些问题但却不知怎么就遇到了那个错误提示,实际我遇到的问题也就不知如何解决,只好改变上下文

1 个赞

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节点


拥有ID的Org节点

1 实现

在 org-N 的基础上,特化出具有 Org ID 的 Org节点。

org-id.PROPERTIES

#+name: 2025-08-20-21-02
#+begin_src emacs-lisp :lexical t :results silent
;;; org-id-property  -*- lexical-binding: t; -*-
(defun org-id-property (property &optional id force)
  "Org ID Property.

PROPERTY: 同 ‘org-referent-get’.
ID: Org ID.
FORCE: t 强制更新缓存。

返回: 属性值。"
  (declare (indent 2))
  (let ((at-point? (null id)) epom)
    (when at-point?
      (setq id (org-entry-get nil "ID" t))
      (unless id
        (user-error
         "No ID refer to position %S %S"
         (current-buffer) (point)))
      ;; (message "org-id at point: %s" id)
      ;; 因为上边获取 ID 时使用了 inherit 的方式
      ;; 所以需要定位所指位置。 在此代码路径下,
      ;; ‘org-referent-get’具有LINK 及 :epom
      ;; 信息,无需再定位。
      (setq epom (org-find-entry-with-id id)))
    (org-N property (format "id:%s" id)
      :type 'id :force force :epom epom)))

(defmacro org-id-defprop (name base &rest kargs)
  (declare (indent defun))
  `(org-N-defprop ,name ,base
     :type id
     :get
     (lambda (&optional id force)
       "ID: Org ID.
FORCE: t 强制更新缓存。"
       (org-id-property ',name id
         (or force (eq this-command fn))))
     ,@kargs))
#+end_src

2 基于Org Properties的属性

#+name: 2025-08-20-21-03
#+begin_src emacs-lisp :lexical t :results silent
(org-id-defprop id "ID") ; => #'org-id.id
(org-id-defprop title "ITEM")
(org-id-defprop todo "TODO")
(org-id-defprop closed "CLOSED")
(org-id-defprop timestamp-ia "TIMESTAMP_IA")
#+end_src

3 基于函数符号的属性

#+name: 2025-08-20-21-04
#+begin_src emacs-lisp :lexical t :results silent
(org-id-defprop tags #'org-get-tags)
(org-id-defprop level #'org-current-level)
#+end_src

4 基于lambda的属性

基于lambda的属性: 节点的前链与后链。

#+name: 2025-08-20-21-05
#+begin_src emacs-lisp :lexical t :results silent
(org-id-defprop links
  (lambda nil
    "返回当前节点中所有 ID 链接。"
    (org-with-wide-buffer
     (org-narrow-to-subtree)
     (org-element-map
         (org-element-parse-buffer nil t t)
         '(link)
       (lambda (link)
         (let ((type (org-element-property
                      :type link)))
           (when (string= type "id")
             (intern
              (org-element-property
               :path link)))))))))
(org-id-defprop backlinks
  (lambda nil
    "返回当前节点所有的 ID 后链。"
    (when-let*
        ;; 同 org-id-property 保持一致,使用 inherited.
        ((id (org-entry-get nil "ID" t))
         (_
          (always
           (unless (hash-table-p org-id-locations)
             (org-id-update-id-locations))))
         (scope
          (when (hash-table-p org-id-locations)
            (seq-uniq
             (hash-table-values
              org-id-locations))))
         (bufs
          (mapcar
           (lambda (file)
             (or (find-buffer-visiting file)
                 (find-file-noselect file)))
           scope))
         (backlinks
          (mapcan
           (lambda (buf)
             (let ((re (concat
                        "\\["
                        "\\[id:" id "\\]"
                        "\\(\\[.*?\\]\\)?"
                        "\\]"))
                   (m (make-marker))
                   P)
               (org-with-point-at (set-marker m 1 buf)
                 (save-match-data
                   (while (re-search-forward re nil t)
                     (and-let*
                         ((p (org-entry-get
                              nil "ID" t))
                          (_ (not (string= p id)))
                          (p (intern p))
                          (_ (not (member p P))))
                       (push p P)))))
               P))
           bufs))
         (backlinks
          (seq-remove #'null backlinks)))
      backlinks)))
#+end_src

用例

ELISP> (org-N.location "id:c095c941-c0ad-48ea-8a7d-0cb5dd667b95")
#<marker at 1217 in t.org>

ELISP> (org-id.backlinks 'e06a5d31-f78c-4ecf-ac12-c1147de3775d)
(7e007f81-1d06-4daf-8696-48c49ab33b91
 7bc39c80-3c9b-4e0b-a7d2-85e51de4decf
 1abbf87a-4408-43c3-ae12-f8620b0bf17c
 72e802af-052c-43e2-8b84-3a673214628a
 9482e4ae-2a44-49a8-9bc4-59f7acd97f32
 7e74169f-0f2d-4f63-814f-7766822821c3
 6925c88a-1952-4a4c-aa7a-4887594018ff
 3006cbcc-0aae-4dfc-96bf-8eab71c375a5
 c7c25ec6-3e1e-4b91-80eb-1c0059178bb9
 3fb7ccee-904e-4e39-b49b-04389b6766c2)

构建:Org节点属性

映射表

#+name: 2025-08-23-11-19
#+begin_src emacs-lisp :eval no
"org-N::org-N"
"https://emacs-china.org/t/org/29965::2025-08-20-21-07"
"org-N::org-N-defprop"
"https://emacs-china.org/t/org/29965::2025-08-20-21-10"
"org-N::doc:org-N-defprop"
"https://emacs-china.org/t/org/29965::2025-08-20-21-09"
"org-N::example-properties:location"
"https://emacs-china.org/t/org/29965?page=2::2025-08-23-16-57"
"org-N::example-properties:element-type"
"https://emacs-china.org/t/org/29965::2025-08-20-21-08"
#+end_src

构建目标

#+name: 2025-08-23-11-20
#+begin_src emacs-lisp :eval no :noweb yes
;;; org-N  -*- lexical-binding: t; -*-
;; (require 'org)
;; (require 'org-element)
<<@([[id:org-N::org-N]])>>

<<@([[id:org-N::org-N-defprop]])>>

<<@([[id:org-N::example-properties:location]])>>
<<@([[id:org-N::example-properties:element-type]])>>
#+end_src

构建入口

#+name: 2025-08-23-11-21
#+header: :var tangle=(ignore) load=(ignore) conf-only="no"
#+begin_src emacs-lisp :results silent :noweb yes
(org-id-remap 'reset)
(org-id-remap t)
(org-exec
  "[[https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-27]]" nil
  :eval "yes"
  'target "[[https://emacs-china.org/t/org/29965::2025-08-23-11-20]]"
  'map-table ''("[[https://emacs-china.org/t/org/29965::2025-08-23-11-19]]")
  'tangle (or tangle "~/org/org-N.el")
  'load (or load "no")
  'conf-only conf-only)
#+end_src

构建:拥有ID的Org节点

构建目标

#+name: 2025-08-23-11-03
#+begin_src emacs-lisp :eval no :noweb yes
<<@([[https://emacs-china.org/t/org/29965::2025-08-20-21-02]])>>

<<@([[https://emacs-china.org/t/org/29965::2025-08-20-21-03]])>>
<<@([[https://emacs-china.org/t/org/29965::2025-08-20-21-04]])>>
<<@([[https://emacs-china.org/t/org/29965::2025-08-20-21-05]])>>
#+end_src

构建入口

#+name: 2025-08-20-16-55
#+header: :var tangle=(ignore) load=(ignore) conf-only="no"
#+begin_src emacs-lisp :results silent :noweb yes
(org-id-remap 'reset)
(org-id-remap t)
(org-exec
  "[[https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-27]]" nil
  :eval "yes"
  'target "[[https://emacs-china.org/t/org/29965::2025-08-23-11-03]]"
  'tangle (or tangle "~/org/org-id-property.el")
  'load (or load "no")
  'conf-only conf-only)
#+end_src

Org所指属性


概述

Org所指属性 (org-referent-get), 一个基于 Org链接,涉及链接定位、属性取值、属性值缓存的属性值提取及缓存工具:

(org-referent-get &optional LINK PROPERTY &rest kargs)

注:链接定位 指 根据给定的 Org链接 将 elisp 执行上下文设置为链接所指位置;属性取值 指 于所指位置调用外部定义的属性值提取函数。

应用

考虑如下 Org 片段:

* TODO Task1   :DEVELOP:ORGMODE:
:PROPERTIES:
:ID: a
:END:

提取基于 Org Properties 的 PROPERTY:

(org-referent-get "id:a" "ITEM") 
;; => Task1
(org-referent-get "id:a" "TODO") 
;; => TODO

提取基于 (函数)符号 的 PROPERTY:

(org-referent-get "id:a" #'org-get-tags) 
;; => '("DEVELOP" "ORGMODE")

缓存控制

org-referent-get 通常会以 LINK + PROPERTY 为键值,缓存获取过的属性值,以便略去链接定位、属性取值的过程。基于“大部分所指属性变更频率低”的假设, org-referent-get 内部暂未提供任何主动更新缓存的机制。当缓存与实际数据不一致时,可通过配置 :force:epom 等参数更新缓存:

通过 :force 参数,令 org-referent-get 定位并跳转至 LINK 所指,取值并缓存属性值:

(org-referent-get LINK PROPERTY :force t)

在已知 LINK 所指位置的情况下,通过 :epom 参数传入所指位置,令 org-referent-get 跳转至 epom 处,取值并缓存属性值:

(org-referent-get LINK PROPERTY :epom epom)

注:使用 :epom 参数时,需确保 :epom 与 LINK 所指位置一致!
注:参数 :force 与 :epom 的区别在于有无“链接定位”——一个较为耗时的过程。

如仅为使用 org-referent-get 的定位取值特性,而无需缓存,可通过配置 :no-cache 参数实现:

(org-referent-get LINK PROPERTY :no-cache t)

缓存持久化

为跨 Emacs session 使用, org-referent-get 借助 org-persist 实现内部缓存(二级哈希结构)的持久化:

延迟加载及缓存持久化 (delay-load):

#+name: 2025-08-23-16-38
#+begin_src emacs-lisp :eval no
;; depends: org-referent-get, cache.
(lambda (&rest a)
  "No load. load by

M-x org-referent-get or M-: (org-referent-get)."
  (interactive)
  (!def 'org-referent-get org-referent-get)
  (require 'org-persist)
  (!def cache
   (or (cadr
        (org-persist-read
         "org-referent--cache"
         nil nil nil :read-related t))
       (make-hash-table :test #'equal)))
  (org-persist-register
   `("org-referent--cache" (elisp-data ,cache))
   nil :write-immediately t :expiry 'never)
  (if (called-interactively-p 'interactive)
      (call-interactively #'org-referent-get)
    (apply org-referent-get a)))
#+end_src

需要注意的是,在 elisp 中,并非所有数据结构都能被持久化,但属性值的具体结构非 org-referent-get 所能定义,使用 org-referent-get 的上层软件需自行约束其属性值的结构,否则将导致 org-referent-get 的持久化特性失效。


命令模式

为便于重置、更新缓存, org-referent-get 同时也是一个 Emacs 命令, 其支持的控制说明可通过 M-x org-referent-get 或其实现了解:

do-cmd:

#+name: 2025-08-23-16-41
#+begin_src emacs-lisp :eval no
;; depends: reset-cache, update-cache.
(lambda ()
  (let ((choices
         '((?r "reset" "reset all cache")
           (?d "delete" "delete a cache entry")
           (?u "update" "update all cache")))
        link)
    (pcase (car (read-multiple-choice "?" choices))
      (?r (reset-cache))
      (?u (update-cache))
      (?d (setq link (read-string "Link: "))
          (when (length> link 0)
            (reset-cache `(,link)))))))
#+end_src

缓存更新与重置

实现:

update-cache:

#+name: 2025-08-23-16-43
#+begin_src emacs-lisp :eval no
;; depends: cache, get-prop.
(lambda (&optional links)
  (!let* ((L links)
          (L (or L (hash-table-keys cache)))
          (N (length L)) (Np 0) (i 0)
          (T #'float-time) (Ts (T)))
   (dolist (l L)
     (message
      "Updating cache (%d/%d): %s, %ds"
      (setf i (1+ i)) N l (- (T) Ts))
     (and-let* ((P (gethash l cache))
                (P (hash-table-keys P)))
       (dolist (p P)
         (setf Np (1+ Np))
         (get-prop l p :force t))))
   (message
    (concat
     "%d links %d properties cache updated, "
     "time cost: %ds.")
    N Np (- (T) Ts))))
#+end_src

reset-cache:

#+name: 2025-08-23-16-44
#+begin_src emacs-lisp :eval no
;; depends: cache.
(lambda (&optional links)
  (cond
   (links
    (dolist (l links)
      (remhash l cache)
      (message "link: %s cache deleted." l)))
   (t (clrhash cache)
      (message "All cache reset."))))
#+end_src

缓存共享

考虑这样一种场景:

有两个属性 #'create-time"CREATE_TIME", 它们各自对应 org-referent-get 内部缓存中的两个键值,并经哈希映射至非 eq 的缓存项,然而前者实际 基于 后者——也就是说: #'create-time 最终会通过 "CREATE_TIME" 获取其属性值。我们希望它们中任一方缓存的属性值更新时,另一方的也能同时更新。为此, org-referent-get 通过引用的方式,将 #'create-time 的缓存值关联到 "CREATE_TIME" 的缓存值上:

bind-value:

#+name: 2025-08-23-16-46
#+begin_src emacs-lisp :eval no
;; depends: cache.
(lambda (link prop base)
  ;; org-persist 会破坏此处的引用式值共
  ;; 享,在其 index.eld 中,引用消失,仅
  ;; 剩值。只有属性被强制更新时才会重新建
  ;; 立共享引用。
  (and-let* ((P (gethash link cache))
             (V (last (gethash base P)))
             (C (cons t (cons nil V)))
             (_ (puthash prop C P)))
    ;; 返回属性缓存值
    (car V)))
#+end_src

一个 PROPERTY 可通过如下方式将其属性缓存值绑定到另一个 PROPERTY 上:

(org-referent-get 'set PROPERTY :base P)


缓存获取与设置

为实现缓存共享, org-referent-get 的缓存 (cache) 中并未直接存储通过 PROPERTY 提取的属性值,而是存储具有一定结构的缓存项:

(gethash PROPERTY (gethash LINK cache)) 
;; => cache-item

注:缓存项结构未来极可能发生变化, org-referent-get 外部不应依赖该结构!

org-referent-get 内部有两个用以隐藏缓存项结构细节的函数: get-cache, put-cache, 它们将处理属性值与缓存之间的程序逻辑——处理属性值与缓存项与缓存 (cache) 之间的关系:

根据 LINK 与 PROPERTY 获取属性值 (get-cache):

#+name: 2025-08-23-16-48
#+begin_src emacs-lisp :eval no
;; depends: cache
(lambda (link property &optional all)
  ;; 缓存项 C 的结构: (flag dependent value)
  ;; flag: t/nil, 表示是否有初始化。
  ;; dependent: symbol 类 property 集。
  ;; value: 属性缓存值,可用 (last cache) 共享。
  (and-let* ((P (gethash link cache))
             (C (gethash property P))
             (v (if all C (car (last C)))))))
#+end_src

根据 LINK 与 PROPERTY 设置属性值 (put-cache):

#+name: 2025-08-23-16-49
#+begin_src emacs-lisp :eval no
;; depends: cache, invalidate-dependent, log.
(lambda (link property value)
  (when (and (or (stringp property)
                 (symbolp property))
             link)
    (log "  %S update" property)
    (!let* ((P (or (gethash link cache)
                   (puthash link (make-hash-table
                                  :test #'equal)
                            cache)))
            (C (or (gethash property P)
                   `(nil nil ,value))))
     (cond
      ;; 引用保持。
      ((car C)
       (unless (equal (car (last C)) value)
         (setf (car (last C)) value)
         (invalidate-dependent link property)))
      ;; 缓存初始化。
      ((setf (car C) t)
       (puthash property C P)))))
  value)
#+end_src