【存档】Org ID Remap

1 背景

尽管 Org Tangle 扩展提供了通过 HTTP-LINK::Option 抓取 HTML 文本块的手段,但 HTTP-LINK 指向的资源(通常)不可变更,当其指向的文本块需要更新时往往导致引用该HTTP-LINK 的文本块不得不跟着更新。

具体而言:

考虑某发布至网络的文本块 A, 其通过 HTTP-LINK 引用其他文本块 B, C, …,

A 的内容因而包含多条 HTTP 链接:

<​<http-to-B>> <​<http-to-C>> ...

http-to-X 指向的文本块有变更——通过新文本块发布至公共网络中——时,该新文本块将对应生成一条新 HTTP 链接: http-to-new-X, 而 A 却仍旧引用旧链接 http-to-X. 为了将 A 升至新版本——引用 http-to-new-X, 我们不得不同时更新 A 的内容。

并且这个更新过程将持续进行至根节点,以致牵一发动全身。

于是,为了尽可能降低更新量,我们考虑将 A 中的 http-to-X 重映射/定向至 http-to-new-X, 以此避免更新 A 的内容。如此,每当有更新时,除了选择重新发布 A 外,我们还可选择更新映射表,以实现更轻量的更新。

:document:
Org ID Remap

在 `org-link-open' 等打开链接的上下文中将 Org ID 链
接重映射为其他链接。

1. 特性

- 支持 ID 到 ID, ID 到 HTTP 的映射。

- ID 到 HTTP 的映射支持 Org 链接缩写特性。

- 基于 `org-persist' 的 HTTP 纯文本缓存。

- 支持 Org Link Search Option.

2. 使用

唯一入口: `org-id-remap'.

零参时, toggle org-id-remap.

单参时:
  (org-id-remap 'reset) 重置重映射;
  (org-id-remap 'enable?) 查询使能情况;
  (org-id-remap nil) 禁用重映射;
  其他使能重映射。

其他情况时,建立映射,调用形式见下:

ID 到 ID 的映射: [[id:drawer1]] -> [[id:drawer]]
(org-id-remap "drawer1" "id:drawer")

ID 到 HTTP 的映射: [[id:drawer]] -> http-link
(org-id-remap
 "drawer"
 (concat
  "https://orgmode.org/manual/Drawers.html"
  ":::DRAWERNAME:"))

与 Org Link Abbreviations 配合:
:end:
#+name: example
#+begin_src emacs-lisp :eval no
(setf (alist-get "emacs-china"
                 org-link-abbrev-alist
                 nil nil #'equal)
      "https://emacs-china.org/t/%s")

(org-id-remap 'reset)

(org-id-remap
 ;; [[id:drawer1]] -> [[id:drawer]]
 "drawer1" "id:drawer"

 ;; [[id:drawer]] -> http-link
 "drawer"
 (concat
  "https://orgmode.org/manual/Drawers.html"
  ":::DRAWERNAME:")

 ;; [[id:!let]]
 "!let"
 (concat
  "emacs-china:pp-fill-lisp/29712/2"
  "::2025-06-29-17-24"))

(org-id-remap t)
#+end_src

2 实现

整体结构:

#+name: org-id-remap
#+begin_src emacs-lisp :eval no :noweb yes
;;; org-id-remap
(!def 'org-id-remap
 (!let* (_
;;; 内部变量
         <<private>>
;;; 命令集(内部)
         <<cmd-set>>)

;;; `org-id-remap' 命令入口
  (!def cmd-entry
   <<cmd-entry>>)

;;; 重映射函数
  ;; 整个重映射逻辑的入口,实现基于
  ;; `org-link-abbrev-alist'.
  (!def remap
   <<remap>>)

;;; 链接有效性检测
  ;; 判断 `link' 是否有效地指向目标。
  ;; 输入: `link', 待映射的链接。
  ;; 输出: `link', 映射后的链接;或 nil, 无效映射。
  (!def link-verify
   <<link-verify>>)

;;; HTTP链接文本缓存
  ;; 缓存 HTTP 页面文本。
  ;; 输入: `link', HTTP 链接。
  ;; 输出: `link', 指向缓存文件的链接;或 nil;或异常。
  (!def cache-http-link
   <<cache-http-link>>)

;;; 命令实现
  <<cmd-impl>>

;;; Org扩展
  ;; Org Link Search Option 扩展。
  ;; 支持 LINK:::DRAWERNAME: 语义。
  (!def search
   <<drawer-search>>)

  ;; Org File Link 扩展。
  ;; 拦截无配置后缀文件链接访问。
  (!def open-file-link
   <<open-file-link>>)

;;; 调试日志
  (!def log
   <<log>>)
  (!def log (log " *log:org-id-remap*"))

;;; org-id-remap End
  (when (fboundp 'org-id-remap)
    (org-id-remap nil))

  cmd-entry))
#+end_src

命令入口:

