引用与反向引用
在深入后续之前,我们先定义两个关于 Org entry 的属性:
(org-E.ref &optional node local)
(org-E.backrefs &optional node)
借助 #'org-E.ref, 我们可以生成任意 Org entry 的引用,其返回的可能是一条带中括号的 Org 链接,也可能是个 marker. 总之,我们暂且称它为 REF. 通过 #'org-E.ref 返回的 REF, 我们可以定位 REF 所指的 Org entry; 而 #'org-E.backrefs 返回的是一个 REF 集合。
注:后续为 #'org-E.ref 及 #'org-E.backrefs 的实现细节,可跳过直转下一节。
注:本文提及的 Org entry 特指 org-element-type 为 headline 的 Org 元素。
注:此处关于 Org entry 引用的定义并非唯一,只是为了演示后续的功能而定义。
注: REF 所指的 Org节点 的类型为 Org entry, 而不是 Org Source Block, 或其他什么东西。
Org entry 引用 (#'org-E.ref):
#+name: 2025-08-29-20-08
#+header: :eval yes
#+begin_src emacs-lisp :lexical t :results silent
(!def 'org-E.ref
(!let (org-E.ref store-ref full-ref local-ref)
(!def org-E.ref
(lambda (&optional node local)
;; 因为我们可能返回 marker, 相比于 LINK, marker 是一
;; 种不稳定的引用,因此 ‘org-E.ref’ 不使用
;; ‘org-referent-get’ 的缓存特性。
(org-referent-get node
(lambda nil (store-ref local))
:no-cache t)))
(!def store-ref
;; 我们统一使用加中括号的 LINK, 且不要 DESC.
(lambda (local)
(let* ((link (or ;; 优先使用 ID 链接。
;; ‘org-id-store-link-maybe’ 返回的
;; 链接格式为 id:x.
(org-id-store-link-maybe)
;; ‘org-store-link’ 返的 FILE LINK
;; 格式为 [[file:xx]].
(org-store-link nil nil)
;; 一些无关联文件且无 ID 的 entry 中,
(when local
(and-let*
((cid (org-entry-get
nil "CUSTOM_ID"))
(cid (format
"[[#%s]]" cid))))))))
(cond
;; 如果有 LINK 可引用当前节点,返回 LINK,
((and
link
;; LINK::*headline 类链接属于非法 ref, 因为此
;; 类链接的定位可能不唯一,所以我们不用此类链接。
(length= (string-split link "::*" t) 1)
(setq link
(with-temp-buffer
(save-excursion
(insert link))
(org-element-link-parser))))
(if local (local-ref link) (full-ref link)))
;; 否则使用 marker 引用当前节点。
;; (global) ref 可能是 LINK 或 marker; 但对于
;; local ref 而言,marker 不算 local, 所以,当
;; LINK 不可用时,只有 not local 时才返回 marker.
((not local)
(save-excursion
;; 如其名中 E 所暗示, ‘org-E.ref’ 只是对 Org
;; entry, 或者说 headline, 的引用。
(org-back-to-heading-or-point-min)
;; 如果当前节点非 Org entry, 直接抛异常。
(unless (org-element-type-p
(org-element-at-point)
'(headline))
(error "Not a valid entry at %S %s"
(current-buffer) (point)))
(point-marker)))))))
;; 全局引用。如:
;; [[id:ID::SEARCH-OPTION]],
;; [[file:path::SEARCH-OPTION]].
(!def full-ref
;; link: org-element link
(lambda (link)
(format "[[%s]]"
(org-element-property
:raw-link link))))
;; 局部引用。如:
;; [[#CUSTOM-ID]], [[#SEARCH-OPTION]].
(!def local-ref
;; link: org-element link
(lambda (link)
(pcase (org-element-property :type link)
("file"
(and-let*
((opt (org-element-property
:search-option link))
(_ (format "[[%s]]" opt)))))
("id"
(and-let*
((rl (org-element-property
:raw-link link))
(opt (string-split rl "::" t))
(_ (length> 1 opt))
(opt (car (last opt)))
(_ (format "[[%s]]" opt)))))
("custom-id"
(format "[[%s]]" (org-element-property
:raw-link link))))))
org-E.ref))
#+end_src
Org entry 反向引用 (#'org-E.backrefs):
#+name: 2025-08-29-20-09
#+header: :eval yes
#+begin_src emacs-lisp :lexical t :results silent
(!def 'org-E.backrefs
(!let (org-E.backrefs
backrefs search ref-re)
(!def org-E.backrefs
(lambda (&optional node)
;; 因为我们可能返回 marker, 相比于 LINK, marker 是一
;; 种不稳定的引用,因此 ‘org-E.ref’ 不使用
;; ‘org-referent-get’ 的缓存特性。
(org-referent-get node
backrefs :no-cache t)))
(!def backrefs
(lambda nil
(when-let*
((ref (org-E.ref))
;; org-E.ref 返回的 LINK 总带中括号。
(ref (if (not (stringp ref)) ref
(string-trim
ref "\\[\\[" "\\]\\]")))
(lref (or (org-E.ref nil t) ""))
(lref (string-trim
lref "\\[\\[" "\\]\\]"))
;; 反向引用查找的范围:所有与文件相关的
;; Org buffers.
(bufs
(seq-filter
(lambda (b)
(with-current-buffer b
(and (derived-mode-p 'org-mode)
(buffer-file-name))))
(buffer-list))))
(seq-uniq
(append
;; 对于带 option 型链接,我们需特殊处理。
(when (length> lref 0)
(search lref))
;; external backrefs
(when (stringp ref)
(mapcan
(lambda nil (search ref)) bufs)))))))
(!def search
;; 根据 re 查找 buf 中的引用。
(lambda (ref &optional buf)
(let ((m (make-marker))
(re (ref-re ref)) R
(buf (or buf (current-buffer))))
(org-with-point-at (set-marker m 1 buf)
(save-match-data
(while (re-search-forward re nil t)
(and-let*
((r (org-E.ref))
;; 注意: org-E.ref 可能返回 marker.
(_ (not (equal r ref)))
(_ (not (member r R))))
(push r R)))))
R)))
(!def ref-re
(lambda (ref)
(concat
"\\[" "\\[" ref "\\]"
"\\(\\[.*?\\]\\)?" "\\]")))
org-E.backrefs))
#+end_src