【存档】Org Tangle 扩展

Org Tangle 扩展。

续:org #+include URL

#+begin_src emacs-lisp :eval no
<​<@(org-link)>>
#+end_src

特性:基于 Org ID 链接;条件 Tangle; Outline 调节;文本 Tangle; HTTP Tangle.


1 基于 Org ID 链接

配合 org-open-at-point-global 使用,方便打开代码块中的 <​<@(link)>>.


2 条件 Tangle

考虑不同平台的键绑定配置:

#+name: android-2025-06-16-17-00
#+begin_src emacs-lisp :expand (eq system-type 'android) :eval no
(defkey "c i" #'open-init.el)
#+end_src

#+name: window-2025-06-16-17-00
#+begin_src emacs-lisp :expand (eq system-type 'windows-nt) :eval no
(defkey "C-c i" #'open-init.el)
#+end_src

#+begin_src emacs-lisp :tangle ~/org/a.txt :eval no
  (keymap-setting platform
    <<@([[id:977ba407-14a4-48c9-a0d4-947ea0e8686f::window-2025-06-16-17-00]])>>
    <<@([[id:977ba407-14a4-48c9-a0d4-947ea0e8686f::android-2025-06-16-17-00]])>>)
#+end_src

Window 上的 tangle 结果:

(keymap-setting platform
  (defkey "C-c i" #'open-init.el)
  )

Android 上的 tangle 结果(自动删除展开为空的行):

(keymap-setting platform
  (defkey "c i" #'open-init.el))

3 Outline 调节

通过 <​<@(link, level)>>,将展开后的代码片段的 Outline 等级 demote 至 level.

给定如下两个片段:

#+name: section-2025-06-16-17-01
#+begin_src emacs-lisp :tangle ~/org/a.txt :eval no
  ;;; Level 1
  (ignore "section")
  <<@([[id:977ba407-14a4-48c9-a0d4-947ea0e8686f::subsection-2025-06-16-17-01]],2)>>
#+end_src

#+name: subsection-2025-06-16-17-01
#+begin_src emacs-lisp :eval no
  ;;; Level 2
  (ignore "subsection")
  ;;;; Level 3
  (ignore "subsubsection")
#+end_src

展开为:

;;; Level 1
(ignore "section")
;;;; Level 2
(ignore "subsection")
;;;;; Level 3
(ignore "subsubsection")

4 文本 Tangle

基于 Org Link Search Option, tangle drawer block, paragraph 等文本。

给定如下 drawer:

:doc-2025-06-16-17-15:
browse-url

借助 eww 将 url 指定的资源渲染为纯文本,当前主要针对 http/https.
由于 Org link 的语法不支持定位 http/https 中的文本块,这里,我们
仿照 Org file/id link, 借助 `org-link-search', 为 http/https
链接提供一个定位文本块用的 search-option, 以如下形式:

`url::search-option'

如此,我们便可用 [​[http://host/path-to-file...::search-option]]
获取网络中的 Org 文本块。
:end:

片段

#+begin_src emacs-lisp :tangle ~/org/a.txt :eval no
  ;; <<@([[id:977ba407-14a4-48c9-a0d4-947ea0e8686f:::doc-2025-06-16-17-15:]])>>
  (lambda (url &rest args)
    ...)
#+end_src

展开为:(见下小节)


5 HTTP Tangle

从 HTTP/HTTPS 指向的资源中 tangle 文本块。

;; browse-url
;;
;; 借助 eww 将 url 指定的资源渲染为纯文本,当前主要针对 http/https.
;; 由于 Org link 的语法不支持定位 http/https 中的文本块,这里,我们
;; 仿照 Org file/id link, 借助 `org-link-search', 为 http/https
;; 链接提供一个定位文本块用的 search-option, 以如下形式:
;;
;; `url::search-option'
;;
;; 如此,我们便可用 [[http://host/path-to-file...::search-option]]
;; 获取网络中的 Org 文本块。
(lambda (url &rest args)
  ...)

配合 url-cache 使用。

一个例子:

#+begin_src emacs-lisp :tangle ~/org/a.txt :eval no
<<@([[https://emacs-china.org/t/org-include-url/29242/24::elisp-2025-03-26-17-50]])>>
#+end_src

展开为:

(unless (assoc 'expand org-babel-library-of-babel)
  (with-temp-buffer
    (org-mode)
    (insert
     "#+name: expand\n"
     "#+begin_src emacs-lisp\n"
     "(org-babel--src-block\n"
     "  (if (boundp 'there) there) (if (boundp 'name) name) nil t)\n"
     "#+end_src")
    (org-babel-lob-ingest)))

6 实现

#+name: elisp-2025-06-16-11-01
#+begin_src emacs-lisp :lexical t :results silent
;;; Org Noweb Expand Link  -*- lexical-binding: t; -*-
;;;; org-noweb-expand-link
(defalias 'org-noweb-expand-link
  ((lambda (locate expand _log _outline-adjust browse-url)
     ;; 注册 org-babel 函数 @
     (with-temp-buffer
       (org-mode)
       (message "org-noweb register org babel function: @")
       (let* ((vars `((level (ignore)) (par (ignore))
                      (src (ignore)) (re (ignore))
                      (drw (ignore))))
              (ha (mapconcat
                   (lambda (v) (format ":var %s=%s" (car v) (cadr v))) vars " "))
              (a (mapconcat
                  (lambda (v) (format ":%s %s" (car v) (car v))) vars " ")))
         (insert
          "#+name: @\n"
          "#+begin_src emacs-lisp :var link=\"\" " ha "\n"
          "(org-noweb-expand-link (format \"%s\" link) " a ")\n"
          "#+end_src")
         (org-babel-lob-ingest)))

     ;; `org-noweb-expand-link' 函数入口
     (lambda (link &rest conf)
       ;; 调试用
       (let ((tsfmt "[%Y-%m-%d %H:%M:%S.%3N]"))
         (princ (format "%s %s\n"
                        (format-time-string tsfmt (current-time)) link)
                (get-buffer-create "*Org Noweb*")))
       (defvar log)
       (defvar outline-adjust)
       (let ((log _log)
             (outline-adjust _outline-adjust)
             (marker (funcall locate link browse-url)))
         (if (null marker)
             (funcall log "failed to expand %s." link)
           (org-with-point-at marker
             (funcall expand conf))))))

;;;; 内部接口

;;;;; locate
   ;; 尽可能地借 `org-link-open' 定位 link 所指定的位置。
   (lambda (link browse-url)
     "返回 link 所指向的位置,以 marker 形式。"
     (save-window-excursion
       (ignore-errors
         (let ((org-link-frame-setup `((file . find-file)
                                       ,@org-link-frame-setup))
               (inhibit-message t)
               (message-log-max nil)
               ;; 使能 text search.
               (org-link-search-must-match-exact-headline nil)
               ;; 这里,我们提供一个配置变量 `org-noweb-browse-url',以便
               ;; 有用户出于某种目的想定制自己的browse-url 函数。比如,为了
               ;; 缓存 url 指定的数据。
               (browse-url-browser-function
                (or (and (boundp 'org-noweb-browse-url) org-noweb-browse-url)
                    browse-url)))
           (if (stringp link)
               (org-link-open-from-string link)
             (org-link-open link)))
         (setq marker (make-marker))
         (set-marker marker (point)))))

;;;;; expand
   (lambda (conf)
     "展开位于当前 buffer 当前 point 位置的 Org element."
     (let* ((ele (org-element-at-point))
            (type (org-element-type ele))
            (par (plist-get conf :par))
            (drw (plist-get conf :drw)))
       (cond
        ;; 展开代码块
        ((eq type 'src-block)
         (let* ((info (org-babel-get-src-block-info))
                (body ""))
           ;; 如果代码块参数 `:expand' 的 sexp 求值
           ;; 为 nil,
           (if (and (assq :expand (nth 2 info))
                    (not (alist-get :expand (nth 2 info))))
               ;; 表明该代码块在当前环境下拒绝展开。
               (funcall log "ignore %s." link)
             ;; 展开代码块
             (setq body (org-babel-expand-noweb-references info))
             ;; 调整 outline
             (setq body (funcall outline-adjust
                                 body (nth 0 info) (plist-get conf :level)))
             body)))

        ;; 展开 drawer
        ((eq type 'drawer)
         (string-trim
          (buffer-substring
           (org-element-contents-begin ele)
           (org-element-contents-end ele))))

        ;; 展开当前段落
        ((eq type 'paragraph)
         (org-with-wide-buffer
          (org-narrow-to-element)
          (string-trim (buffer-string))))

        ;; 展开注释块
        ((eq type 'comment-block)
         (string-trim (org-element-property :value ele)))

        ;; 除 link 提供的接口外,在下面的 case 中,我们通过 conf 参数提供一些
        ;; 额外定位 element 的手段,但不建议使用。

        ;; 展开指定段落文本
        ((and (eq type 'headline) par (> par 0))
         (org-with-wide-buffer
          (org-narrow-to-subtree)
          (catch 'break
            (org-element-map (org-element-parse-buffer nil t t)
                '(paragraph)
              (lambda (p)
                (setq par (1- par))
                (when (= par 0)
                  (setq par (org-element-interpret-data p))
                  (throw 'break (string-trim par))))))))

        ;; 展开 drawer 中的文本
        ((and (eq type 'headline) drw)
         (org-with-wide-buffer
          (org-narrow-to-subtree)
          (catch 'break
            (org-element-map (org-element-parse-buffer nil t t)
                '(drawer)
              (lambda (d)
                (when (string= (org-element-property :drawer-name d) drw)
                  (setq drw (string-trim
                             (buffer-substring
                              (org-element-contents-begin d)
                              (org-element-contents-end d))))
                  (throw 'break drw)))))))

        ;; 未定义
        (t (funcall log "%s point to an unsupported position." link)))))

;;;;; log
   ;; 这个函数有两个作用:
   ;; 一、将 msg 输出为 warning, 以便用户知道哪些 link 展开失败;
   ;; 二、将 msg 作为展开失败时的展开结果,即,如果给定 link 展开失败,
   ;; 将 msg 作为该 link 的展开结果,此时, msg 作为非法字符串,最终
   ;; 由 remove-log 从 tangle 出的文件中移除。
   (letrec ((tangling nil)
            (remove-log
             (lambda ()
               (save-excursion
                 (let* ((zw-spc (char-to-string ?\u200B))
                        (re (concat zw-spc "org-noweb-expand-link .*" zw-spc))
                        (re (format "^\\(.*\\)\\(%s\\)\\(\n?\\)" re)))
                   (save-match-data
                     (goto-char (point-min))
                     (while (re-search-forward re nil t)
                       (if (and (length= (string-trim (match-string 1)) 0)
                                (length= (match-string 3) 1))
                           (replace-match "")
                         (replace-match "" nil nil nil 2))))))
               (remove-hook 'org-babel-tangle-body-hook remove-log))))
     ;; 虽然但是, it works.
     (remove-hook 'org-babel-pre-tangle-hook (lambda () (setq tangling t)))
     (remove-hook 'org-babel-post-tangle-hook (lambda () (setq tangling nil)))
     (add-hook 'org-babel-pre-tangle-hook (lambda () (setq tangling t)))
     (add-hook 'org-babel-post-tangle-hook (lambda () (setq tangling nil)))
     (lambda (fmt &rest args)
       (add-hook 'org-babel-tangle-body-hook remove-log)
       (let* ((msg (apply #'format fmt args))
              (zw-spc (char-to-string ?\u200B))
              ;; 用零宽空格及 org-noweb-expand-link 界定非法字符串。
              (msg (concat zw-spc "org-noweb-expand-link " msg zw-spc)))
         (display-warning 'org-noweb msg :warning)
         (if tangling msg ""))))

;;;;; outline-adjust
   ;; 根据 lang 调节 body 中的 outline。具体实现交由
   ;; `org-noweb-demote:LANG' 处理。这里只提供一个针对 emacs-lisp 的默
   ;; 认调整函数。
   ((lambda (org-noweb-demote-emacs-lisp)
      (lambda (body lang level)
        "将 body 中所有 level 1 的 outline 及其子树 demote 至 level `LEVEL'."
        (or
         (when-let*
             ((_ (and level (> level 1)))
              (major-mode (org-src-get-lang-mode lang))
              (demote (string-trim-right
                       (format "org-noweb-demote:%s" major-mode) "-mode"))
              (demote (intern demote))
              (demote (or (and (functionp demote) demote)
                          (and (eq demote 'org-noweb-demote:emacs-lisp)
                               org-noweb-demote-emacs-lisp))))
           (ignore-errors
             (with-temp-buffer
               (outline-mode)
               (save-excursion (insert body))
               (funcall demote level)
               (buffer-string))))
         body)))
    ;; 提供一个调节 emacs-lisp outline 的默认函数。
    (lambda (level)
      (let ((outline-regexp ";;\\(;\\)\\{1,20\\} ")
            (outline-heading-alist
             (mapcar
              (lambda (n) `(,(concat (make-string (+ 2 n) ?\;) " ") . ,n))
              (number-sequence 1 20))))
        (save-excursion
          (outline-map-region
           (lambda () (dotimes (_ (1- level)) (outline-demote nil)))
           (point-min) (point-max))))))

;;;;; browse-url
   ;; 借助 eww 将 url 指定的资源渲染为纯文本,当前主要针对 http/https.
   ;; 由于 Org link 的语法不支持定位 http/https 中的文本块,这里,我们
   ;; 仿照 Org file/id link, 借助 `org-link-search', 为 http/https
   ;; 链接提供一个定位文本块用的 search-option, 以如下形式:
   ;;
   ;; `url::search-option'
   ;;
   ;; 如此,我们便可用 [[http://host/path-to-file...::search-option]]
   ;; 获取网络中的 Org 文本块。
   (lambda (url &rest args)
     (let* ((url (string-split url "::"))
            (search-option (if (length> url 1) (car (last url))))
            (url (if (null search-option)
                     (car url)
                   ;; 移除 search-option.
                   (setf (nthcdr (1- (length url)) url) nil)
                   (string-join url "::")))
            ;; (eww-retrieve-command 'sync)
            (timeout 10)
            (done nil)
            (hook (lambda () (setq done t))))
       (add-hook 'eww-after-render-hook hook)
       (eww-browse-url url)
       (while (and (> timeout 0) (not done))
         (sit-for 1)
         (setf timeout (1- timeout)))
       (remove-hook 'eww-after-render-hook hook)
       (if (= timeout 0)
           (error "eww browse url %S timeout" url)
         (when search-option
           (org-mode)
           (org-link-search search-option)))))))
#+end_src
3 个赞

附:

#+begin_src emacs-lisp :lexical t :results silent
  (defalias 'org-exec
    (lambda (link)
      (letrec ((url (string-trim link "\\[\\[" "\\]\\]"))
               (hook (lambda ()
                       (remove-hook 'eww-after-render-hook hook)
                       (org-mode)
                       (org-babel-execute-buffer))))
        (add-hook 'eww-after-render-hook hook)
        (eww-browse-url url))))
#+end_src
(org-exec "[[https://emacs-china.org/t/org-tangle/29663]]")
2 个赞

特性:前缀空白字符移除

文档

:2025-07-26-11-07:

移除前缀空白字符。

使用: <​<@(LINK,rm-ws-p=1)>>

如片段展开的最终结果嵌套在另外的片段中,且含缩进,移
除其缩进。

使用场景:

考虑如下文本块:

a:
(progn
    <​<b>>)

b:
(def rm-ws-p
  \"Remove whitespace prefix
<​<c>>\")

c:
移除前缀空白字符。

当展开 a 时,我们会得到如下片段:

(progn
    (def rm-ws-p
      \"Remove whitespace prefix
    移除前缀空白字符。\"))

受 a 缩进影响,作为 b 的 docstring, c 每行都被填
充了缩进用的空白字符。为此,我们提供此特性移除前
缀空白字符。经此特性处理后, a 展开为:

(progn
    (def rm-ws-p
      \"Remove whitespace prefix
移除前缀空白字符。\"))

:end:

实现

#+name: 2025-07-26-11-08
#+begin_src emacs-lisp :eval no
(lambda (&optional body conf)
  "Remove whitespace prefix.

<<@([[id:org-noweb-expand-link::feat-doc:remove-whitespace-prefix]],rm-ws-p=1)>>"
  ;; 有 body 时往 body 每行行首加入 mark;
  ;; 无 body 时当 hook 用。
  ;;
  ;; 依赖: rm-ws-p.
  (let* ((mark (concat
                "org-noweb-expand-link"
                ":rm-ws-p"))
         (re (concat "^[ \t]*" mark)))
    (cond
     (body
      (cond
       ((and (plist-get conf :rm-ws-p)
             (not (plist-get conf :failed)))
        (add-hook 'org-babel-tangle-body-hook
                  rm-ws-p)
        (string-join
         (mapcar
          (lambda (line) (concat mark line))
          (string-split body "\n"))
         "\n"))
       (body)))
     (t
      (save-match-data
        (goto-char (point-min))
        (while (re-search-forward re nil t)
          (replace-match "")))
      (remove-hook 'org-babel-tangle-body-hook
                   rm-ws-p)))))
#+end_src

注册到整体结构中的文本片段:

#+name: 2025-07-26-11-09
#+begin_src emacs-lisp :eval no

;;; Remove Whitespace Prefix
  (!let ((rm-ws-p
          (make-symbol
           (concat
            "org-noweb-expand-link" ":"
            "remove-whitespace-prefix"))))
   (!def rm-ws-p
    <<@([[id:org-noweb-expand-link::remove-whitespace-prefix]])>>)
   (entry 'add-post-process
          rm-ws-p 'remove-whitespace-prefix)
   (entry 'add-babel-var 'rm-ws-p))
#+end_src
1 个赞

特性:大纲等级调节

文档

:2025-07-26-11-04:

大纲调节。

使用: <​<@(LINK,LEVEL)>><​<@(LINK,level=LEVEL)>>

将展开后的代码片段的 Outline 等级 demote 至 LEVEL.

考虑如下两个片段:

#+name: section
#+begin_src emacs-lisp :eval no
;;; Level 1
(ignore 'section)
<<@([[id:ID::subsection]],2)>>
#+end_src
#+name: subsection
#+begin_src emacs-lisp :eval no
;;; Level 2
(ignore 'subsection)
;;;; Level 3
(ignore 'subsubsection)
#+end_src

<​<@([​[id​:ID::section]])>> 展开为:

#+begin_src emacs-lisp :eval no
;;; Level 1
(ignore 'section)
;;;; Level 2
(ignore 'subsection)
;;;;; Level 3
(ignore 'subsubsection)
#+end_src

:end:

实现

#+name: 2025-07-26-11-05
#+begin_src emacs-lisp :eval no
;; 根据 lang 调节 body 中的 outline。具体实现交由
;; `org-noweb-demote:LANG' 处理。这里只提供一个针对
;; emacs-lisp 的默认调整函数。
(!let (entry demote:emacs-lisp)
 (!def entry
  ;; 将 body 中所有 level 1 的 outline 及其子树
  ;; demote 至 level `LEVEL'.
  (lambda (body conf)
    "Outline Adjust.

<<@([[id:org-noweb-expand-link::feat-doc:outline-adjust]],rm-ws-p=1)>>"
    (or
     (when-let*
         ((_ (not (plist-get conf :failed)))
          (level (plist-get conf :level))
          (_ (> level 1))
          (lang (plist-get conf :lang))
          (major-mode
           (org-src-get-lang-mode lang))
          (demote
           (string-trim-right
            (format "org-noweb-demote:%s"
                    major-mode)
            "-mode"))
          (demote (intern demote))
          (demote
           (or (and (functionp demote) demote)
               (and
                (eq demote
                    'org-noweb-demote:emacs-lisp)
                demote:emacs-lisp))))
       (ignore-errors
         (with-temp-buffer
           (outline-mode)
           (save-excursion (insert body))
           (funcall demote level)
           (buffer-string))))
     body)))

 (!def demote:emacs-lisp
  ;; 提供一个调节 emacs-lisp outline 的默认函数。
  (lambda (level)
    (let ((outline-regexp
           ";;\\(;\\)\\{1,20\\} ")
          (outline-heading-alist
           (mapcar
            (lambda (n)
              `(,(concat
                  (make-string (+ 2 n) ?\;)
                  " ")
                . ,n))
            (number-sequence 1 20))))
      (save-excursion
        (outline-map-region
         (lambda ()
           (dotimes (_ (1- level))
             (outline-demote nil)))
         (point-min) (point-max))))))

 entry)
#+end_src

注册到整体结构中的文本片段:

#+name: 2025-07-26-11-06
#+begin_src emacs-lisp :eval no

;;; Outline Adjust
  (!let ((outline-adjust
          (make-symbol
           (concat
            "org-noweb-expand-link" ":"
            "outline-adjust"))))
   (!def outline-adjust
    <<@([[id:org-noweb-expand-link::outline-adjust]])>>)
   (entry 'add-post-process
          outline-adjust 'outline-adjust))
#+end_src

特性:条件 Tangle

文档

:2025-07-26-11-00:

条件展开。

通过 Header Argument :expand 指定代码块是否在其
被引用处展开. :expandnil\"no\" 时不展开
该代码块.

使用场景:

考虑不同平台的键绑定配置:

#+name: android
#+header: :expand (eq system-type 'android)
#+begin_src emacs-lisp :eval no
(defkey [c i] #'open-init.el)
#+end_src
#+name: window
#+header: :expand (eq system-type 'windows-nt)
#+begin_src emacs-lisp :eval no
(defkey [C-c i] #'open-init.el)
#+end_src
#+begin_src emacs-lisp :eval no
  (keymap-setting platform
    <<@([[id:ID::window]])>>
    <<@([[id:ID::android]])>>)
#+end_src

Window 上的展开为:

#+begin_src emacs-lisp :eval no
(keymap-setting platform
  (defkey [C-c i] #'open-init.el)
  )
#+end_src

Android 上的展开为(自动删除展开为空的行):

#+begin_src emacs-lisp :eval no
(keymap-setting platform
  (defkey [c i] #'open-init.el))
#+end_src

:end:

实现

#+name: 2025-07-26-11-01
#+begin_src emacs-lisp :eval no
(lambda (&optional body conf)
  "Conditional Tangle.

<<@([[id:org-noweb-expand-link::feat-doc:conditional-tangle]],rm-ws-p=1)>>"
  (cond
   ;; process body
   (body
    (cond
     ((and
       (plist-get conf :failed)
       (backtrace-frame 0 'org-babel-tangle))
      (add-hook 'org-babel-tangle-body-hook
                cond-tangle)
      (let* ((msg (plist-get conf :err-msg))
             (msg (or msg "_"))
             (zw-spc (char-to-string ?\u200B)))
        ;; 用 org-noweb-expand-link 及零宽空格界
        ;; 定非法字符串。
        (concat zw-spc
                "org-noweb-expand-link " msg
                zw-spc)))
     (body)))
   ;; org-babel-tangle-body-hook
   (t
    (save-excursion
      (let* ((zw-spc (char-to-string ?\u200B))
             (re (concat
                  zw-spc
                  "org-noweb-expand-link .*"
                  zw-spc))
             ;; if remove trailing space
             (re (concat re "[ \t]*"))
             (re (format
                  "^\\(.*?\\)\\(%s\\)\\(\n?\\)"
                  re)))
        (save-match-data
          (goto-char (point-min))
          (while (re-search-forward re nil t)
            (if (and (length=
                      (save-match-data
                        (string-trim
                         (match-string 1)))
                      0)
                     (length= (match-string 3)
                              1))
                (replace-match "")
              (replace-match "" nil nil nil 2)))))
      (remove-hook 'org-babel-tangle-body-hook
                   cond-tangle)))))
#+end_src

注册到整体结构中的文本片段:

#+name: 2025-07-26-11-02
#+begin_src emacs-lisp :eval no

;;; Conditional Tangle
  (!let ((cond-tangle
          (make-symbol
           (concat
            "org-noweb-expand-link" ":"
            "conditional-tangle"))))
   (!def cond-tangle
    <<@([[id:org-noweb-expand-link::conditional-tangle]])>>)
   (entry 'add-post-process
          cond-tangle 'conditional-tangle))
#+end_src

特性:代码块展开

实现

#+name: 2025-07-26-10-50
#+begin_src emacs-lisp :eval no
(lambda (ele conf)
  (setq info (org-babel-get-src-block-info))
  (cond
   ;; 如果代码块参数 `:expand' 的 sexp 求值
   ;; 为 nil 或 "no",
   ((and
     (assq :expand (nth 2 info))
     (member
      (alist-get :expand (nth 2 info))
      '(nil "no")))
    ;; 表明该代码块在当前环境下拒绝展开。
    (plist-put conf :failed t)
    (plist-put
     conf :err-msg
     (format-message "ignore %s." link))
    "")
   (t
    (plist-put conf :lang (nth 0 info))
    (org-babel-expand-noweb-references info))))
#+end_src

注册到整体结构中的文本片段:

#+name: 2025-07-26-10-48
#+begin_src emacs-lisp :eval no

;;; src-block Expander
  (!let ((expand (make-symbol "src-block-expand")))
   (!def expand
    <<@([[id:org-noweb-expand-link::expand-src-block]])>>)
   (entry 'set-expander
          'src-block expand 'src-block-expand))
#+end_src

特性: drawer 展开

实现

#+name: 2025-07-26-10-53
#+begin_src emacs-lisp :eval no
(lambda (ele conf)
  (string-trim
   (buffer-substring
    (org-element-contents-begin ele)
    (org-element-contents-end ele))))
#+end_src

注册到整体结构中的文本片段:

#+name: 2025-07-26-10-54
#+begin_src emacs-lisp :eval no

;;; drawer Expander
  (!let ((expand (make-symbol "expand-drawer")))
   (!def expand
    <<@([[id:org-noweb-expand-link::expand-drawer]])>>)
   (entry 'set-expander
          'drawer expand 'drawer-expand))
#+end_src

Org Tangle 扩展

鉴于我们已经有了Org ID Remap, 于此重构 Org Tangle 扩展.

HTTP Tangle 特性已被移除,Org Tangle 只专注于文本编织。

1 整体结构

#+name: 2025-06-20-21-35
#+begin_src emacs-lisp :eval no
;;; Org Noweb Expand Link  -*- lexical-binding: t; -*-
(!def 'org-noweb-expand-link
 (!let* (entry
;;;; Private
         <<@([[id:org-noweb-expand-link::private]])>>)

;;;; Entry
  ;; `org-noweb-expand-link' 函数入口
  (!def entry
   <<@([[id:org-noweb-expand-link::entry]])>>)

;;;; Locate
  ;; 定位 `link' 所指。
  ;; 输入: `link', Org 链接。
  ;; 输出: marker, `link' 所指,或 nil.
  (!def locate
   <<@([[id::org-noweb-expand-link::locate]])>>)

;;;; Expand
  ;; 展开当前位置的 Org 元素
  ;; 输入: `conf', 同 entry 入参。
  ;; 输出: string, Org 元素展开后的文本。
  (!def expand
   <<@([[id:org-noweb-expand-link::expand]])>>)
<<@([[id:org-noweb-expand-link::features]],2)>>

;;;; Logging
  (!def log
   <<@([[id:org-noweb-expand-link::log]])>>)
  (!def log (log " log:org-noweb"))

;;;; Prelude
  ;; 注册 org-babel 函数 @
  <<@([[id:org-noweb-expand-link::org-babel-register]])>>

;;;; End
  entry))
#+end_src

2 入口 (entry)

:2025-07-26-10-14:

Org Noweb 扩展。

结合 Org-Babel 提供额外的引用文本块的语义:

#+begin_src emacs-lisp :eval no
<​<@(org-link)>>
#+end_src

配合 `org-open-at-point-global’ 使用,方便打开代
码块中的 Org 链接。

特性查询: M-x org-noweb-expand-link

:end:

实现:

#+name: 2025-07-26-10-15
#+begin_src emacs-lisp :eval no
(lambda (link &rest conf)
  "Org Noweb Extension.

<<@([[id:org-noweb-expand-link::entry-docstring]],rm-ws-p=1)>>"
  (interactive (list nil))
  (cond
   ((stringp link)
    (log "%s" link)
    (let ((marker (locate link)) body)
      (cond
       (marker
        (org-with-point-at marker
          ;; 如果展开失败,设置 :failed 及
          ;; :err-msg, 并将 body 置为空串。
          (setq body (expand conf))))
       (t
        (plist-put conf :failed t)
        (plist-put
         conf :err-msg
         (format-message
          "failed to expand %s" link))
        (setq body "")))

      (dolist (p post-process)
        (setq body (funcall p body conf)))

      ;; display warning
      (when (plist-get conf :failed)
        (display-warning
         'org-noweb
         (plist-get conf :err-msg)
         :warning))
      body))
   <<@([[id:org-noweb-expand-link::cmd-set]])>>))
#+end_src

3 文本定位 (locate)

根据 Org链接 定位文本位置。

#+name: 2025-07-26-10-27
#+begin_src emacs-lisp :eval no
;; 借 `org-link-open' 定位 `link' 所指。
;;
;; 很遗憾, Org 本身并没有提供编程级别的 API 实现类似的
;; 接口。为了尽可能复用已有代码,我们只能借
;; `org-link-open' 之类的带副作用 (改变 window 或
;; buffer 或 point) 的接口实现。
(lambda (link)
  (let ((inhibit-message t)
        (message-log-max nil)
        (org-link-search-must-match-exact-headline t)
        (org-link-frame-setup
         `((file . find-file-other-window)
    	   ,@org-link-frame-setup))
        (marker (make-marker)))
    (ignore-errors
      (save-window-excursion
        ;; 这个 guard 实际只对当前 buffer 有效,无法处
        ;; 理 open-link 跑到别的 buffer 的情况。但考虑
        ;; 到有时我们会 open 当前 buffer 中的 link, 为
        ;; 了防止因为可见性引发的链接查找失败,我们还
        ;; 是在这里加上这个 gaurd, 当然,也许还有更好
        ;; 的实现方法,但到时再说。
        (org-with-wide-buffer
         (if (not (stringp link))
             (org-link-open link)
           (org-link-open-from-string link))
         (set-marker marker (point)))))))
#+end_src

4 文本展开 (expand)

根据当前位置的 Org元素 的类型将该元素展开为文本。转交给 org-element-expanders 处理。

#+name: 2025-07-26-10-30
#+begin_src emacs-lisp :eval no
(lambda (conf)
  "展开位于 current point 的 Org element."
  (!let* ((ele (org-element-at-point))
          (type (org-element-type ele))
          (expand
           (alist-get
            type org-element-expanders)))
   (cond
    ((and expand (expand ele conf)))

    ;; 未定义
    (t
     (plist-put conf :failed t)
     (plist-put
      conf :err-msg
      (format-message
       "%s point to an unsupported position."
       link))
     ""))))
#+end_src

5 Org Babel 接口

提供一个 Org Babel 函数: @,

以便将 babel call 转换为对 org-noweb-expand-link 的调用。

babel-variables 中的符号将作为 babel call 变量,并传递给 org-noweb-expand-link.

#+name: 2025-07-26-10-32
#+begin_src emacs-lisp :eval no
(with-temp-buffer
  (org-mode)
  (log "register org babel function: @")
  (let* ((vars
          `((level (ignore))
            ,@(mapcar
               (lambda (v) `(,v (ignore)))
               babel-variables)))
         (ha (mapconcat
              (lambda (v)
                (format
                 ":var %s=%s" (car v) (cadr v)))
              vars " "))
         (a (mapconcat
             (lambda (v)
               (format
                ":%s %s" (car v) (car v)))
             vars " ")))
    (insert
     "#+name: @\n"
     "#+begin_src emacs-lisp "
     ":var link=\"\" " ha "\n"
     "(org-noweb-expand-link "
     "(format \"%s\" link) " a ")\n"
     "#+end_src")
    (org-babel-lob-ingest)))
#+end_src

6 内部用变量

#+name: 2025-07-26-09-47
#+begin_src emacs-lisp :eval no
locate expand features log
babel-variables
org-element-expanders post-process
#+end_src

7 命令集与特性集

实际上, org-noweb-expand-link 只是一个简单的程序框架,并未提供任何特性支撑。实际的特性需通过 org-noweb-expand-link 提供的命令/接口进行注册。具体细节见代码,使用例子可参考单独的特性。

文本展开相关

org-element-expanders 存放不同类型的 Org元素 所需的文本展开函数,调用约定:

(lambda (org-element, plist-conf) ...)

展开后处理相关

post-process 存放需对 展开后的文本 进行处理的函数,调用约定:

(lambda (expanded-text, plist-conf) ...)

配置变量相关

babel-variables 存放将被作为 babel variable 的符号,这些 variables 最终被收集为 plist 传入特性相关的处理函数中:

<<@(link,a=1,...)>> => 
(org-noweb-expand-link link :a 1 ...) => 
(feature-func `(:a 1 ...))

其他

关于使用 plist-conf 传递状态的约定:

见代码注释。

用于收集内置特性的特性集区域:

<<@([[id:org-noweb-expand-link::features]])>>

命令集

#+name: 2025-07-26-10-25
#+begin_src emacs-lisp :eval no
;; 如空,提供一个接口查询特性说明。
((null link)
 (when-let*
     ((f (completing-read
          "Describe feature: "
          features))
      (f (seq-position
          features
          f #'string=))
      (f (nth f features))
      (_ (functionp f)))
   (describe-function f)))
;; 按 Org 元素类型设置元素展开函数。
;; (type expander &optional feature-name)
((eq link 'set-expander)
 (when-let* ((type (car conf))
             (expander (cadr conf)))
   (setf (alist-get
          type org-element-expanders)
         expander)
   (when-let*
       ((f (caddr conf))
        (_ (symbolp expander))
        (f (make-symbol (symbol-name f))))
     (!def f expander)
     (setq features
           (append features `(,f))))))
;; 添加展开后处理函数。
;; (post-process &optional feature-name)
((eq link 'add-post-process)
 (when-let* ((proc (car conf)))
   (push proc post-process)
   (when-let*
       ((f (cadr conf))
        (_ (symbolp proc))
        (f (make-symbol (symbol-name f))))
     (!def f proc)
     (setq features
           (append features `(,f))))))
;; 添加 Babel Variable.
;; (variable)
((eq link 'add-babel-var)
 (when (car conf)
   (setq
    babel-variables
    (append babel-variables `(,(car conf))))))
;; export log function
((eq link 'log) log)
((eq link 'features) features)
((eq link 'version) nil)
#+end_src

特性集

#+name: 2025-07-26-15-11
#+begin_src emacs-lisp :eval no
<<@([[id:org-noweb-expand-link::feature:expand-src-block]])>>
<<@([[id:org-noweb-expand-link::feature:expand-drawer]])>>
<<@([[id:org-noweb-expand-link::feature:conditional-tangle]])>>
<<@([[id:org-noweb-expand-link::feature:outline-adjust]])>>
<<@([[id:org-noweb-expand-link::feature:remove-whitespace-prefix]])>>
#+end_src

构建用碎片(有些碎片应该也可以复用,待优化,甚至复用其他地方的碎片)。

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

org-noweb-expand-link 完整代码,用于 eval 或 tangle.

#+name: 2025-07-26-11-26
#+header: :tangle (org-sbe "2025-07-26-11-30" ":eval yes")
#+header: :eval (org-sbe "2025-07-26-11-30" ":eval yes" (ok \"yes\"))
#+begin_src emacs-lisp :noweb yes :results silent :lexical t
<<@([[id:org-noweb-expand-link::org-noweb-expand-link]])>>
#+end_src

Eval 及 Tangle 入口

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

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

映射表

#+name: 2025-07-26-15-13
#+begin_src emacs-lisp :results silent
"org-noweb-expand-link::org-noweb-expand-link"
"ec:org-tangle/29663::2025-06-20-21-35"

"org-noweb-expand-link::entry"
"ec:org-tangle/29663::2025-07-26-10-15"
"org-noweb-expand-link::entry-docstring"
"ec:org-tangle/29663::2025-07-26-10-14"
"org-noweb-expand-link::private"
"ec:org-tangle/29663::2025-07-26-09-47"
"org-noweb-expand-link::cmd-set"
"ec:org-tangle/29663::2025-07-26-10-25"

"org-noweb-expand-link::locate"
"ec:org-tangle/29663::2025-07-26-10-27"
"org-noweb-expand-link::expand"
"ec:org-tangle/29663::2025-07-26-10-30"

"org-noweb-expand-link::features"
"ec:org-tangle/29663::2025-07-26-15-11"

"org-noweb-expand-link::log"
"ec:org-id-remap/29814::log"

"org-noweb-expand-link::org-babel-register"
"ec:org-tangle/29663::2025-07-26-10-32"

"org-noweb-expand-link::feature:expand-src-block"
"ec:org-tangle/29663::2025-07-26-10-48"
"org-noweb-expand-link::expand-src-block"
"ec:org-tangle/29663::2025-07-26-10-50"

"org-noweb-expand-link::feature:expand-drawer"
"ec:org-tangle/29663::2025-07-26-10-54"
"org-noweb-expand-link::expand-drawer"
"ec:org-tangle/29663::2025-07-26-10-53"

"org-noweb-expand-link::feature:conditional-tangle"
"ec:org-tangle/29663::2025-07-26-11-02"
"org-noweb-expand-link::feat-doc:conditional-tangle"
"ec:org-tangle/29663::2025-07-26-11-00"
"org-noweb-expand-link::conditional-tangle"
"ec:org-tangle/29663::2025-07-26-11-01"

"org-noweb-expand-link::feature:outline-adjust"
"ec:org-tangle/29663::2025-07-26-11-06"
"org-noweb-expand-link::feat-doc:outline-adjust"
"ec:org-tangle/29663::2025-07-26-11-04"
"org-noweb-expand-link::outline-adjust"
"ec:org-tangle/29663::2025-07-26-11-05"

"org-noweb-expand-link::feature:remove-whitespace-prefix"
"ec:org-tangle/29663::2025-07-26-11-09"
"org-noweb-expand-link::feat-doc:remove-whitespace-prefix"
"ec:org-tangle/29663::2025-07-26-11-07"
"org-noweb-expand-link::remove-whitespace-prefix"
"ec:org-tangle/29663::2025-07-26-11-08"
#+end_src

Eval 及 Tangle 入口

#+name: 2025-07-26-11-21
#+begin_src emacs-lisp :eval no
(org-id-remap
 "org-noweb-expand-link::2025-07-26-15-13"
 "https://emacs-china.org/t/org-tangle/29663::2025-07-26-15-13"
 )
#+end_src
#+name: 2025-07-26-11-22
#+header: :depend (org-sbe "2025-07-26-11-21" ":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-noweb-expand-link::2025-07-26-15-13]])>>
 )
(org-with-wide-buffer (org-sbe "2025-07-26-11-27" ":eval yes"))
#+end_src

更新

locate 符号化;基于 org-exec 的构建。

1 locate 符号化

动机:上个版本中,我们已经裁掉了 org-noweb-expand-link 展开远程资源的特性,转而把展开远程资源的特性支持借由 org-id-remap 实现。现在,我们仿照 org-exec, 利用 link-open-conf 扩展 org-noweb-expand-linklocate 函数,以使其支持展开远程资源,无需依赖 org-id-remap.

locate => (locate (make-symbol "locate"))

#+name: 2025-08-03-16-19
#+begin_src emacs-lisp :eval no
(locate (make-symbol "locate"))
expand features log
babel-variables
org-element-expanders post-process
#+end_src

提供一个命令以便导出 locate: (org-noweb-expand-link 'locate).

#+name: 2025-08-03-16-20
#+begin_src emacs-lisp :eval no
;; 如空,提供一个接口查询特性说明。
((null link)
 (when-let*
     ((f (completing-read
          "Describe feature: "
          features))
      (f (seq-position
          features
          f #'string=))
      (f (nth f features))
      (_ (functionp f)))
   (describe-function f)))
;; 按 Org 元素类型设置元素展开函数。
;; (type expander &optional feature-name)
((eq link 'set-expander)
 (when-let* ((type (car conf))
             (expander (cadr conf)))
   (setf (alist-get
          type org-element-expanders)
         expander)
   (when-let*
       ((f (caddr conf))
        (_ (symbolp expander))
        (f (make-symbol (symbol-name f))))
     (!def f expander)
     (setq features
           (append features `(,f))))))
;; 添加展开后处理函数。
;; (post-process &optional feature-name)
((eq link 'add-post-process)
 (when-let* ((proc (car conf)))
   (push proc post-process)
   (when-let*
       ((f (cadr conf))
        (_ (symbolp proc))
        (f (make-symbol (symbol-name f))))
     (!def f proc)
     (setq features
           (append features `(,f))))))
;; 添加 Babel Variable.
;; (variable)
((eq link 'add-babel-var)
 (when (car conf)
   (setq
    babel-variables
    (append babel-variables `(,(car conf))))))
;; export log function
((eq link 'log) log)
((eq link 'locate) locate)
((eq link 'features) features)
((eq link 'version) nil)
#+end_src

映射表:

注:由于这里使用了 org-id-remap 表项覆盖 (2) 特性,所以,想要构建这个版本,需要先将 org-id-remap 升级到支持该特性的版本。

#+name: 2025-08-03-16-39
#+begin_src emacs-lisp :eval no
(org-id-remap
 "map-table-base"
 "https://emacs-china.org/t/org-tangle/29663::2025-07-26-15-13")
#+end_src
#+name: 2025-08-03-16-38
#+header: :depend (org-sbe "2025-08-03-16-39" ":eval yes")
#+begin_src emacs-lisp :eval no
<<@([[id:map-table-base]])>>
nil nil
"org-noweb-expand-link::private"
"ec:org-tangle/29663::2025-08-03-16-19"
"org-noweb-expand-link::cmd-set"
"ec:org-tangle/29663::2025-08-03-16-20"
#+end_src

2 基于 org-exec 的构建

构建入口:

#+name: 2025-08-03-16-42
#+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-tangle/29663::2025-07-26-11-26"
 "map-table"
 "https://emacs-china.org/t/org-tangle/29663::2025-08-03-16-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-noweb-expand-link.el")
  'load (or load "yes"))
#+end_src

特性:动态 Block 展开

背景

当前, elisp docstring 可以通过 drawer 导入代码中,然 drawer 无类似 src-block 的 header-args, 而 dynamic block 有 arguments, 故新增此特性。至此,headline, src-block, dynamic-block 都可实现某种共通的 proerties 机制。

实现

#+name: 2025-08-17-16-48
#+begin_src emacs-lisp :eval no
(!let ((expand (make-symbol "expand-dynamic-block")))
 (!def expand
  (lambda (ele conf)
    ;; conf 输入输出
    ;; 输入: :link.
    ;; 输出: :block-name, :failed, :err-msg.
    (let* ((link (plist-get conf :link))
           (beg (org-element-contents-begin ele))
           (end (org-element-contents-end ele))
           ;; org-element parser 与 org.el 中
           ;; 的不一致,导致解析失败,所以有了这段
           ;; workground 代码。
           (org-element-dynamic-block-open-re
            org-dblock-start-re)
           (ele
            (save-excursion
              (save-match-data
                (re-search-forward
                 org-dblock-start-re nil t)
                (forward-line 0)
                (org-element-dynamic-block-parser
                 nil nil))))
           (args (eval
                  (read
                   (format
                    "'(%s)" (org-element-property
                             :arguments ele)))))
           (block-name
            (org-element-property :block-name ele)))
      (cond
       ;; 如果块参数 `:expand' 存在,且其 sexp 求值
       ;; 为 nil 或 "no",
       ((and
         (plist-member args :expand)
         (member (eval (plist-get args :expand))
                 '(nil "no")))
        ;; 表明该块在当前环境下拒绝展开。
        (plist-put conf :failed t)
        (plist-put conf :err-msg
                   (format "ignore %s." link))
        "")
       (t
        (plist-put conf :block-name block-name)
        (string-trim
         (buffer-substring beg end)))))))
 (org-noweb-expand-link
  'set-expander 'dynamic-block expand
  'expand-dynamic-block))
#+end_src

新构建目标:

由于先前版本提供了新增特性的接口,于此在上版本尾部加入 动态 Block 展开 特性。若整体结构中的 entry 符号更名为 org-noweb-expand-link, 此特性——动态 Block 展开——才能纳入特性集中,但历史版本的变更涉及大重构,此贴暂不处理。

#+name: 2025-08-17-16-45
#+header: :tangle (org-sbe "2025-07-26-11-30" ":eval yes")
#+header: :eval (org-sbe "2025-07-26-11-30" ":eval yes" (ok \"yes\"))
#+begin_src emacs-lisp :noweb yes :results silent :lexical t
<<@([[id:org-noweb-expand-link::org-noweb-expand-link]])>>

<<@([[id:org-noweb-expand-link::feature:expand-dynamic-block]])>>
#+end_src

映射表:

#+name: 2025-08-17-16-43
#+begin_src emacs-lisp :eval no
(org-id-remap
 "map-table-base"
 "https://emacs-china.org/t/org-tangle/29663::2025-08-03-16-38")
#+end_src
#+name: 2025-08-17-16-42
#+header: :depend (org-sbe "2025-08-17-16-43" ":eval yes")
#+begin_src emacs-lisp :eval no
<<@([[id:map-table-base]])>>
nil nil
"org-noweb-expand-link"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-16-45"
"org-noweb-expand-link::feature:expand-dynamic-block"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-16-48"
#+end_src

构建入口:

#+name: 2025-08-17-16-44
#+header: :var tangle=(ignore) load=(ignore)
#+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-tangle/29663::2025-08-17-16-45"
 "map-table"
 "https://emacs-china.org/t/org-tangle/29663::2025-08-17-16-42")
(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-noweb-expand-link.el")
  'load (or load "yes"))
#+end_src

特性: Elisp docstring 格式化

扩展把: 前缀空白字符移除

文档

#+name: 2025-08-17-17-18
#+begin: elisp-docstring

将文本格式化为 elisp docstring。转义文本中的特殊字
符,并移除文本在根节点的缩进。

使用:

将文本置于 Org Dynamic Block elisp-docstring 中。
见 Info node ‘(org)Dynamic Blocks’.

-- 特殊字符转义

“"”转义“\"”“\”转义“\\”,此外,在 elisp
docstring 中,字符“`”“'” 需用“\=”转义。
见 Info node ‘(elisp)Text Quoting Style’.

-- 缩进移除

如片段展开的最终结果嵌套在另外的片段中,且含缩进,
移除其缩进。

考虑如下代码块:

a:
(progn
    <​<b>>)

b:
(def remove-indent
  "Remove indent.
<​<c>>")

c:
移除文本在根节点的缩进。

正常情况下,展开 a 得:

(progn
    (def remove-indent
      "Remove indent
    移除文本在根节点的缩进。"))

受 a 缩进影响,作为 b 的 docstring, c 每行都被填
充了缩进用的空白字符。移除缩进后,可得如下结果:

(progn
    (def remove-indent
      "Remove indent
移除文本在根节点的缩进。"))

#+end:

实现

由于此处 docstring 涉及自引用,此版本不可在 动态 Block 展开 的基础上构建,必须 先于 或 与动态 Block 展开同时 构建。

#+name: 2025-08-17-17-19
#+begin_src emacs-lisp :eval no
(lambda (&optional body conf)
  "Format emacs lisp docstring.

<<@([[id:org-noweb-expand-link::doc:format-elisp-docstring]])>>"
  ;; 依赖: elisp-doc.
  (let* ((name "elisp-docstring")
         (mark "org-noweb-expand-link:")
         (mark (concat mark name)))
    (cond
     ;; 无 body 时当 hook 用;
     ((null body)
      (remove-hook 'org-babel-tangle-body-hook
                   elisp-doc)
      (goto-char (point-min))
      (setq mark (concat "^[ \t]*" mark))
      (while (re-search-forward mark nil t)
        (replace-match "")))
     ;; 有 body 时转义特殊字符,并往所有行行首加 mark.
     ((and
       (equal (plist-get conf :block-name) name)
       (not (plist-get conf :failed)))
      (add-hook 'org-babel-tangle-body-hook
                elisp-doc)
      (with-temp-buffer
        (save-excursion (insert body))
        (while (re-search-forward
                "[`'\"\\]" nil t)
          (pcase (match-string 0)
            ("`" (replace-match "\\\\=`" nil t))
            ("'" (replace-match "\\\\='" nil t))
            ("\"" (replace-match "\\\"" nil t))
            ((and "\\"
                  (guard (looking-at-p "=")))
             (replace-match "\\\\=\\\\" nil t))
            ("\\"
             (replace-match "\\\\" nil t))))
        (setq body (buffer-string)))
      (string-join
       (mapcar
        (lambda (line) (concat mark line))
        (string-split body "\n"))
       "\n"))
     (t body))))
#+end_src
#+name: 2025-08-17-17-21
#+begin_src emacs-lisp :eval no
(!let ((elisp-doc
        (make-symbol
         (concat
          "org-noweb-expand-link" ":"
          "elisp-doc"))))
 (!def elisp-doc
  <<@([[id:org-noweb-expand-link::format-elisp-docstring]])>>)
 (defun org-dblock-write:elisp-docstring (p)
   (insert (string-trim (plist-get p :content))))
 (org-noweb-expand-link
  'add-post-process elisp-doc
  'format-elisp-docstring))
#+end_src

构建目标:

#+name: 2025-08-17-17-30
#+header: :tangle (org-sbe "2025-07-26-11-30" ":eval yes")
#+header: :eval (org-sbe "2025-07-26-11-30" ":eval yes" (ok \"yes\"))
#+begin_src emacs-lisp :noweb yes :results silent :lexical t
<<@([[id:org-noweb-expand-link]])>>

<<@([[id:org-noweb-expand-link::feature:format-elisp-docstring]])>>
#+end_src

映射表:

#+name: 2025-08-17-17-27
#+begin_src emacs-lisp :eval no
(org-id-remap
 "map-table-base"
 "https://emacs-china.org/t/org-tangle/29663::2025-08-17-16-42")
#+end_src
#+name: 2025-08-17-17-28
#+header: :depend (org-sbe "2025-08-17-17-27" ":eval yes")
#+begin_src emacs-lisp :eval no
<<@([[id:map-table-base]])>>
nil nil
"org-noweb-expand-link"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-16-45"
"org-noweb-expand-link::feature:format-elisp-docstring"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-17-21"
"org-noweb-expand-link::format-elisp-docstring"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-17-19"
"org-noweb-expand-link::doc:format-elisp-docstring"
"https://emacs-china.org/t/org-tangle/29663::2025-08-17-17-18"
#+end_src

构建入口:

#+name: 2025-08-17-17-29
#+header: :var tangle=(ignore) load=(ignore)
#+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-tangle/29663::2025-08-17-17-30"
 "map-table"
 "https://emacs-china.org/t/org-tangle/29663::2025-08-17-17-28")
(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-noweb-expand-link.el")
  'load (or load "yes"))
#+end_src

自举版本

一个自举用的极简版本:

#+name: 2025-08-17-18-08
#+begin_src emacs-lisp :lexical t :results silent
;;; Org Noweb Expand Link  -*- lexical-binding: t; -*-
(!def 'org-noweb-expand-link
 (!let
  (org-noweb-expand-link
;;;; Private
   (locate (make-symbol "locate")) expand org-element-expanders
   set-expander post-processes add-post-process babel-variables
   add-babel-var ref-properties add-ref-prop (log (lambda (&rest _))))

;;;; org-noweb-expand-link
  ;; `org-noweb-expand-link' 函数入口
  (!def org-noweb-expand-link
   (lambda (link &rest conf)
     "Org Noweb Extension.

   "
     (interactive (list nil))
     (cond
      ((stringp link)
       (log "%s" link)
       (let ((marker (locate link)) body)
         (cond
          (marker
           (plist-put conf :link link)
           (org-with-point-at marker
             ;; 如果展开失败,设置 :failed 及:err-msg, 并将 body 置为空串。
             (setq body (expand conf))))
          (t
           (plist-put conf :failed t)
           (plist-put conf :err-msg (format "failed to expand %s" link))
           (setq body "")))

         (dolist (p post-processes) (setq body (funcall p body conf)))

         ;; display warning
         (when (plist-get conf :failed)
           (display-warning
            'org-noweb (plist-get conf :err-msg) :warning))
         body))
      ((symbolp link)
       ;; export symbols
       (ignore set-expander add-post-process add-babel-var add-ref-prop)
       (and-let*
           ((f org-noweb-expand-link) (f (alist-get link (aref f 2))))
         (if conf (apply f conf) f))))))

  ;; 按 Org 元素类型设置元素展开函数。
  (!def set-expander
   (lambda (type expander)
     (when (and type (symbolp type) expander)
       (setf (alist-get type org-element-expanders) expander))))

  ;; 添加展开后处理函数。
  (!def add-post-process
   (lambda (proc) (when proc (push proc post-processes))))

  ;; 添加 Babel Variable.
  (!def add-babel-var
   (lambda (var)
     (when var (setq babel-variables (append babel-variables `(,var))))))

  ;; 添加文本块属性/变量
  (!def add-ref-prop
   (lambda (p)
     (when p (setq ref-properties (append ref-properties `(,p))))))

;;;; Locate
  ;; 定位 `link' 所指。
  ;; 输入: `link', Org 链接。
  ;; 输出: marker, `link' 所指,或 nil.
  (!def locate
   (lambda (link &optional signal-errors)
     (let ((inhibit-message t)
           (message-log-max nil)
           (org-link-search-must-match-exact-headline t)
           (org-link-frame-setup
            `((file . find-file-other-window) ,@org-link-frame-setup))
           (marker (make-marker)))
       (condition-case msg
           (save-window-excursion
             ;; 这个 guard 实际只对当前 buffer 有效,无法处理 open-link 跑
             ;; 到别的 buffer 的情况。但考虑到有时我们会 open 当前buffer 中
             ;; 的 link, 为了防止因为可见性引发的链接查找失败,我们还是在这
             ;; 里加上这个 gaurd, 当然,也许还有更好的实现方法,但到时再说。
             (org-with-wide-buffer
              (if (not (stringp link)) (org-link-open link t)
                (org-link-open-from-string link t))
              (set-marker marker (point))))
         (error (when signal-errors (signal (car msg) (cdr msg))))))))

;;;; Expand
  ;; 展开当前位置的 Org 元素
  ;; 输入: `conf', 同 org-noweb-expand-link 入参。
  ;; 输出: string, Org 元素展开后的文本。
  (!def expand
   (lambda (conf) "展开位于 current point 的 Org element."
     (!let*
      ((ele (org-element-at-point))
       (type (org-element-type ele))
       (expand (alist-get type org-element-expanders)))
      (cond
       ((and expand (expand ele conf)))

       ;; 未定义
       (t
        (plist-put conf :failed t)
        (plist-put
         conf :err-msg
         (format
          "%s unsupported target %S %S" link (current-buffer) (point)))
        "")))))

;;;; src-block Expander
  (!let ((expand (make-symbol "expand-src-block")))
   (!def expand
    (lambda (ele conf)
      ;; conf 输入输出输入: :link. 输出: :lang, :failed, :err-msg.
      (let* ((link (plist-get conf :link))
             (info (org-babel-get-src-block-info))
             (params (nth 2 info)))
        (cond
         ;; 如果代码块参数 `:expand' 存在且其 sexp 求值为 nil 或 "no",
         ((and
           (assq :expand params)
           (member (alist-get :expand params) '(nil "no")))
          ;; 表明该代码块在当前环境下拒绝展开。
          (plist-put conf :failed t)
          (plist-put conf :err-msg (format "ignore %s." link))
          "")
         (t
          (mapcar
           (lambda (p) (plist-put conf p (alist-get p params)))
           ref-properties)
          (plist-put conf :lang (nth 0 info))
          (org-babel-expand-noweb-references info))))))
   (org-noweb-expand-link 'set-expander 'src-block expand)
   (org-noweb-expand-link 'add-feature 'expand-src-block expand))

;;;; Prelude
  ;; 注册 org-babel 函数 @
  (with-temp-buffer
    (org-mode) (log "register org babel function: @")
    (let* ((vars
            `((level (ignore))
              ,@(mapcar (lambda (v) `(,v (ignore))) babel-variables)))
           (ha
            (mapconcat
             (lambda (v) (format ":var %s=%s" (car v) (cadr v))) vars
             " "))
           (a
            (mapconcat
             (lambda (v) (format ":%s %s" (car v) (car v))) vars " ")))
      (insert
       "#+name: @\n" "#+begin_src emacs-lisp " ":var link=\"\" " ha "\n"
       "(org-noweb-expand-link " "(format \"%s\" link) " a ")\n"
       "#+end_src")
      (org-babel-lob-ingest)))

;;;; End
  org-noweb-expand-link))
#+end_src