#+name: cmd-entry
#+begin_src emacs-lisp :eval no
(lambda (&rest args)
  "Org ID 重映射。

将 Org ID 链接重映射为其他链接。
重映射仅在“打开链接”的上下文中有效。

零参时, toggle org-id-remap.

单参时:
  'reset 重置重映射;
  'enable? 查询使能情况;
  nil 禁用重映射;其他使能。

其他情况时,建立 id 映射,调用形式如下:
  (org-id-remap
   \"id1\" \"id:real-target1\"
   \"id2\" \"id:real-target2\"
   ...)"
  (interactive)
  (declare (indent 0))
  (cond
   ((length= args 0)
    (if (enable?) (disable) (enable)))
   ((length= args 1)
    (setq args (car args))
    (cond
     ((eq 'enable? args) (enable?))
     ((eq 'reset args) (reset))
     ((eq 'get-mapping args) (get-mapping))
     ((null args) (disable))
     (t (enable))))
   (t (set-mapping args))))
#+end_src

重映射入口:

#+name: remap
#+begin_src emacs-lisp :eval no
(lambda (&optional id)
  (cond
   ((and
     ;; Org Link 缩写特性可能会在非 打开链接 的上下
     ;; 文中使用,这里我们只处理 打开链接 时的情况。
     (catch 'break
       (mapbacktrace
        (lambda (&rest f)
          (when (memq (cadr f)
                      '(org-open-at-point
                        org-link-open-from-string
                        org-link-open))
            (log "try remap %s..." id)
            (throw 'break t)))))
     ;; 从 mapping 中寻找一个有效目标。
     (let ((links (gethash id mapping))
           id-links other-links new-id-links
           id-links-without-remap)
       ;; 分类处理 links, 递归处理 id-links.
       (while links
         (setq new-id-links nil)
         (mapcar
          (lambda (link)
            ;; 我们在这里移除 link 中可能存在的方括号,
            ;; 以免 link 再次被
            ;; `org-element-link-parser' 用
            ;; `org-link-expand-abbrev' 展开,造成递
            ;; 归。
            (setq link (string-trim
                        link "\\[\\[" "\\]\\]"))
            (cond
             ((and (string-prefix-p "id:" link t)
                   (not (member link id-links)))
              (push link new-id-links)
              (push link id-links))
             (t (unless (member link other-links)
                  (push link other-links)))))
          links)
         (setq links
               (flatten-list
                (mapcar
                 (lambda (link)
                   (gethash
                    (string-trim link "id:")
                    mapping))
                 new-id-links))))

       ;; links 优先级调整。
       ;; 优化:启发式优先级。
       (setq new-id-links id-links)
       (setq id-links nil)
       (mapcar
        (let ((id-links-with-remap
               (hash-table-keys mapping)))
          (lambda (link)
            (if (member (string-trim link "id:")
                        id-links-with-remap)
                (push link id-links)
              (push link id-links-without-remap))))
        new-id-links)
       (setq links `(,@id-links
                     ,@(nreverse other-links)
                     ,@id-links-without-remap))

       ;; 遍历 links, 直至我们找到一个可用的目标。
       (log "pick from %S links" (length links))
       (catch 'break
         (dolist (link links)
           (when (setq link (link-verify link))
             (log "remap id:%s to %s\n" id link)
             (throw 'break link)))
         nil))))

   ;; 找不到 id 的 mapping, 返回其自身。
   ((ignore (log "not found")))
   ((concat "id:" id))))
#+end_src
#+name: link-verify
#+begin_src emacs-lisp :eval no
(lambda (link)
  ;; 对于 id-link, 我们直接尝试 open 它,
  ;; 如无错误,则该链接有效,`link' 被原样返回;
  ;;
  ;; 对于 http-link, 我们将 `link' 指向的资源缓存为
  ;; 纯文本,并尝试跳转到目标,如无错误,则该链接有
  ;; 效,返回缓存文件的链接。
  ;;
  ;; 依赖: log, cache-http-link
  (log "valid? %s..." link)
  (let ((inhibit-message t)
        (message-log-max nil)
        (org-link-search-must-match-exact-headline t))
    (ignore-errors
      ;; 如果 link 非 id-link, 我们预期它将被 expand
      ;; 为 http-link.
      (unless (string-prefix-p "id:" link t)
        (setq link (org-link-expand-abbrev link))
        (unless (string-match-p "^http[s]:" link)
          (error
           "Expect HTTP/S link, but %s was given."
           link))
        (setq link (cache-http-link link))
        (log "cache: %s" link)

        ;; 这里,为了使用我们自定义的 :DRAWERNAME:
        ;; search option, 我们不得不给 link 加上方括
        ;; 号,因为 `org-element-link-parser' 对下面
        ;; 两种链接解析的结果不一致:
        ;;
        ;; file:///~/a.org:::drawer:
        ;; => file:///~/a.org:::drawer
        ;;
        ;; [[file:///~/a.org:::drawer:]]
        ;; => file:///~/a.org:::drawer:
        (setq link (concat "[[" link "]]")))

      ;; 预期 `org-link-open-from-string' 在找不到目
      ;; 标时抛出异常;如果 `link' 可以正常打开,则
      ;; `link' 是我们的目标。
      (log "verify %s..." link)
      (save-window-excursion
        (org-link-open-from-string link))

      (string-trim link "\\[\\[" "\\]\\]"))))
#+end_src
#+name: cache-http-link
#+begin_src emacs-lisp :eval no
(lambda (link)
  ;; 用 `org-persist' 将 `link' 指向的资源存为纯文本。
  ;; 移除缓存: (org-persist-unregister 'url url)
  (log "try locate %s..." link)
  (let* ((url (string-trim-right
               link "::[:]?[^:]*[:]?"))
         (option (string-trim-left
                  link (concat url "::")))
         ;; `org-persist' 的缓存文件后缀无法修改,导
         ;; 致访`.nil' 文件时被迫使用操作系统接口,
         ;; 在此规避。
         ;; (org-file-apps
         ;;  (cons '(t . emacs) org-file-apps))
         (org-resource-download-policy t)
         (file (org-persist-read 'url url)))
    (unless file
      ;; cache and render
      (log "try cache %s..." url)
      (let* ((f (org-persist-register
                 'url url :write-immediately t))
             (buf (find-file-noselect f))
             (shr-inhibit-images t)
             (shr-bullet "- "))
        (with-temp-buffer
          (shr-insert-document
           (with-current-buffer buf
             (libxml-parse-html-region
              (point-min) (point-max))))
          (goto-char (point-min))
          (replace-regexp "^[*]" "# *")
          (buffer-swap-text buf)
          (with-current-buffer buf (save-buffer))
          (log "cache to %s" f))
        (setq file f)))
    (when file
      (concat "file:///" file "::" option))))
#+end_src
#+name: drawer-search
#+begin_src emacs-lisp :eval no
(lambda (option)
  (when (string-match-p org-drawer-regexp
                        option)
    (goto-char (point-min))
    (catch 'found
      (while (re-search-forward
              option nil t)
        (when (org-element-type-p
               (org-element-context)
               'drawer)
          (throw 'found 'drawer))))))
#+end_src
#+name: open-file-link
#+begin_src emacs-lisp :eval no
(lambda (f l)
  ;; `org-persist' 的缓存文件后缀无法配置,导被缓存为
  ;; `*.nil' 的文件访问时被迫使用操作系统接口,为此,
  ;; 我们提供一个默认打开文件的函数,配合
  ;; `org-file-apps' 使用。
  (let* ((s (string-trim l f))
         (s (string-trim s "::")))
    (find-file-other-window f)
    (org-mode)
    (org-link-search s)))
#+end_src

命令实现:

#+name: cmd-set
#+begin_src emacs-lisp :eval no
(enable? nil) (enable nil) (disable nil)
(reset nil) (set-mapping nil)
(get-mapping nil)
#+end_src
#+name: cmd-impl
#+begin_src emacs-lisp :eval no
(!def enable?
 (lambda ()
   (get remap 'org-link-abbrev-safe)))
(!def enable
 (lambda ()
   (put remap 'org-link-abbrev-safe t)
   (setf (alist-get
          "id" org-link-abbrev-alist
          nil nil #'equal)
         remap)
   (push `(t . ,open-file-link)
         org-file-apps)
   (add-hook
    'org-execute-file-search-functions
    search)
   (message "Org ID remap enable.")))
(!def disable
 (lambda ()
   (put remap 'org-link-abbrev-safe nil)
   (setq org-link-abbrev-alist
         (assoc-delete-all
          "id" org-link-abbrev-alist
          #'equal))
   (setq org-file-apps
         (seq-filter
          (lambda (it)
            (not
             (equal
              it `(t . ,open-file-link))))
          org-file-apps))
   (remove-hook
    'org-execute-file-search-functions
    search)
   (message "Org ID remap disable.")))
(!def reset
 (lambda ()
   (clrhash mapping)
   (message "Org ID mapping reset.")))
(!def set-mapping
 (lambda (mappings)
   (mapcar
    (lambda (kv)
      (let ((k (car kv)) (v (cadr kv)))
        (unless
            (member v (gethash k mapping))
          (push v (gethash k mapping)))))
    (seq-partition mappings 2))))
(!def get-mapping
 (lambda ()
   (!let (r)
    (maphash
     (lambda (k vs)
       (mapc
        (lambda (v) (push (list k v) r)) vs))
     mapping)
    (flatten-list r))))
#+end_src

调试及其他:

#+name: log
#+begin_src emacs-lisp :eval no
(lambda (log-target)
  (lambda (fmt &rest args)
    (when debug-on-error
      (let* ((ts (format-time-string
                  "[%Y-%m-%d %H:%M:%S.%3N]"))
             (fmt (concat ts fmt "\n"))
             (buf (get-buffer-create
                   log-target)))
        (with-current-buffer buf
          (goto-char (point-max)))
        (princ (apply #'format fmt args)
               buf)))))
#+end_src
#+name: private
#+begin_src emacs-lisp :eval no
(mapping (make-hash-table :test #'equal))
(cmd-entry nil) (log nil)
(link-verify nil) (cache-http-link nil)
;; `org-link-abbrev-alist' 的元素 (key . val)
;; 中, val 可以是指向 function 的 symbol. 效
;; 果和 "%(sym-of-func)" 类似,但文档中未注明。
(remap (make-symbol "org-id-remap"))
;; 用于打开某些特殊后缀文件。
(open-file-link
 (make-symbol "org-id-remap-open-file"))
;; Org Link Search Option 扩展。
(search (make-symbol "org-id-remap-search"))
#+end_src

3 附录

let改:

#+name: !let
#+begin_src emacs-lisp :eval no
(defmacro !let (bindings &rest body)
  (declare
   (indent
    (lambda (p s)
      (save-excursion
        (goto-char (car (last (nth 9 s))))
        (1+ (current-column))))))
  (cond
   ((null bindings) `(progn ,@body))
   (t
    (let (vars vals)
      (mapc
       (lambda (binding)
         (push (or (car-safe binding) binding) vars)
         (push (car (cdr-safe binding)) vals))
       bindings)
      `(funcall
        (lambda (,@(nreverse vars))
          (cl-macrolet
              ,(mapcar
                (lambda (s)
                  `(,s (&rest args)
                       `(funcall
                         ;;,',s
                         (or (and (functionp ,',s) ,',s)
                             (function ,',s))
                         ,@args)))
                (nreverse vars))
            ,@body))
        ,@(nreverse vals))))))
#+end_src
#+name: !let*
#+begin_src emacs-lisp :eval no
(defmacro !let* (bindings &rest body)
  (declare
   (indent
    (lambda (p s)
      (save-excursion
        (goto-char (car (last (nth 9 s))))
        (1+ (current-column))))))
  (if (null bindings) `(progn ,@body)
    (setq bindings (reverse bindings))
    (while bindings
      (setq body (list `(!let (,(pop bindings))
                          ,@body))))
    (car body)))
#+end_src
#+name: !def
#+begin_src emacs-lisp :eval no
(defmacro !def (sym val)
  (declare
   (indent
    (lambda (p s)
      (save-excursion
        (goto-char (car (last (nth 9 s))))
        (1+ (current-column))))))
  `(!let ((val ,val))
    (if (ignore-errors
          (and ,sym (symbolp ,sym) (functionp val)))
        (defalias ,sym val)
      (setq ,sym val))))
#+end_src

加载用:

#+begin_src emacs-lisp :results silent :lexical t :eval yes :noweb no-export
<<!let>>
<<!let*>>
<<!def>>
<<org-id-remap>>
#+end_src

加载及测试

M-x eww RET https://emacs-china.org/t/org-id-remap/29814 RET
M-x eww-readable
M-x org-mode
M-x read-only-mode
M-x org-babel-execute-buffer
M-x org-id-remap
M-: (org-id-remap "drw" "https://orgmode.org/manual/Drawers.html:::DRAWERNAME:")
M-x org-link-open-from-string RET [[id:drw]] RET

有了 org-id-remap 之后(需自举),将原始 noweb-ref 改为 <​<@(org-link)>> 形式:

#+name: 2025-07-22-21-34
#+begin_src emacs-lisp :results silent :lexical t :eval no
;;; org-id-remap -*- lexical-binding: t; -*-
(!def 'org-id-remap
 (!let* (_
;;; 内部变量
         <<@([[id:org-id-remap::private]])>>
;;; 命令集(内部)
         <<@([[id:org-id-remap::cmd-set]])>>)

;;; `org-id-remap' 命令入口
  (!def cmd-entry
   <<@([[id:org-id-remap::cmd-entry]])>>)

;;; 重映射函数
  ;; 整个重映射逻辑的入口,实现基于
  ;; `org-link-abbrev-alist'.
  (!def remap
   <<@([[id:org-id-remap::remap]])>>)

;;; 链接有效性检测
  ;; 判断 `link' 是否有效地指向目标。
  ;; 输入: `link', 待映射的链接。
  ;; 输出: `link', 映射后的链接;或 nil, 无效映射。
  (!def link-verify
   <<@([[id:org-id-remap::link-verify]])>>)

;;; HTTP链接文本缓存
  ;; 缓存 HTTP 页面文本。
  ;; 输入: `link', HTTP 链接。
  ;; 输出: `link', 指向缓存文件的链接;或 nil;或异常。
  (!def cache-http-link
   <<@([[id:org-id-remap::cache-http-link]])>>)

;;; 命令实现
  <<@([[id:org-id-remap::cmd-impl]])>>

;;; Org扩展
  ;; Org Link Search Option 扩展。
  ;; 支持 LINK:::DRAWERNAME: 语义。
  (!def search
   <<@([[id:org-id-remap::drawer-search]])>>)

  ;; Org File Link 扩展。
  ;; 拦截无配置后缀文件链接访问。
  (!def open-file-link
   <<@([[id:org-id-remap::open-file-link]])>>)

;;; 调试日志
  (!def log
   <<@([[id:org-id-remap::log]])>>)
  (!def log (log " log:org-id-remap"))

;;; org-id-remap End
  (when (fboundp 'org-id-remap)
    (org-id-remap nil))

  cmd-entry))
#+end_src

在新环境中 (emacs -Q), 执行如下操作加载 org-id-remap.

关闭代码块执行确认:

#+begin_src emacs-lisp :eval no
(setq org-confirm-babel-evaluate nil)
#+end_src

M-x (org-babel-execute-buffer). 以下代码块会按彼此之间的依赖顺序执行。


自举

碎片 2025-07-22-21-34 依赖自身 (org-id-remap) 及 org-noweb-expand-link. 这里,我们从云端加载自举用的碎片。

#+name: 2025-07-22-21-03
#+header: :eval no
#+header: :noweb yes
#+begin_src emacs-lisp :results silent
(funcall
 (lambda (org-exec)
   (cond
    ((and
      (if (fboundp 'org-id-remap) t
        (funcall
         org-exec
         "https://emacs-china.org/t/org-id-remap/29814"))
      (if (fboundp 'org-noweb-expand-link) t
        (funcall
         org-exec
         "https://emacs-china.org/t/org-tangle/29663"))
      "yes"))
    (t "no")))
 <<2025-07-22-23-25>>)
#+end_src
#+name: 2025-07-22-23-25
#+begin_src emacs-lisp :eval no
(lambda (link)
  (letrec ((url (string-trim
                 link "\\[\\[" "\\]\\]"))
           (hook (lambda ()
                   (message "done")
                   (remove-hook
                    'eww-after-render-hook hook)
                   (org-mode)
                   (org-babel-execute-buffer)))
           (eww-retrieve-command
            (cond
             ((executable-find "curl")
              '("curl" "-s"))
             (t eww-retrieve-command))))
    (add-hook 'eww-after-render-hook hook)
    (save-window-excursion
      (eww-browse-url url t)
      (with-timeout (60 nil)
        (while (member hook eww-after-render-hook)
          (sit-for 1)
          (message (format-time-string "%s")))
        t))))
#+end_src

建立映射

建立 ID链接 映射。

#+name: 2025-07-22-21-02
#+header: :depend (org-sbe "2025-07-22-21-03" ":eval yes")
#+header: :var ok="yes" err="no" ret=""
#+begin_src emacs-lisp :results silent :eval no
(setq ret err)
(setf
 (alist-get "emacs-china/org-id-remap"
            org-link-abbrev-alist
            nil nil #'equal)
 "https://emacs-china.org/t/org-id-remap/%s")
(setf
 (alist-get "emacs-china/org-tangle"
            org-link-abbrev-alist
            nil nil #'equal)
 "https://emacs-china.org/t/org-tangle/%s")
(ignore-errors
  (org-id-remap 'reset)
  (org-id-remap
   "org-id-remap::org-id-remap"
   "emacs-china/org-id-remap:29814::2025-07-22-21-34"

   "org-id-remap::cmd-entry"
   "emacs-china/org-id-remap:29814::cmd-entry"

   "org-id-remap::remap"
   "emacs-china/org-id-remap:29814::remap"

   "org-id-remap::link-verify"
   "emacs-china/org-id-remap:29814::link-verify"

   "org-id-remap::cache-http-link"
   "emacs-china/org-id-remap:29814::cache-http-link"

   "org-id-remap::drawer-search"
   "emacs-china/org-id-remap:29814::drawer-search"

   "org-id-remap::open-file-link"
   "emacs-china/org-id-remap:29814::open-file-link"

   "org-id-remap::cmd-set"
   "emacs-china/org-id-remap:29814::cmd-set"

   "org-id-remap::cmd-impl"
   "emacs-china/org-id-remap:29814::cmd-impl"

   "org-id-remap::log"
   "emacs-china/org-id-remap:29814::log"

   "org-id-remap::private"
   "emacs-china/org-id-remap:29814::private"

   "org-noweb::org-noweb-expand-link"
   "emacs-china/org-tangle:29663::elisp-2025-06-16-11-01"
   )
  (org-id-remap t)
  (setq ret ok))
ret
#+end_src

前置检测

#+name: 2025-07-22-20-56
#+header: :var ok="yes" err="no"
#+begin_src emacs-lisp :eval no :results silent
(cond
 ((and (fboundp 'org-id-remap)
       (fboundp 'org-noweb-expand-link))
  ok)
 (t err))
#+end_src

Eval 及 Tangle 入口

#+name: 2025-07-22-20-58
#+header: :depend (org-sbe "2025-07-22-21-02" ":eval yes")
#+header: :tangle (org-sbe "2025-07-22-20-56" ":eval yes" (ok \"org-id-remap.el\"))
#+header: :eval (org-sbe "2025-07-22-20-56" ":eval yes")
#+begin_src emacs-lisp :noweb yes :results silent :lexical t
<<@([[id:org-id-remap::org-id-remap]])>>
#+end_src

一些构建用的碎片

前置检测

判断是否具备构建用的环境。(尽管对于我们的目标而言,检测限定的范围有些宽泛。)

此外,这里还配置了 tangle 的默认输出文件位置。

#+name: 2025-07-25-20-35
#+header: :var ok="~/org/org-id-remap.el" err="no"
#+begin_src emacs-lisp :eval no :results silent
(setf
 (alist-get "emacs-china/org-id-remap"
            org-link-abbrev-alist
            nil nil #'equal)
 "https://emacs-china.org/t/org-id-remap/%s")
(setf
 (alist-get "emacs-china/org-tangle"
            org-link-abbrev-alist
            nil nil #'equal)
 "https://emacs-china.org/t/org-tangle/%s")
(cond
 ((and (fboundp 'org-id-remap)
       (fboundp 'org-noweb-expand-link))
  (org-id-remap t)
  ok)
 (t err))
#+end_src

org-id-remap 完整代码,用于 eval 或 tangle.

输入: <<@([[id:org-id-remap::org-id-remap]])>>.

#+name: 2025-07-25-20-36
#+header: :tangle (org-sbe "2025-07-25-20-35" ":eval yes")
#+header: :eval (org-sbe "2025-07-25-20-35" ":eval yes" (ok \"yes\"))
#+begin_src emacs-lisp :noweb yes :results silent :lexical t
<<@([[id:org-id-remap::org-id-remap]])>>
#+end_src

Eval 及 Tangle 入口

根据事先输入的映射表, Eval 并 Tangle org-id-remap.

#+name: 2025-07-25-20-39
#+begin_src emacs-lisp :results silent :eval no
(org-with-wide-buffer
 (org-link-open-from-string "[[2025-07-25-20-36]]")
 ;; 先 tangle 再 exec, 因为加载
 ;; org-id-remap 会重置映射表
 (org-babel-tangle '(4))
 (org-babel-execute-src-block))
#+end_src

一些修复及变更

修复及变更: drawer-search 语义变更; open-file-link 缺陷修复;链接查找优先级重调。

1 drawer-search 语义变更

之前抓取 drawer 的语义为 LINK:::DRAWER:, 其中 :DRAWER: 作为 option 被传入 org-link-search. 然,因为 org-element-link-parserfile​:///~/a.org:::drawer:[​[file​:///~/a.org:::drawer:]] 的解析结果不一致,现改用 ::NAME 语义,代码块是 ::SRC-BLOCK-NAME, drawer 是 ::DRAWER-NAME.

org-id-remap. 变更文档说明,从 LINK:::DRAWERNAE:LINK::DRAWERNAME

#+name: 2025-07-25-20-48
#+header: :eval no
#+begin_src emacs-lisp :results silent :lexical t
;;; org-id-remap -*- lexical-binding: t; -*-
(!def 'org-id-remap
 (!let* (_
;;;; 内部变量
         <<@([[id:org-id-remap::private]])>>
;;;; 命令集(内部)
         <<@([[id:org-id-remap::cmd-set]])>>)

;;;; `org-id-remap' 命令入口
  (!def cmd-entry
   <<@([[id:org-id-remap::cmd-entry]])>>)

;;;; 重映射函数
  ;; 整个重映射逻辑的入口,实现基于
  ;; `org-link-abbrev-alist'.
  (!def remap
   <<@([[id:org-id-remap::remap]])>>)

;;;; 链接有效性检测
  ;; 判断 `link' 是否有效地指向目标。
  ;; 输入: `link', 待映射的链接。
  ;; 输出: `link', 映射后的链接;或 nil, 无效映射。
  (!def link-verify
   <<@([[id:org-id-remap::link-verify]])>>)

;;;; HTTP链接文本缓存
  ;; 缓存 HTTP 页面文本。
  ;; 输入: `link', HTTP 链接。
  ;; 输出: `link', 指向缓存文件的链接;或 nil;或异常。
  (!def cache-http-link
   <<@([[id:org-id-remap::cache-http-link]])>>)

;;;; 命令实现
  <<@([[id:org-id-remap::cmd-impl]])>>

;;;; Org扩展
  ;; Org Link Search Option 扩展。
  ;; 支持 LINK::DRAWERNAME 语义。
  (!def search
   <<@([[id:org-id-remap::drawer-search]])>>)

  ;; Org File Link 扩展。
  ;; 拦截无配置后缀文件链接访问。
  (!def open-file-link
   <<@([[id:org-id-remap::open-file-link]])>>)

;;;; 调试日志
  (!def log
   <<@([[id:org-id-remap::log]])>>)
  (!def log (log " log:org-id-remap"))

;;;; org-id-remap End
  (when (fboundp 'org-id-remap)
    (ignore-errors (org-id-remap nil)))

  cmd-entry))
#+end_src

link-verify. 移除原来对 link 加方括号的代码——对两种解析结果不一致的规避。

#+name: 2025-07-25-20-49
#+begin_src emacs-lisp :eval no
(lambda (link)
  ;; 对于 id-link, 我们直接尝试 open 它,
  ;; 如无错误,则该链接有效,`link' 被原样返回;
  ;;
  ;; 对于 http-link, 我们将 `link' 指向的资源缓存为
  ;; 纯文本,并尝试跳转到目标,如无错误,则该链接有
  ;; 效,返回缓存文件的链接。
  ;;
  ;; 依赖: log, cache-http-link
  (log "valid? %s..." link)
  (ignore-errors
    ;; 如果 link 非 id-link, 我们预期它将被 expand
    ;; 为 http-link.
    (unless (string-prefix-p "id:" link t)
      (setq link (org-link-expand-abbrev link))
      (unless (string-match-p "^http[s]:" link)
        (error
         "Expect HTTP/S link, but %s was given."
         link))
      (setq link (cache-http-link link))
      (log "cache: %s" link))
    (log "verify %s..." link)
    (save-window-excursion
      (org-link-open-from-string link))

    link))
#+end_src

drawer-search. 改 drawer search 实现。

#+name: 2025-07-25-20-50
#+begin_src emacs-lisp :eval no
(lambda (option)
  (setq option (format ":%s:" option))
  (when (string-match-p org-drawer-regexp
                        option)
    (goto-char (point-min))
    (catch 'found
      (while (re-search-forward
              option nil t)
        (when (org-element-type-p
               (org-element-context)
               'drawer)
          (throw 'found 'drawer))))))
#+end_src

2 open-file-link 缺陷修复

open-file-link. 修复因为判空串引发的异常;修复因可见性引发的查找失败。

#+name: 2025-07-25-20-51
#+begin_src emacs-lisp :eval no
(lambda (f l)
  ;; `org-persist' 的缓存文件后缀无法配置,导被缓存为
  ;; `*.nil' 的文件访问时被迫使用操作系统接口,为此,
  ;; 我们提供一个默认打开文件的函数,配合
  ;; `org-file-apps' 使用。
  (let* ((s (string-trim l f))
         (s (string-trim s "::"))
         p)
    (find-file-other-window f)
    (org-mode)
    (when (length> s 0)
      (org-with-wide-buffer
       (org-link-search s)
       (setq p (point)))
      (when p (widen) (goto-char p)))))
#+end_src

3 链接查找优先级重调

分裂 remap, 剥离链接优先级调整相关代码。

有重映射的 id-links 放前边,以便重映射能递归;其次是无映射的 id-link, 因为本地优先级高;最后是 http-link.

#+name: 2025-07-25-20-52
#+begin_src emacs-lisp :eval no
(lambda (&optional id)
  (cond
   ((and
     ;; Org Link 缩写特性可能会在非 打开链接 的上下
     ;; 文中使用,这里我们只处理 打开链接 时的情况。
     (catch 'break
       (mapbacktrace
        (lambda (&rest f)
          (when (memq (cadr f)
                      '(org-open-at-point
                        org-link-open-from-string
                        org-link-open))
            (log "try remap %s..." id)
            (throw 'break t)))))
     ;; 从 mapping 中寻找一个有效目标。
     (let ((links (gethash id mapping))
           id-links other-links new-id-links)
       ;; 分类处理 links, 递归处理 id-links.
       (while links
         (setq new-id-links nil)
         (mapcar
          (lambda (link)
            ;; 我们在这里移除 link 中可能存在的方括号,
            ;; 以免 link 再次被
            ;; `org-element-link-parser' 用
            ;; `org-link-expand-abbrev' 展开,造成
            ;; 递归。
            (setq link (string-trim
                        link "\\[\\[" "\\]\\]"))
            (cond
             ((and (string-prefix-p "id:" link t)
                   (not (member link id-links)))
              (push link new-id-links)
              (push link id-links))
             (t (unless (member link other-links)
                  (push link other-links)))))
          links)
         (setq links
               (flatten-list
                (mapcar
                 (lambda (link)
                   (gethash
                    (string-trim link "id:")
                    mapping))
                 new-id-links))))

       (setq
        links
        <<@([[id:org-id-remap::remap-sort-links]])>>)

       ;; 遍历 links, 直至我们找到一个可用的目标。
       (log "pick from %S links" (length links))
       (catch 'break
         (dolist (link links)
           (when (setq link (link-verify link))
             (log "remap id:%s to %s\n" id link)
             (throw 'break link)))
         nil))))

   ;; 找不到 id 的 mapping, 返回其自身。
   ((ignore (log "not found")))
   ((concat "id:" id))))
#+end_src
#+name: 2025-07-25-20-53
#+begin_src emacs-lisp :eval no
;; links 优先级调整。
;; 输入: id-links, other-links.
;; 输出: links.
;; 优化:启发式优先级。
(let (id-links-with-remap
      id-links-without-remap
      (ids-in-map-table
       (hash-table-keys mapping)))
  (mapcar
   (lambda (link)
     (if (member (string-trim link "id:")
                 ids-in-map-table)
         (push link id-links-with-remap)
       (push link id-links-without-remap)))
   id-links)
  ;; 有重映射的 id-links 放前边,以便重映射能递
  ;; 归;其次是无映射的 id-link, 因为本地优先级
  ;; 高;最后是 http-link.
  `(,@id-links-with-remap
    ,@id-links-without-remap
    ,@other-links))
#+end_src

构建配置

映射表:

#+name: 2025-07-25-20-54
#+begin_src emacs-lisp :eval no
"org-id-remap::org-id-remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-48"

"org-id-remap::link-verify"
"emacs-china/org-id-remap:29814::2025-07-25-20-49"

"org-id-remap::drawer-search"
"emacs-china/org-id-remap:29814::2025-07-25-20-50"

"org-id-remap::open-file-link"
"emacs-china/org-id-remap:29814::2025-07-25-20-51"

"org-id-remap::remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-52"
"org-id-remap::remap-sort-links"
"emacs-china/org-id-remap:29814::2025-07-25-20-53"

;; 未变更部分
"org-id-remap::cmd-entry"
"emacs-china/org-id-remap:29814::cmd-entry"

"org-id-remap::cache-http-link"
"emacs-china/org-id-remap:29814::cache-http-link"

"org-id-remap::cmd-set"
"emacs-china/org-id-remap:29814::cmd-set"

"org-id-remap::cmd-impl"
"emacs-china/org-id-remap:29814::cmd-impl"

"org-id-remap::log"
"emacs-china/org-id-remap:29814::log"

"org-id-remap::private"
"emacs-china/org-id-remap:29814::private"
#+end_src

Eval 及 Tangle 入口

#+name: 2025-07-25-20-56
#+begin_src emacs-lisp :eval no
(org-id-remap
 "org-id-remap::2025-07-25-20-54"
 "https://emacs-china.org/t/org-id-remap/29814::2025-07-25-20-54")
#+end_src
#+name: 2025-07-25-20-57
#+header: :depend (org-sbe "2025-07-25-20-56" ":eval yes")
#+header: :depend (org-id-remap t)
#+begin_src emacs-lisp :results silent :noweb yes
(org-id-remap 'reset)
(org-id-remap
 <<@([[id:org-id-remap::2025-07-25-20-54]])>>
 )
(org-with-wide-buffer (org-sbe "2025-07-25-20-39" ":eval yes"))
#+end_src

更新

表项覆盖;构建时间嵌入(附带更新)。

1 表项覆盖

在使用的过程中,常常会遇到这种场景[S]:个别文本块需要更新,进而联动到复制一份映射表,然后更新映射表中的 “个别” 表项。同样地,为了减少更新量,我们期望能够复用先前的映射表,或者说,在先前映射表的基础上,覆盖掉个别需要更新的表项。

原先 org-id-remap 的调用设计是为了实现一对多映射,即某个 id 存在多条映射链接(出于数据冗余考虑),具体来说:

映射表:

(org-id-remap
 "id1" "link1"
 "id1" "link2"
 "id1" "link3")

可以让我们具有多份 id1 的映射,以便某个 link 损毁的情况下能使用其他 links.

现在,场景[S]对映射表提出了另一种需求:同一 id 不同版本。

为此,最简单且直观的实现方式为:新增一个特性,使得 “每次” 调用 org-id-remap 建立表项时,覆盖已存在的表项。具体来说:

某次调用:

(org-id-remap
 "id1" "link1"
 "id1" "link2"
 "id2" "link3"
 "id3" "link4")

后跟有调用:

(org-id-remap
 "id2" "link6"
 "id2" "link5")

此时,旧的 id2 将被覆盖,即当前映射表为:

id1->{link1,link2}, id2->{link5,link6}, id3->{link4}.

实现

分裂 cmd-impl, 剥离 set-mapping.

#+name: 2025-07-25-21-30
#+begin_src emacs-lisp :eval no
(!def enable?
 (lambda ()
   (get remap 'org-link-abbrev-safe)))
(!def enable
 (lambda ()
   (put remap 'org-link-abbrev-safe t)
   (setf (alist-get
          "id" org-link-abbrev-alist
          nil nil #'equal)
         remap)
   (push `(t . ,open-file-link)
         org-file-apps)
   (add-hook
    'org-execute-file-search-functions
    search)
   (message "Org ID remap enable.")))
(!def disable
 (lambda ()
   (put remap 'org-link-abbrev-safe nil)
   (setq org-link-abbrev-alist
         (assoc-delete-all
          "id" org-link-abbrev-alist
          #'equal))
   (setq org-file-apps
         (seq-filter
          (lambda (it)
            (not
             (equal
              it `(t . ,open-file-link))))
          org-file-apps))
   (remove-hook
    'org-execute-file-search-functions
    search)
   (message "Org ID remap disable.")))
(!def reset
 (lambda ()
   (clrhash mapping)
   (message "Org ID mapping reset.")))
<<@([[id:org-id-remap::set-mapping]])>>
(!def get-mapping
 (lambda ()
   (!let (r)
    (maphash
     (lambda (k vs)
       (mapc
        (lambda (v) (push (list k v) r)) vs))
     mapping)
    (flatten-list r))))
#+end_src

更新 set-mapping. 实现旧表项覆盖。

#+name: 2025-07-25-21-31
#+begin_src emacs-lisp :eval no
(!def set-mapping
 (lambda (mappings)
   (let ((mappings
          (seq-partition mappings 2)))
     ;; 覆盖已有的映射表项,以用新表项变更文本版本。
     (mapcar
      (lambda (kv)
        (setf (gethash (car kv) mapping) nil))
      mappings)
     ;; 建立映射。
     (mapcar
      (lambda (kv)
        (let ((k (car kv)) (v (cadr kv)))
          (unless
              (member v (gethash k mapping))
            (push v (gethash k mapping)))))
      mappings))))
#+end_src

2 构建时间嵌入(附带更新)

新增 build-time 命令,以识别不同的 build.

#+name: 2025-07-25-21-32
#+begin_src emacs-lisp :results silent
(format-time-string "%FT%T%z")
#+end_src
:2025-07-25-21-33:
将 Org ID 链接重映射为其他链接。
重映射仅在“打开链接”的上下文中有效。

零参时, toggle org-id-remap.

单参时:
  'reset 重置重映射;
  'enable? 查询使能情况;
  nil 禁用重映射;其他使能。

其他情况时,建立 id 映射,调用形式如下:
  (org-id-remap
   \"id1\" \"id:real-target1\"
   \"id2\" \"id:real-target2\"
   ...)
:end:
#+name: 2025-07-25-21-34
#+begin_src emacs-lisp :eval no
(lambda (&rest args)
  "Org ID 重映射。

<<@([[id:org-id-remap::cmd-doc]],rm-ws-p=1)>>"
  (interactive)
  (declare (indent 0))
  (cond
   ((length= args 0)
    (if (enable?) (disable) (enable)))
   ((length= args 1)
    (setq args (car args))
    (cond
     ((eq 'enable? args) (enable?))
     ((eq 'reset args) (reset))
     ((eq 'get-mapping args) (get-mapping))
     ((eq 'build-time args)
      "<<2025-07-25-21-32()>>")
     ((null args) (disable))
     (t (enable))))
   (t (set-mapping args))))
#+end_src

构建配置

映射表:

#+name: 2025-07-25-21-38
#+begin_src emacs-lisp :eval no
"org-id-remap::cmd-impl"
"emacs-china/org-id-remap:29814::2025-07-25-21-30"
"org-id-remap::set-mapping"
"emacs-china/org-id-remap:29814::2025-07-25-21-31"

"org-id-remap::cmd-entry"
"emacs-china/org-id-remap:29814::2025-07-25-21-34"
"org-id-remap::cmd-doc"
"emacs-china/org-id-remap:29814::2025-07-25-21-33"

;; 未变更部分

"org-id-remap::org-id-remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-48"
"org-id-remap::link-verify"
"emacs-china/org-id-remap:29814::2025-07-25-20-49"
"org-id-remap::drawer-search"
"emacs-china/org-id-remap:29814::2025-07-25-20-50"
"org-id-remap::open-file-link"
"emacs-china/org-id-remap:29814::2025-07-25-20-51"
"org-id-remap::remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-52"
"org-id-remap::remap-sort-links"
"emacs-china/org-id-remap:29814::2025-07-25-20-53"
"org-id-remap::cache-http-link"
"emacs-china/org-id-remap:29814::cache-http-link"
"org-id-remap::cmd-set"
"emacs-china/org-id-remap:29814::cmd-set"
"org-id-remap::log"
"emacs-china/org-id-remap:29814::log"
"org-id-remap::private"
"emacs-china/org-id-remap:29814::private"
#+end_src

Eval 及 Tangle入口

#+name: 2025-07-25-21-36
#+begin_src emacs-lisp :eval no
(org-id-remap
 "org-id-remap::2025-07-25-21-38"
 "https://emacs-china.org/t/org-id-remap/29814::2025-07-25-21-38"
 )
#+end_src
#+name: 2025-07-25-21-37
#+header: :depend (org-sbe "2025-07-25-21-36" ":eval yes")
#+header: :depend (org-id-remap t)
#+begin_src emacs-lisp :results silent :noweb yes
(org-id-remap 'reset)
(org-id-remap
 <<@([[id:org-id-remap::2025-07-25-21-38]])>>
 )
(org-with-wide-buffer (org-sbe "2025-07-25-20-39" ":eval yes"))
#+end_src

更新

基于 org-exec 的构建。

构建脚本(可复用):

#+name: 2025-08-03-11-27
#+header: :var tangle=(ignore) load=(ignore)
#+header: :var target=(ignore) map-table=(ignore) ret="yes"
#+header: :var conf-only="no"
#+begin_src emacs-lisp :results silent :eval no :lexical t
(progn
  ;; 指向映射表的链接集
  ;; (unless map-table (error "No mapping."))
  (unless target (error "No target."))

  (when (string= conf-only "yes")
    (setq tangle "no")
    (setq load "no")
    (setq ret ""))

  (!let
   ((load load) (tangle tangle)
    (target target)
    (map-table map-table)
    (setup-mapping
     (lambda (map-table)
       (mapc
        (lambda (link)
          (let* ((mapping
                  (org-noweb-expand-link link))
                 (mapping
                  (read
                   (concat "(" mapping ")"))))
            (apply #'org-id-remap mapping)))
        map-table)))
    (do-load
     (lambda (f) (load f nil nil t))))

   ;; 建立映射表
   (setup-mapping map-table)

   ;; Tangle 及 Eval
   (when (and (stringp tangle)
              (length> tangle 0)
              (not (string= tangle "no")))
     (org-exec target #'org-babel-tangle '(4) tangle)
     (when-let*
         ((buf (find-buffer-visiting tangle)))
       (with-current-buffer buf
         (revert-buffer t t t)))
     (when (string= load "yes")
       (do-load tangle)
       (setq load "no")))

   (when (string= load "yes")
     (let ((f (make-temp-file "org-tangle")))
       (org-exec target #'org-babel-tangle '(4) f)
       (do-load f)))

   ret))
#+end_src

当前版本的构建入口:

#+name: 2025-08-03-11-22
#+header: :var tangle="no" load="no"
#+begin_src emacs-lisp :results silent :noweb yes :eval no
(org-id-remap 'reset)
(org-id-remap
 "build-script"
 "https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-27"
 "build-target"
 "https://emacs-china.org/t/org-id-remap/29814::2025-07-25-20-36"
 "map-table"
 "https://emacs-china.org/t/org-id-remap/29814::2025-07-25-21-38")
(org-id-remap t)
(org-exec "[[id:build-script]]" nil
  :eval "yes"
  'target "[[id:build-target]]"
  'map-table ''("[[id:map-table]]")
  'tangle (or tangle "~/org/org-id-remap.el")
  'load (or load "yes"))
#+end_src

构建:

#+begin_src emacs-lisp :eval no
  (org-exec
    "[[https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-22]]" nil
    :eval "yes" 'tangle "~/org/org-id-remap.el")
#+end_src

更新

表项覆盖 (2).

特性的上下文见表项覆盖

现在我们提供另一种表项覆盖的方法:

(org-id-remap  'reset)
(org-id-remap
 ;; old version
 "id1" "link1"
 "id1" "link2"
 "id2" "link3"
 "id3" "link4"
 nil nil
 ;; new version
 "id2" "link6"
 "id2" "link5")

当前映射表:id1->{link1,link2}, id2->{link5,link6}, id3->{link4}.

实现:

#+name: 2025-08-03-15-43
#+begin_src emacs-lisp :eval no
(!def set-mapping
 (lambda (mappings)
   (let ((mappings
          (seq-partition mappings 2))
         kv k v)
     ;; 覆盖已有的映射表项,以用新表项变更文本版本。
     (mapcar
      (lambda (kv)
        (setf (gethash (car kv) mapping) nil))
      mappings)
     ;; 建立映射。
     (catch 'break
       (while (length> mappings 0)
         (setq kv (car mappings))
         (setq mappings (cdr mappings))
         (setq k (car kv))
         (setq v (cadr kv))
         ;; nil nil 分隔不同版本。
         (when (and (null k) (null v))
           (throw 'break t))
         (unless
             (member v (gethash k mapping))
           (push v (gethash k mapping)))))
     (when mappings
       (apply #'org-id-remap
              (flatten-list mappings))))))
#+end_src

映射表:

尽管前一版本提供了一个复用映射表的特性,但这里我们没使用该特性,而是依旧复制了一份完整的映射表,因为在使用的过程,org-id-remap 的构建存在自我指涉问题,现简单表述如下:

本贴中有一个无任何依赖的初版 A, 但是相比于最新版本, A 少了某些特性 (后称特性 b), 而最新版本的 org-id-remap (后简称 remap) 的构建中有使用到特性 b, 这意味着——我们光有 A 还不足以一步到位地将 remap 升级到最新版本,我们必须先从 A 开始,升级到具有 b 特性的版本,然后才能升级到最新版本。如果后续版本的 remap 不断有新特性,而构建 remap 最新版本时又不断地使用这些新特性,那意味着我们只能一步一步地升级到最新版本的 remap.

由于 表项覆盖 (2) 这个特性与 org-id-remap 的构建相关,所以,我们这里完整拷贝一份映射表,以便从 A 直接更新到当前版本。

#+name: 2025-08-03-15-49
#+begin_src emacs-lisp :eval no
"org-id-remap::set-mapping"
"emacs-china/org-id-remap:29814::2025-08-03-15-43"

;; 未变更部分

"org-id-remap::cmd-impl"
"emacs-china/org-id-remap:29814::2025-07-25-21-30"
"org-id-remap::cmd-entry"
"emacs-china/org-id-remap:29814::2025-07-25-21-34"
"org-id-remap::cmd-doc"
"emacs-china/org-id-remap:29814::2025-07-25-21-33"
"org-id-remap::org-id-remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-48"
"org-id-remap::link-verify"
"emacs-china/org-id-remap:29814::2025-07-25-20-49"
"org-id-remap::drawer-search"
"emacs-china/org-id-remap:29814::2025-07-25-20-50"
"org-id-remap::open-file-link"
"emacs-china/org-id-remap:29814::2025-07-25-20-51"
"org-id-remap::remap"
"emacs-china/org-id-remap:29814::2025-07-25-20-52"
"org-id-remap::remap-sort-links"
"emacs-china/org-id-remap:29814::2025-07-25-20-53"
"org-id-remap::cache-http-link"
"emacs-china/org-id-remap:29814::cache-http-link"
"org-id-remap::cmd-set"
"emacs-china/org-id-remap:29814::cmd-set"
"org-id-remap::log"
"emacs-china/org-id-remap:29814::log"
"org-id-remap::private"
"emacs-china/org-id-remap:29814::private"
#+end_src

构建入口:

#+name: 2025-08-03-15-53
#+header: :var tangle="no" load="no"
#+begin_src emacs-lisp :results silent :noweb yes :eval no
(org-id-remap 'reset)
(org-id-remap
 "build-script"
 "https://emacs-china.org/t/org-id-remap/29814::2025-08-03-11-27"
 "build-target"
 "https://emacs-china.org/t/org-id-remap/29814::2025-07-25-20-36"
 "map-table"
 "https://emacs-china.org/t/org-id-remap/29814::2025-08-03-15-49")
(org-id-remap t)
(org-exec "[[id:build-script]]" nil
  :eval "yes"
  'target "[[id:build-target]]"
  'map-table ''("[[id:map-table]]")
  'tangle (or tangle "~/org/org-id-remap.el")
  'load (or load "yes"))
#+end_src
1 个赞