用于Capture到Org-roam知识库的模板函数

合理的org捕获模板应该使其具备:捕获内容到org-roam知识库中任意一个标题之下。 且这个模板应该有一个函数自动寻找目标地址。

例如,有如下模板:

      (setq org-capture-templates
            '(; For email todo ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
              ("e" "Email todo")
              ("et" "Mail org file" entry (file+headline "~/NutstoreFiles/Nutstore/org-roam/20220715212413-mail.org" "Mails TODO")
               "* TODO %?	 :MAIL:%^{Set tag|WORK|HOME|BABY|}:\n%U %a")
              ("eo" "Mail org file" entry (function my-org/roam-capture-to-headline)
               "* TODO %?	 :MAIL:\n%U %a")
              ("t" "Temp todo templates")
              ("tt" "Temp org file" entry (file+headline "~/NutstoreFiles/Nutstore/org-roam/20220715212959-temp.org" "Temporary task")
               "* TODO %?    :%^{Set tag|WORK|HOME|BABY|}:"
               :jump-to-captured t)
              ("to" "Org-roam TODO" entry (function my-org/roam-capture-to-headline)
               "* TODO %?"
               :jump-to-captured t))

那么如果设置如下函数,可以实现更智能的目标查找。

(defun my-org/roam-capture-to-headline ()
    "Visit an Org-roam file and move point just under a chosen headline."
    (let* ((node (org-roam-node-read))
           (file (org-roam-node-file node))
           (headline
            (with-current-buffer (find-file-noselect file)
              (org-mode)
              (org-element-cache-reset)
              (let ((headlines
                     (org-element-map (org-element-parse-buffer) 'headline
                       (lambda (hl)
                         (org-element-property :raw-value hl)))))
                (completing-read "Choose headline: " headlines nil nil)))))
      (find-file file)
      (goto-char (point-min))
      (if (re-search-forward
           (format org-complex-heading-regexp-format (regexp-quote headline))
           nil t)
          (point)
        (progn
          (goto-char (point-max))
          (unless (bolp) (insert "\n\n"))
          (if (yes-or-no-p "Insert the new headline?")
              (insert (concat "* " headline)))
          (point)))))

2025-11-13更新:

以上,所有各级标题都在一个级别目录内,且无法进行roam跳转。 所以一个更合理的函数是my-org/roam-capture-to-headline,能够把当前roam节点的org文件中所有标题构建一个树形结构,然后按照树形结构逐级来回跳转,同时如果是链接到新节点,那么提供选项跳到新节点。并提供反跳会含有该节点连接的任意节点。 这就真正意义上实现了“漫游”。

  (cl-defstruct hl-node title begin level children parent)


  (defun my-org/parse-headline-tree ()
    "Return headline tree for current buffer."
    (let* ((parsed (org-element-parse-buffer))
           (root (make-hl-node :title "ROOT" :begin (point-min) :level 0 :children nil))
           (stack (list root)))
      (org-element-map parsed 'headline
        (lambda (hl)
          (let* ((title (org-element-property :raw-value hl))
                 (level (org-element-property :level hl))
                 (begin (org-element-property :begin hl))
                 (node (make-hl-node :title title :level level :begin begin :children nil)))
            ;; Pop stack until parent level matches
            (while (> (hl-node-level (car stack)) (1- level))
              (pop stack))
            ;; Add node to parent's children
            (setf (hl-node-children (car stack))
                  (cons node (hl-node-children (car stack))))
            ;; Push node to stack
            (push node stack))))
      ;; Reverse children lists recursively
      (cl-labels ((fix (n)
                    (setf (hl-node-children n)
                          (mapcar #'fix (nreverse (hl-node-children n))))
                    n))
        (fix root))))


  (defun my-org/set-parents (node &optional parent)
    (setf (hl-node-parent node) parent)
    (dolist (c (hl-node-children node))
      (my-org/set-parents c node))
    node)


  (defun my-org/extract-id-from-link (s)
    "Return Org-roam ID if S contains a link of the form [[id:UUID]]."
    (when (string-match "\\[\\[id:\\([-0-9a-f]+\\)\\]\\[[^]]*\\]\\]" s)
      (match-string 1 s)))


  (defun my-org/helm-select-from-tree (node &optional file)
    "Recursively choose a headline from NODE. Return either:
 - an hl-node
 - or (node . new-title) for insertion."
    (let* ((children (hl-node-children node))
           (parent (when (> (hl-node-level node) 0) (hl-node-parent node)))
           (node-title (when node (hl-node-title node)))
           (node-id (when node-title (my-org/extract-id-from-link node-title)))
           (child-cands (mapcar (lambda (c) (cons (hl-node-title c) c))
                                children)))

      (helm
       :prompt (format "Choose in [%s]: " (hl-node-title node))
       :sources
       (list
        ;; ---- Source 1 ----
        (helm-build-sync-source "Actions"
          :match (lambda (_) t)
          :candidates
          (append
           (when parent (list (cons "↑↑ Go up" :up)))
           (when node-id (list (cons "→→ Open linked node" :open-link)))
           (list (cons "++ Create new headline WITH typed prefix" :create-with-prefix))
           (list (cons "+- Create new headline WITHOUT prefix" :create-no-prefix))
           (list (cons "↩↩ Open backlinks" :open-backlinks))
           (list (cons "XX Cancel" :cancel))
           )
          :action
          (lambda (choice)
            (cond
             ((eq choice :up)
              (my-org/helm-select-from-tree parent file))

             ;; open linked node
             ((eq choice :open-link)
              (let ((target (org-roam-node-from-id node-id)))
                (when target
                  (let ((next-file (org-roam-node-file target)))
                    (find-file next-file)
                    (goto-char (point-min))
                    (my-org/helm-select-from-tree
                     (my-org/set-parents
                      (my-org/parse-headline-tree))
                     next-file)))))

             ((memq choice '(:create-with-prefix :create-no-prefix))
              ;; Return: (node . (choice . typed-text))
              (cons file (cons node (cons choice helm-pattern))))

             ((eq choice :open-backlinks)
              ;; Call backlink selector for current file
              (my-org/helm-open-backlinks file))

             ((eq choice :cancel)
              :cancel)
             )))

        ;; ---- Source 2 ----
        (helm-build-sync-source "Headlines"
          :candidates child-cands
          :action (lambda (choice)
                    (my-org/helm-select-from-tree choice file)))))))


  (defun my-org/roam-capture-to-tree ()
    "Use the headline tree selector and insert according to returned action.
Return the point where the content ends up."
    (interactive)
    (let* ((node (org-roam-node-read))
           (file (org-roam-node-file node))
           ;; Step 1: parse headline tree in a temp buffer
           (root (with-current-buffer (find-file-noselect file)
                   (org-mode)
                   (org-element-cache-reset)
                   (my-org/set-parents (my-org/parse-headline-tree))))
           ;; Step 2: let user select headline or create new
           (result (my-org/helm-select-from-tree root file))
           parent-node typed)
      ;; (find-file file)
      ;; (message "result: %S" result)
      (pcase result
        ;; Cancel
        (:cancel
         (user-error "cancel"))

        ;; Create with prefix: insert typed headline under parent
        (`(,target-file . (,parent-node . (:create-with-prefix . ,typed)))
         (find-file target-file)
         (goto-char (hl-node-begin parent-node))
         (unless (string-empty-p typed)
           (org-end-of-subtree t)
           (unless (bolp) (insert "\n"))
           (insert (make-string (+ 1 (hl-node-level parent-node)) ?*) " " typed))
         (cons target-file (point-marker))
         )
        ;; Create NO prefix: insert empty under parent
        (`(,target-file . (,parent-node . (:create-no-prefix . ,_typed)))
         (find-file target-file)
         (goto-char (hl-node-begin parent-node))
         (cons target-file (point-marker)))
        ;; Should not happen
        (_
         (user-error "cancle")))))

  (defun my-org/get-backlink-nodes (file)
    "Return a list of (source-id . source-file) that link to FILE."
    (org-roam-db-query
     [:select [links:source nodes:file]
              :from links
              :left-join nodes
              :on (= links:source nodes:id)
              :where (= links:dest
                        [:select id
                                 :from nodes
                                 :where (= file $s1)])]
     file))

  (defun my-org/helm-open-backlinks (current-file)
    "Helm interface to open backlinks of CURRENT-FILE.
Returns the node struct of the selected backlink file."
    (let* ((backlinks (my-org/get-backlink-nodes current-file))
           (candidates
            (mapcar (lambda (row)
                      (let* ((src-id (car row))
                             (src-file (cadr row))
                             (node (org-roam-node-from-id src-id)))
                        (cons (org-roam-node-title node) node)))
                    backlinks)))
      (helm :prompt "Backlinks: "
            :sources
            (helm-build-sync-source "Backlinked Nodes"
              :candidates candidates
              :action (lambda (node)
                        (let ((file (org-roam-node-file node)))
                          (find-file file)
                          (goto-char (point-min))
                          (my-org/helm-select-from-tree
                           (with-current-buffer (current-buffer)
                             (my-org/set-parents (my-org/parse-headline-tree)))
                           file )))))))
石见 石页 /
在共享协议(CC)下发布于
类别: technology 
标签: org-roam  emacs  知识管理  信息管理  中