背景
在笔记系统的自动化方面常常有这样的需求:如果某些节点不存在,创建这些节点,如存在,视情况更新它们的内容。
Org mode 提供的 org-capture 特性一定程度上处理了节点创建相关的方面,但我们还需要某种检测及更新机制:检测目标节点在笔记系统中是否存在,并提供一个更新已存在节点的机制。
概述
org-autogen-defentry, 一个Org节点自动化工具。
注:本文中的 Org节点 特指 Org entry.
org-autogen-defentry 的核心逻辑很简单:目标节点存在则视情况更新,不存在则创建(并写入内容)。
为实现上述逻辑, org-autogen-defentry 定义了一些接口,比如判断当前位置是否是目标节点类型的 here?; 又比如提供节点信息的 info 以及根据节点信息寻找目标节点位置的 find; 还有用于判断目标节点是否需要写入内容的 dirty?; 用于更新目标节点内容的 update; 用于生成目标节点元数据和内容的 meta-data 及 content; 用于插入目标节点内容的 insert 等。(一个用例:仅往节点中插入内容)
这些接口的实现都需要由调用方提供给 org-autogen-defentry. org-autogen-defentry 通过这些接口将 节点的具体定义细节 与 节点的自动创建及更新逻辑 隔离开。
注:注意这两个概念:“节点定义细节”;“节点更新及创建逻辑”. org-autogen-defentry 只负责后者。
用例
以下的用例展示了通过 org-autogen-defentry 定义一个用于将日总结自动化的节点类型。
(org-autogen-defentry
;; 定义名为 day-log 的节点类型。
;; 可通过 M-x org-entry:day-log 创建或
;; 更新现存节点。
day-log
;; 通过“意识流”标签及是否具备 CUSTOM_ID
;; 判断当前位置是否目标节点。
"+意识流+CUSTOM_ID={.}"
;; 寻找、创建目标节点所需的信息。
:info (org-read-date nil t)
;; 通过 match 寻找目标节点。
:find-match
(format
"+ITEM={%s}+意识流+CUSTOM_ID={.}"
(format-time-string "%y.%-m.%-d" info))
;; match 寻找的范围。
:find-match-scope '("~/org/stream.org")
;; 创建目标节点时,节点的元数据。
:meta-data
(list
:ITEM (format-time-string
"%y/%-m/%-d" info)
:TAGS "意识流"
:CUSTOM_ID
(format-time-string "%Y-%m-%d" info)
:CREATE_TIME
(format-time-string
(org-time-stamp-format t t) info))
;; 目标节点的内容
:content
(concat "#+begin: ts-text :scope all\n"
"#+end:")
;; 目标节点将存于何处?
:target
'(id "a867203a-ab58-40b9-becb-1443f700a391")
;; 目标节点将如何更新?
:update (org-map-dblocks))
执行上述代码片段后, org-autogen-defentry 将定义一个名为 org-entry:day-log 的 Emacs 命令,节点的更新或创建可通过 M-x org-entry:day-log 完成。
本文后续将逐步深入 org-autogen-defentry 的实现,介绍其更新或创建节点的逻辑,以及其配置参数的具体用法。
接口说明
(org-autogen-defentry NAME HERE? &rest CONF)
定义一个名为 NAME 的自动化节点类型,其节点可通过
M-x org-entry:NAME
更新或创建。
HERE? 判断此处的节点是否为目标类型。
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::here?]])>>
CONF
:info 定位、创建节点时所需的节点信息。
:meta-data 诸如标题、标签、属性等节点的元数据。
:content 节点的内容。
:find 定位节点所在位置。
:find-match 通过 Org Match String 定位节点。
:find-match-scope :find-match 的寻找范围。
:dirty? 判断是否需要插入节点内容。
:insert 节点内容的插入函数。
:update 用于更新节点的函数。
:update-just-created 是否更新刚刚创建的节点。
:update-here 是否更新此处的节点。
:update-elsewhere 是否更新别处的节点。
:pre 用于扩展的前处理函数。
:post 用于扩展的后处理函数。
:target ‘org-capture-templates’ target.
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::conf]])>>
节点命令
如前文所述,对于名为 NAME 的节点类型, org-autogen-defentry 会生成一个名为 org-entry:NAME 的 Emacs 命令。节点则通过 M-x org-entry:NAME 更新或创建。
org-entry:NAME 的结构如下所示:
`(defun ,(intern (concat ns ":" n))
(&optional info &rest args)
(interactive)
(!let ((cmd this-command) ret
(info info) (find ,find)
(here? ,here?) (dirty? ,dirty?)
(insert ,insert) (update ,update)
(meta-data ,meta-data) (content ,content)
(target ,target) (pre ,pre) (post ,post))
(catch ',return-sym
;; PRE
(apply pre info args)
;; 节点更新及创建逻辑
(!def ret
(cond
;; 更新此处节点
<<@([[id:org-autogen-defentry::org-entry:x--update-here]])>>
;; 获取节点信息
<<@([[id:org-autogen-defentry::org-entry:x--read-info]])>>
;; 更新别处节点
<<@([[id:org-autogen-defentry::org-entry:x--update-elsewhere]])>>
;; 创建新节点
<<@([[id:org-autogen-defentry::org-entry:x--create]])>>))
;; POST
(when (and post (markerp ret))
(let ((this-command cmd)) (post ret)))
ret)))
对于 org-autogen-defentry 的“节点更新及创建逻辑”而言,输入只需节点信息 (info), 输出只有节点位置 (marker). 不过,对于 org-entry:NAME 而言,它本身除了作为 Emacs 命令用以更新或创建节点之外,它同时还可以作为一个 elisp 接口。作为 elisp 接口时,它可以有除 info 之外的其他输入,输出还可能是其他数据结构,其具体定义属于“节点定义细节”,不属于 org-autogen-defentry 所负责的“节点更新及创建逻辑”。
如其结构所示,通过 pre 参数和 catch 语句, org-entry:NAME 可以绕开“节点更新及创建逻辑”,以作为一个纯粹的 elisp 函数使用。
创建节点
org-autogen-defentry 将一个节点划分为两个部分:包含标题、标签、属性等信息的节点元数据 (meta-data), 除了元数据之外的节点内容 (content), 如下所示:
* ITEM TAGS
:PROPERTIES:
:P1: V1:
:P2: V2:
:END:
--此上为节点元数据,此下为节点内容--
通常,有一些特别的 block, 如
dynamic block, 会作为“更新节点内容”
的目标,被视为自动内容。
此外,节点中也许还有手动部分,这部分
不在节点更新范围中。
为了实现节点自动化, org-autogen-defentry 将节点生成的过程分为“创建节点模板”及“更新节点内容”两个步骤,并且将“节点内容”进一步区分为手动部分和自动部分。其中,节点内容的自动部分是 org-autogen-defentry “更新节点内容”的更新目标。
org-capture 为创建节点提供了丰富的特性,比如指定节点的存储位置,比如替换 org-capture template 中的模板参数等. 故在创建新节点时, org-autogen-defentry 将直接使用 org-capture, 并裁剪 org-capture 的配置参数, 比如 org-autogen-defentry 的 :target 参数 等价于 org-capture 的 target 参数;而 org-capture 的 template 将由 org-autogen-defentry 的 :info, :meta-data, :content, :update 配合参数生成。
注:本文中的“节点模板”和 org-capture template 非同一概念。“创建节点模板”并“更新节点内容”所得的最终结果才是 org-capture template.
org-autogen-defentry 创建节点的逻辑如下:
org-autogen-defentry::org-autogen-defentry–create
((setq update ,update-just-created)
(require 'org-capture)
(let* ((org-capture-mode-hook nil)
(org-capture-templates
`(("t" ""
entry ,target
,(with-temp-buffer
(setq insert nil)
(org-mode)
;; 创建节点模板
(save-excursion
(insert (meta-data info))
(insert (content)))
;; 更新节点内容
(update)
(buffer-string))
:immediate-finish t))))
(org-capture nil "t")
org-capture-last-stored-marker))
更新节点
更新节点涉及“寻找目标节点”以及“更新节点内容”,而寻找目标节点又可细分为:判断当前位置是否是目标节点 或 寻找位于其他位置的目标节点,故更新节点细分为“更新此处节点”和“更新别处节点”。
进行此区分的原因在于:只有需要定位位于别处的节点时才需要输入节点信息 (info), 该信息通常由用户以交互的方式提供。
注:想象一下,你当前已经打开了某个类型为 NAME 的节点, 此时,若你在该节点上 M-x org-entry:NAME, org-entry:NAME 将断定你的意图为更新当前这个打开的节点,而不是向你询问节点信息。
更新此处节点:
org-autogen-defentry::org-autogen-defentry–update-here
;; info 非空时直接进入查找节点的分支。
((and here? (null info) (here?))
(setq update ,update-here)
(save-excursion
(org-back-to-heading)
(setq ret (point-marker))
(unless (dirty?) (insert (content)))
(goto-char ret)
(update)
ret))
当此处非目标节点时, org-entry:NAME 将试图通过输入的节点信息 info 寻找目标节点。
获取节点信息:
org-autogen-defentry::org-autogen-defentry–read-info
;; 通常由用户交互式输入,也可由程序输入。
((ignore
(setq info (if info info ,info))
(when (functionp info)
(let ((this-command cmd))
(setq info (info))))))
更新别处节点:
org-autogen-defentry::org-autogen-defentry–update-elsewhere
((and find (setq ret (find info)))
(setq update ,update-elsewhere)
(org-with-point-at ret
(unless (dirty?) (insert (content)))
(goto-char ret)
(update)
ret))
作为elisp接口
在详细介绍 org-autogen-defentry 的各个配置参数之前,我们先介绍一个关于 pre 参数的特殊用例。
如前文所述,通过 pre 参数和 catch 语句, org-entry:NAME 可以绕开“节点更新及创建逻辑”,作为一个纯粹的 elisp 函数使用.
:pre 的定义如下:
org-autogen-defentry::pre
(!def pre
(pcase pre
;; lambda?
(`(lambda . ,_)
`(!let ((here? ,here?)
(return ,return))
,pre))
;; let 裹 lambda?
(`(,(and (or 'let* 'let
'!let '!let*))
,bindings .
,(and body
(guard
(functionp
(car (last body))))))
(eval
`(!let ((here? ,here?)
(return ,return))
,pre)
t))
;; sexp
(_ `(!let ((here? ,here?)
(return ,return))
(lambda (info &rest args)
,pre)))))
考虑这样一个节点类型:
(org-autogen-defentry X nil
:pre
(let ((hooks nil))
(lambda (info &rest args)
(cond
((eq info 'add-hook)
(push (car args) hooks)
(return hooks))
((eq info 'hook)
(return hooks)))))
;; ...
:content
(mapconcat
#'funcall (org-entry:X 'hook) "\n")
;; ...
)
在上述样例中, :pre 被配置为一个“被 let 裹住的 lambda”, 该 lambda 中使用了一个由 org-autogen-defentry 预先绑定函数 return. 借助 return,
(org-entry:X 'hook) 将返回上述片段中被 let-绑定 的变量 hooks.
如此, org-autogen-defentry 为用户提供一种“避免往全局环境中引入 org-entry:X–hook”的手段,同时依旧极大程度上保持 org-entry:X 符号的可定义性。
除 return 外, org-autogen-defentry 还绑定了一个函数 here?, 以便有用户欲借 (org-entry:X 'here?) 检测当前位置是否为目标节点,比如,配合 org-ctrl-c-ctrl-c-hook 检测并更新节点。
整体结构
org-autogen-defentry 实现为宏,其结构如下:
;;; org-autogen-defentry -*- lexical-binding: t; -*-
(defmacro org-autogen-defentry (name here? &rest conf)
"Define Org autogen entry.
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry]])>>"
(declare (indent defun))
(!let ((ns "org-entry") (n (format "%s" name))
(prompt (format "Update %s?" name))
<<@([[id:org-autogen-defentry::conf]])>>
query-update gen-update gen-meta-data
(return-sym (gensym "return")) return)
(!def return
(lambda (v) (throw return-sym v)))
<<@([[id:org-autogen-defentry::info]])>>
<<@([[id:org-autogen-defentry::gen-meta-data]])>>
<<@([[id:org-autogen-defentry::meta-data]])>>
<<@([[id:org-autogen-defentry::content]])>>
<<@([[id:org-autogen-defentry::here?]])>>
<<@([[id:org-autogen-defentry::find]])>>
<<@([[id:org-autogen-defentry::dirty?]])>>
<<@([[id:org-autogen-defentry::insert]])>>
<<@([[id:org-autogen-defentry::update]])>>
<<@([[id:org-autogen-defentry::pre]])>>
<<@([[id:org-autogen-defentry::post]])>>
<<@([[id:org-autogen-defentry::org-entry:x]])>>))
org-autogen-defentry::conf
(dirty? (plist-get conf :dirty?))
(content (plist-get conf :content))
(meta-data (plist-get conf :meta-data))
(info (plist-get conf :info))
(find (plist-get conf :find))
(target (plist-get conf :target))
(pre (plist-get conf :pre))
(post (plist-get conf :post))
(insert (plist-get conf :insert))
(update (plist-get conf :update))
(update-here
(plist-get conf :update-here))
(update-elsewhere
(plist-get conf :update-elsewhere))
(update-just-created
(plist-get conf :update-just-created))
配置参数
至此,我们基本介绍完了 org-autogen-defentry 为实现节点自动化而设计的“节点更新及创建逻辑”以及一些特殊用法,其中涉及了许多配置参数,现依次说明。
info
:info 定位、创建节点时所需的节点信息。
类型: sexp, (lambda nil _).
‘org-autogen-defentry’ 的节点更新及创建逻辑并不直接使用该
参数,而是将其传递给 :meta-data, :find, :find-match 等。
org-autogen-defentry::info
(!def info
(cond ((functionp info) info)
(`(lambda nil ,info))))
meta-data
:meta-data 诸如标题、标签、属性等节点的元数据。
类型: sexp, str, (lambda (info) (or str plist)).
为 sexp 时, info 绑定为 :info 返回值。
当输出 plist 时, plist 中的 键值对 将生成节点的属性及属性
值。其中, :ITEM 和 :TAGS 将被特殊处理. :ITEM 将生成节点
标题, :TAGS 则生成节点标签。
org-autogen-defentry::meta-data
(!def meta-data
(cond ((functionp meta-data) meta-data)
((stringp meta-data) meta-data)
(`(lambda (info) (or ,meta-data "")))))
(!def meta-data
(!let ((md meta-data))
(lambda (info)
(let ((md (md info)))
(when (listp md)
(setq md (gen-meta-data md)))
md))))
因为 meta-data 支持输出 plist, org-autogen-defentry 中提供如下函数将 plist 类 meta-data 转化为 string 类 meta-data.
org-autogen-defentry::gen-meta-data
(!def gen-meta-data
(lambda (meta)
(with-temp-buffer
(org-mode)
(let ((insert nil))
(insert "* " (or (plist-get meta :ITEM) ""))
(!def meta (org-plist-delete meta :ITEM))
(goto-char (point-min))
(org-set-tags (plist-get meta :TAGS))
(!def meta (org-plist-delete meta :TAGS))
(mapcar
(lambda (pv)
(org-set-property
(string-trim
(format "%s" (car pv)) ":")
(format "%s" (cadr pv))))
(seq-partition meta 2))
(end-of-buffer)
(insert "\n")
(buffer-string)))))
content
:content 节点的内容。
类型: sexp, (lambda nil str).
执行上下文为目标节点所在位置。
org-autogen-defentry::content
(!def content
(cond ((functionp content) content)
(`(lambda nil (or ,content "")))))
here?
类型: str, (lambda nil bool).
HERE? 通常为 str, 表示 Org Match String. 为 lambda 时
的执行上下文为当前位置。
org-autogen-defentry::here?
(!def here?
(cond ((stringp here?)
`(lambda nil
(when (derived-mode-p 'org-mode)
(ignore-errors
(org-with-wide-buffer
(org-back-to-heading)
(narrow-to-region
(point)
(or (outline-next-heading)
(org-end-of-subtree)))
(goto-char (point-min))
(org-map-entries t ,here?))))))
((null here?) here?)
((functionp here?) here?)
(`(lambda nil ,here?))))
find
:find 定位节点所在位置。
类型: sexp, (lambda (info) marker).
为 sexp 时, info 绑定为 :info 返回值。
当提供 :find-match 时, :find 参数将被忽略。
:find-match 通过 Org Match String 定位节点。
类型: str, sexp, (lambda (info) str).
为 sexp 时, info 绑定为 :info 返回值。
当提供 :find-match 时, :find 参数将被忽略。
:find-match-scope :find-match 的寻找范围。
同 ‘org-map-entries’ SCOPE 参数。
需配合 :find-match 使用。
org-autogen-defentry::find
(!def find
(cond ((plist-get conf :find-match)
(let ((match
(plist-get conf :find-match))
(scope
(plist-get
conf :find-match-scope)))
`(lambda (info)
(car
(org-map-entries
#'point-marker
,(cond
((stringp match) match)
((functionp match)
`(funcall ,match info))
((listp match) match)
((error "Bad `match': %S"
match)))
,scope)))))
((null find) find)
((functionp find) find)
(`(lambda (info) ,find))))
dirty
:dirty? 判断是否需要插入节点内容。
类型: sexp, (lambda nil bool).
dirty? 的执行上下文为目标节点所在位置。
存在内置实现,内置实现通过检测节点内容长度判断是否需插入内容。
org-autogen-defentry::dirty?
(!def dirty?
(cond ((null dirty?)
(lambda nil
(and-let* ((content
(save-mark-and-excursion
(org-mark-subtree)
(deactivate-mark)
(org-end-of-meta-data)
(buffer-substring
(point) (mark))))
(content
(replace-regexp-in-string
"[ \n\t]" "" content))
(_ (length> content 10))))))
((functionp dirty?) dirty?)
(`(lambda nil ,dirty?))))
insert
:insert 节点内容的插入函数。
类型: sexp, (lambda (content) _).
为 sexp 时, content 绑定为 :content 返回值。
insert 的执行上下文为目标节点所在位置。
存在内置实现,内置实现将节点内容插入于节点末尾。
org-autogen-defentry::insert
(!def insert
(cond ((null insert)
(lambda (content)
;; Insert to entry end
(save-excursion
(org-end-of-subtree nil t)
(let ((insert nil))
(insert content)
(unless (eolp) (insert "\n"))))))
((functionp insert) insert)
(`(lambda (content) ,insert))))
update
在更新节点内容方面,为对更新流程的控制提供最大的灵活性, org-autogen-defentry 将 更新时机 细分为三种情况:于此处更新;于别处更新;于创建时更新。这三种情况是否触发更新操作可分别由 :update-here, :update-elsewhere, update-just-created 控制。每个配置参数都有三种选择:静默更新、静默不更新、询问是否更新。
:update 用于更新节点的函数。
类型: sexp, (lambda nil _).
update 的执行上下为目标节点所在位置。
:update-here nil, t, 'query. 默认 t.
:update-elsewhere nil, t, 'query. 默认 nil.
:update-just-created nil, t, 'query. 默认 nil.
更新节点内容在不同情况下的细分配置。nil 不更新; t 更新;
'query 询问更新。
org-autogen-defentry::update
(!def update
(cond ((functionp update) update)
(`(lambda nil ,update))))
(!def query-update
(cond
((equal update (lambda)) (lambda))
((lambda nil
(save-window-excursion
(save-restriction
(org-narrow-to-subtree)
(pop-to-buffer (current-buffer))
(when (y-or-n-p prompt)
(update))))))))
;; update-here 默认询问更新
(!def update-here
(if (plist-member conf :update-here)
update-here 'query))
(!def gen-update
(lambda (type)
(cond
((eq type t) update)
((eq type 'query) query-update)
((lambda)))))
(setq update-here (gen-update update-here))
;; update-elsewhere 默认静默不更新
(setq update-elsewhere
(gen-update update-elsewhere))
;; update-just-created 默认静默不更新
(setq update-just-created
(gen-update update-just-created))
pre&post
为扩展 org-entry:X, org-autogen-defentry 提供了 :pre 及 :post 参数。通过 :pre 可以在控制流进入“节点更新及创建逻辑”之前执行额外的操作,甚至绕过整个节点创建及更新逻辑;通过 :post 可以在“节点更新及创建逻辑”之后执行某些额外的操作,比如跳转至节点所在位置。
pre:
:pre 用于扩展的前处理函数。
类型: sexp, (lambda (info &rest args) _).
为 sexp 时, info, args 绑定同 org-entry:NAME 入参。
为 lambda 且裹于 let 中, :pre 将立即被求值为闭包。
无论何种情况, pre 的执行上下文中均含两个绑定函数 here? 以及
return.
若 pre 中含 return 语句, org-entry:NAME 将绕过节点更新及
创建逻辑, 直接将 (return value) 的 value 作为返回值返回。
post:
:post 用于扩展的后处理函数。
类型: sexp, (lambda (marker) _).
为 sexp 时, location 绑定为目标节点位置 (marker).
org-autogen-defentry::post
(!def post
(cond ((null post) post)
((functionp post) post)
(`(lambda (location) ,post))))
conf
org-autogen-defentry::doc:org-autogen-defentry::conf
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::info]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::meta-data]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::content]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::find]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::dirty?]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::insert]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::update]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::pre]])>>
<<@([[id:org-autogen-defentry::doc:org-autogen-defentry::post]])>>