branch: externals/denote-org
commit 55400da4af184536e0650096a2422850cdea4662
Author: Protesilaos Stavrou <i...@protesilaos.com>
Commit: Protesilaos Stavrou <i...@protesilaos.com>

    Add WORK-IN-PROGRESS denote-org package
---
 README.md     |   3 +
 denote-org.el | 794 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 797 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..c702beeb42
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# denote-org: Extras to integrate Org mode with Denote (like Org dynamic 
blocks)
+
+WORK-IN-PROGRESS: 
<https://protesilaos.com/codelog/2025-02-11-emacs-splitting-denote-many-packages/>.
diff --git a/denote-org.el b/denote-org.el
new file mode 100644
index 0000000000..cec840e44b
--- /dev/null
+++ b/denote-org.el
@@ -0,0 +1,794 @@
+;;; denote-org-extras.el --- Denote extensions for Org mode -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2024-2025  Free Software Foundation, Inc.
+
+;; Author: Protesilaos Stavrou <i...@protesilaos.com>
+;; Maintainer: Protesilaos Stavrou <i...@protesilaos.com>
+;; URL: https://github.com/protesilaos/denote
+
+;; This file is NOT part of GNU Emacs.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Optional extensions to Denote that work specifically with Org mode.
+
+;;; Code:
+
+(require 'denote)
+(require 'denote-sort)
+(require 'org)
+
+;;;; Link to file and heading
+
+(defun denote-org-extras--get-outline (file)
+  "Return `outline-regexp' headings and line numbers of FILE."
+  (with-current-buffer (find-file-noselect file)
+    (let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p 
outline-regexp) "\\*+ ")))
+          candidates)
+      (save-excursion
+        (goto-char (point-min))
+        (while (if (bound-and-true-p outline-search-function)
+                   (funcall outline-search-function)
+                 (re-search-forward outline-regexp nil t))
+          (push
+           ;; NOTE 2024-01-20: The -5 (minimum width) is a
+           ;; sufficiently high number to keep the alignment
+           ;; consistent in most cases.  Larger files will simply
+           ;; shift the heading text in minibuffer, but this is not an
+           ;; issue anymore.
+           (format "%-5s %s"
+                   (line-number-at-pos (point))
+                   (buffer-substring-no-properties (line-beginning-position) 
(line-end-position)))
+           candidates)
+          (goto-char (1+ (line-end-position)))))
+      (if candidates
+          (nreverse candidates)
+        (user-error "No outline")))))
+
+(define-obsolete-function-alias
+  'denote-org-extras--outline-prompt
+  'denote-org-extras-outline-prompt
+  "3.1.0")
+
+(defun denote-org-extras-outline-prompt (&optional file)
+  "Prompt for outline among headings retrieved by 
`denote-org-extras--get-outline'.
+With optional FILE use the outline of it, otherwise use that of
+the current file."
+  (let ((current-file (or file buffer-file-name)))
+    (completing-read
+     (format "Select heading inside `%s': "
+             (propertize (file-name-nondirectory current-file) 'face 
'denote-faces-prompt-current-name))
+     (denote--completion-table-no-sort 'imenu (denote-org-extras--get-outline 
current-file))
+     nil :require-match)))
+
+(defun denote-org-extras--get-heading-and-id-from-line (line file)
+  "Return heading text and CUSTOM_ID from the given LINE in FILE."
+  (with-current-buffer (find-file-noselect file)
+    (save-excursion
+      (goto-char (point-min))
+      (forward-line (1- line))
+      (cons (denote-link-ol-get-heading)
+            (if (eq denote-org-store-link-to-heading 'context)
+                (org-entry-get (point) "CUSTOM_ID")
+              (denote-link-ol-get-id))))))
+
+(defun denote-org-extras-format-link-with-heading (file heading-id description 
&optional format)
+  "Prepare link to FILE with HEADING-ID using DESCRIPTION.
+Optional FORMAT is the exact link pattern to use."
+  (when (region-active-p)
+    (setq description (buffer-substring-no-properties (region-beginning) 
(region-end)))
+    (denote--delete-active-region-content))
+  (format
+   (or format "[[denote:%s::#%s][%s]]")
+   (denote-retrieve-filename-identifier file)
+   heading-id
+   description))
+
+;;;###autoload
+(defun denote-org-extras-link-to-heading ()
+  "Link to file and then specify a heading to extend the link to.
+
+The resulting link has the following pattern:
+
+[[denote:IDENTIFIER::#ORG-HEADING-CUSTOM-ID]][Description::Heading text]].
+
+Because only Org files can have links to individual headings,
+limit the list of possible files to those which include the .org
+file extension (remember that Denote works with many file types,
+per the user option `denote-file-type').
+
+The user option `denote-org-extras-store-link-to-heading'
+determined whether the `org-store-link' function can save a link
+to the current heading.  Such links look the same as those of
+this command, though the functionality defined herein is
+independent of it.
+
+To only link to a file, use the `denote-link' command.
+
+Also see `denote-org-extras-backlinks-for-heading'."
+  (declare (interactive-only t))
+  (interactive nil org-mode)
+  (unless (derived-mode-p 'org-mode)
+    (user-error "Links to headings only work between Org files"))
+  (let ((context-p (eq denote-org-store-link-to-heading 'context)))
+    (when-let* ((file (denote-file-prompt ".*\\.org"))
+                (file-text (denote-get-link-description file))
+                (heading (denote-org-extras-outline-prompt file))
+                (line (string-to-number (car (split-string heading "\t"))))
+                (heading-data (denote-org-extras--get-heading-and-id-from-line 
line file))
+                (heading-text (car heading-data))
+                (heading-id (if (and context-p (null (cdr heading-data)))
+                                heading-text
+                              (cdr heading-data)))
+                (description (denote-link-format-heading-description file-text 
heading-text)))
+      (insert
+       (denote-org-extras-format-link-with-heading
+        file
+        heading-id
+        description
+        (when (equal heading-text heading-id)
+          "[[denote:%s::*%s][%s]]"))))))
+
+;;;; Heading backlinks
+
+(defun denote-org-extras--get-file-id-and-heading-id-or-context ()
+  "Return link to current file and heading.
+If a CUSTOM_ID is present and the value of the user option
+`denote-org-store-link-to-heading' is set to `context', then return a
+regexp that matches both the CUSTOM_ID and the context of the current
+heading.  This looks like:
+
+    \\(ID::*HEADING-TEXT\\|ID::#HEADING-ID\\)
+
+If CUSTOM_ID is present but `denote-org-store-link-to-heading' is not
+set to `context', then return a patternf of the following form:
+
+    ID::#HEADING-ID"
+  (when-let* ((id (denote-retrieve-filename-identifier-with-error 
buffer-file-name)))
+    (let ((context-p (eq denote-org-store-link-to-heading 'context))
+          (heading-id (org-entry-get (point) "CUSTOM_ID")))
+      (cond
+       ((and context-p heading-id)
+        (format "\\(%s::%s%s\\|#%s\\)" id (shell-quote-argument "*") 
(denote-link-ol-get-heading) heading-id))
+       (context-p
+        (concat id "::" (shell-quote-argument "*") 
(denote-link-ol-get-heading)))
+       (heading-id
+        (concat id "::#" heading-id))
+       (t
+        (error "No way to get link to a heading at point in file `%s'" 
buffer-file-name))))))
+
+(defun denote-org-extras--get-backlinks-buffer-name (text)
+  "Format a buffer name for `denote-org-extras-backlinks-for-heading' with 
TEXT."
+  (format "*Denote HEADING backlinks for %S*" text))
+
+(defun denote-org-extras--get-backlinks-for-heading (file-and-heading-id)
+  "Get backlinks to FILE-AND-HEADING-ID as a list of strings."
+  (when-let* ((files (denote-directory-files nil :omit-current :text-only))
+              (xref-file-name-display 'abs)
+              (matches-in-files (xref-matches-in-files file-and-heading-id 
files))
+              (xref-alist (xref--analyze matches-in-files)))
+    (mapcar
+     (lambda (x)
+       (denote-get-file-name-relative-to-denote-directory (car x)))
+     xref-alist)))
+
+;;;###autoload
+(defun denote-org-extras-backlinks-for-heading ()
+  "Produce backlinks for the current heading.
+This otherwise has the same behaviour as `denote-backlinks'---refer to
+that for the details.
+
+Also see `denote-org-extras-link-to-heading'."
+  (interactive)
+  (when-let* ((heading-id 
(denote-org-extras--get-file-id-and-heading-id-or-context))
+              (heading-text (substring-no-properties 
(denote-link-ol-get-heading))))
+    (denote-link--prepare-backlinks heading-id ".*\\.org" 
(denote-org-extras--get-backlinks-buffer-name heading-text))))
+
+;;;; Extract subtree into its own note
+
+(defun denote-org-extras--get-heading-date ()
+  "Try to return a timestamp for the current Org heading.
+This can be used as the value for the DATE argument of the
+`denote' command."
+  (when-let* ((pos (point))
+              (timestamp (or (org-entry-get pos "DATE")
+                             (org-entry-get pos "CREATED")
+                             (org-entry-get pos "CLOSED"))))
+    (date-to-time timestamp)))
+
+;;;###autoload
+(defun denote-org-extras-extract-org-subtree ()
+  "Create new Denote note using the current Org subtree as input.
+Remove the subtree from its current file and move its contents into a
+new Denote file (a subtree is a heading with all of its contents,
+including subheadings).
+
+Take the text of the subtree's top level heading and use it as the title
+of the new note.
+
+If the heading has any tags, use them as the keywords of the new note.
+If the Org file has any #+filetags use them as well (Org's filetags are
+inherited by the headings).  If none of these are true and the user
+option `denote-prompts' includes an entry for keywords, then prompt for
+keywords.  Else do not include any keywords.
+
+If the heading has a PROPERTIES drawer, retain it for further review.
+
+If the heading's PROPERTIES drawer includes a DATE or CREATED property,
+or there exists a CLOSED statement with a timestamp value, use that to
+derive the date (or date and time) of the new note (if there is only a
+date, the time is taken as 00:00).  If more than one of these is
+present, the order of preference is DATE, then CREATED, then CLOSED.  If
+none of these is present, use the current time.  If the `denote-prompts'
+includes an entry for a date, then prompt for a date at this stage (also
+see `denote-date-prompt-use-org-read-date').
+
+For the rest, consult the value of the user option `denote-prompts' in
+the following scenaria:
+
+- Optionally prompt for a subdirectory, otherwise produce the new note
+  in the variable `denote-directory'.
+
+- Optionally prompt for a file signature, otherwise do not use one.
+
+Make the new note an Org file regardless of the value of the user option
+`denote-file-type'."
+  (interactive nil org-mode)
+  (unless (derived-mode-p 'org-mode)
+    (user-error "Headings can only be extracted from Org files"))
+  (if-let* ((text (org-get-entry))
+            (heading (denote-link-ol-get-heading)))
+      (let ((tags (org-get-tags))
+            (date (denote-org-extras--get-heading-date))
+            subdirectory
+            signature)
+        (dolist (prompt denote-prompts)
+          (pcase prompt
+            ('keywords (when (not tags) (setq tags (denote-keywords-prompt))))
+            ('subdirectory (setq subdirectory (denote-subdirectory-prompt)))
+            ('date (when (not date) (setq date (denote-date-prompt))))
+            ('signature (setq signature (denote-signature-prompt)))))
+        (delete-region (org-entry-beginning-position)
+                       (save-excursion (org-end-of-subtree t) (point)))
+        (denote heading tags 'org subdirectory date text signature))
+    (user-error "No subtree to extract; aborting")))
+
+;;;; Convert links from `:denote' to `:file' and vice versa
+
+(defun denote-org-extras--get-link-type-regexp (type)
+  "Return regexp for Org link TYPE.
+TYPE is a symbol of either `file' or `denote'.
+
+The regexp consists of four groups.  Group 1 is the link type, 2
+is the target, 3 is the target's search terms, and 4 is the
+description."
+  (let ((group-1))
+    (pcase type
+      ('denote (setq group-1 "denote"))
+      ('file (setq group-1 "file"))
+      (_ (error "`%s' is an unknown link type" type)))
+    (format 
"\\[\\[\\(?1:%s:\\)\\(?:\\(?2:.*?\\)\\(?3:::.*\\)?\\]\\|\\]\\)\\(?4:\\[\\(?:.*?\\)\\]\\)?\\]"
 group-1)))
+
+(defun denote-org-extras--get-path (id)
+  "Return file path to ID according to `org-link-file-path-type'."
+  (if (or (eq org-link-file-path-type 'adaptive)
+          (eq org-link-file-path-type 'relative))
+      (denote-get-relative-path-by-id id)
+    (denote-get-path-by-id id)))
+
+;;;###autoload
+(defun denote-org-extras-convert-links-to-file-type ()
+  "Convert denote: links to file: links in the current Org buffer.
+Ignore all other link types.  Also ignore links that do not
+resolve to a file in the variable `denote-directory'."
+  (interactive nil org-mode)
+  (if (derived-mode-p 'org-mode)
+      (save-excursion
+        (let ((count 0))
+          (goto-char (point-min))
+          (while (re-search-forward (denote-org-extras--get-link-type-regexp 
'denote) nil :no-error)
+            (let* ((id (match-string-no-properties 2))
+                   (search (or (match-string-no-properties 3) ""))
+                   (desc (or (match-string-no-properties 4) ""))
+                   (file (save-match-data (denote-org-extras--get-path id))))
+              (when file
+                (let ((new-text (if desc
+                                    (format "[[file:%s%s]%s]" file search desc)
+                                  (format "[[file:%s%s]]" file search))))
+                  (replace-match new-text :fixed-case :literal)
+                  (setq count (1+ count))))))
+          (message "Converted %d `denote:' links to `file:' links" count)))
+    (user-error "The current file is not using Org mode")))
+
+;;;###autoload
+(defun denote-org-extras-convert-links-to-denote-type ()
+  "Convert file: links to denote: links in the current Org buffer.
+Ignore all other link types.  Also ignore file: links that do not
+point to a file with a Denote file name."
+  (interactive nil org-mode)
+  (if (derived-mode-p 'org-mode)
+      (save-excursion
+        (let ((count 0))
+          (goto-char (point-min))
+          (while (re-search-forward (denote-org-extras--get-link-type-regexp 
'file) nil :no-error)
+            (let* ((file (match-string-no-properties 2))
+                   (search (or (match-string-no-properties 3) ""))
+                   (desc (or (match-string-no-properties 4) ""))
+                   (id (save-match-data (denote-retrieve-filename-identifier 
file))))
+              (when id
+                (let ((new-text (if desc
+                                    (format "[[denote:%s%s]%s]" id search desc)
+                                  (format "[[denote:%s%s]]" id search))))
+                  (replace-match new-text :fixed-case :literal)
+                  (setq count (1+ count))))))
+          (message "Converted %d `file:' links to `denote:' links" count)))
+    (user-error "The current file is not using Org mode")))
+
+;;;; Org dynamic blocks
+
+;; NOTE 2024-01-22 12:26:13 +0200: The following is copied from the
+;; now-deleted denote-org-dblock.el.  Its original author was Elias
+;; Storms <elias.sto...@gmail.com>, with substantial contributions and
+;; further developments by me (Protesilaos).
+
+;; This section defines Org dynamic blocks using the facility described
+;; in the Org manual.  Evaluate this:
+;;
+;;    (info "(org) Dynamic Blocks")
+;;
+;; The dynamic blocks defined herein are documented at length in the
+;; Denote manual.  See the following node and its subsections:
+;;
+;;    (info "(denote) Use Org dynamic blocks")
+
+;;;;; Common helper functions
+
+(defun denote-org-extras-dblock--files (files-matching-regexp &optional 
sort-by-component reverse exclude-regexp)
+  "Return list of FILES-MATCHING-REGEXP in variable `denote-directory'.
+SORT-BY-COMPONENT, REVERSE, EXCLUDE-REGEXP have the same meaning as
+`denote-sort-get-directory-files'.  If both are nil, do not try to
+perform any sorting.
+
+Also see `denote-org-extras-dblock--files-missing-only'."
+  (cond
+   ((and sort-by-component reverse)
+    (denote-sort-get-directory-files files-matching-regexp sort-by-component 
reverse :omit-current exclude-regexp))
+   (sort-by-component
+    (denote-sort-get-directory-files files-matching-regexp sort-by-component 
nil :omit-current exclude-regexp))
+   (reverse
+    (denote-sort-get-directory-files files-matching-regexp 
:no-component-specified reverse :omit-current exclude-regexp))
+   (t
+    (denote-directory-files files-matching-regexp :omit-current nil 
exclude-regexp))))
+
+(defun denote-org-extras-dblock--get-missing-links (regexp)
+  "Return list of missing links to all notes matching REGEXP.
+Missing links are those for which REGEXP does not have a match in
+the current buffer."
+  (let ((found-files (denote-directory-files regexp :omit-current))
+        (linked-files (denote-link--expand-identifiers 
denote-org-link-in-context-regexp)))
+    (if-let* ((final-files (seq-difference found-files linked-files)))
+        final-files
+      (message "All links matching `%s' are present" regexp)
+      '())))
+
+(defun denote-org-extras-dblock--files-missing-only (files-matching-regexp 
&optional sort-by-component reverse)
+  "Return list of missing links to FILES-MATCHING-REGEXP.
+SORT-BY-COMPONENT and REVERSE have the same meaning as
+`denote-sort-files'.  If both are nil, do not try to perform any
+sorting.
+
+Also see `denote-org-extras-dblock--files'."
+  (denote-sort-files
+   (denote-org-extras-dblock--get-missing-links files-matching-regexp)
+   sort-by-component
+   reverse))
+
+;;;;; Dynamic block to insert links
+
+;;;###autoload
+(defun denote-org-extras-dblock-insert-links (regexp)
+  "Create Org dynamic block to insert Denote links matching REGEXP."
+  (interactive
+   (list
+    (denote-files-matching-regexp-prompt))
+   org-mode)
+  (org-create-dblock (list :name "denote-links"
+                           :regexp regexp
+                           :not-regexp nil
+                           :excluded-dirs-regexp nil
+                           :sort-by-component nil
+                           :reverse-sort nil
+                           :id-only nil
+                           :include-date nil))
+  (org-update-dblock))
+
+;; NOTE 2024-03-30: This is how the autoload is done in org.el.
+;;;###autoload
+(eval-after-load 'org
+  '(progn
+     (org-dynamic-block-define "denote-links" 
'denote-org-extras-dblock-insert-links)))
+
+;; TODO 2024-12-04: Maybe we can do this for anything that deals with
+;; regular expressions that users provide?  I prefer not to do the
+;; work if nobody wants it, though I am mentioning this here just in
+;; case.
+(defun denote-org-extras--parse-rx (regexp)
+  "Parse REGEXP as an `rx' argument or string and return string."
+  (cond
+   ((null regexp)
+    nil)
+   ((listp regexp)
+    (rx-to-string regexp))
+   ((stringp regexp)
+    regexp)
+   (t
+    (error "Regexp `%s' is neither a list nor a string" regexp))))
+
+;;;###autoload
+(defun org-dblock-write:denote-links (params)
+  "Function to update `denote-links' Org Dynamic blocks.
+Used by `org-dblock-update' with PARAMS provided by the dynamic block."
+  (let* ((rx (denote-org-extras--parse-rx (plist-get params :regexp)))
+         (not-rx (denote-org-extras--parse-rx (plist-get params :not-regexp)))
+         (sort (plist-get params :sort-by-component))
+         (reverse (plist-get params :reverse-sort))
+         (include-date (plist-get params :include-date))
+         (block-name (plist-get params :block-name))
+         (denote-excluded-directories-regexp (or (plist-get params 
:excluded-dirs-regexp)
+                                                 
denote-excluded-directories-regexp))
+         (files (denote-org-extras-dblock--files rx sort reverse not-rx)))
+    (when block-name (insert "#+name: " block-name "\n"))
+    (denote-link--insert-links files 'org (plist-get params :id-only) 
:no-other-sorting include-date)
+    (join-line))) ; remove trailing empty line
+
+;;;;; Dynamic block to insert missing links
+
+;; TODO 2024-12-03: Do we need the :not-regexp here?  I think yes,
+;; though I prefer to have a user of this kind of dblock send me their
+;; feedback.
+
+;;;###autoload
+(defun denote-org-extras-dblock-insert-missing-links (regexp)
+  "Create Org dynamic block to insert Denote links matching REGEXP."
+  (interactive
+   (list
+    (denote-files-matching-regexp-prompt))
+   org-mode)
+  (org-create-dblock (list :name "denote-missing-links"
+                           :regexp regexp
+                           :excluded-dirs-regexp nil
+                           :sort-by-component nil
+                           :reverse-sort nil
+                           :id-only nil
+                           :include-date nil))
+  (org-update-dblock))
+
+;; NOTE 2024-03-30: This is how the autoload is done in org.el.
+;;;###autoload
+(eval-after-load 'org
+  '(progn
+     (org-dynamic-block-define "denote-missing-links" 
'denote-org-extras-dblock-insert-missing-links)))
+
+;;;###autoload
+(defun org-dblock-write:denote-missing-links (params)
+  "Function to update `denote-links' Org Dynamic blocks.
+Used by `org-dblock-update' with PARAMS provided by the dynamic block."
+  (let* ((rx (denote-org-extras--parse-rx (plist-get params :regexp)))
+         (sort (plist-get params :sort-by-component))
+         (reverse (plist-get params :reverse-sort))
+         (include-date (plist-get params :include-date))
+         (block-name (plist-get params :block-name))
+         (denote-excluded-directories-regexp (or (plist-get params 
:excluded-dirs-regexp)
+                                                 
denote-excluded-directories-regexp))
+         (files (denote-org-extras-dblock--files-missing-only rx sort 
reverse)))
+    (when block-name (insert "#+name: " block-name "\n"))
+    (denote-link--insert-links files 'org (plist-get params :id-only) 
:no-other-sorting include-date)
+    (join-line))) ; remove trailing empty line
+
+;;;;; Dynamic block to insert backlinks
+
+(defun denote-org-extras-dblock--maybe-sort-backlinks (files sort-by-component 
reverse)
+  "Sort backlink FILES if SORT-BY-COMPONENT and/or REVERSE is non-nil."
+  (cond
+   ((and sort-by-component reverse)
+    (denote-sort-files files sort-by-component reverse))
+   (sort-by-component
+    (denote-sort-files files sort-by-component))
+   (reverse
+    (denote-sort-files files :no-component-specified reverse))
+   (t
+    files)))
+
+;; TODO 2024-12-03: Do we need the :not-regexp here?  I think yes,
+;; though I prefer to have a user of this kind of dblock send me their
+;; feedback.
+
+;;;###autoload
+(defun denote-org-extras-dblock-insert-backlinks ()
+  "Create Org dynamic block to insert Denote backlinks to current file."
+  (interactive nil org-mode)
+  (org-create-dblock (list :name "denote-backlinks"
+                           :excluded-dirs-regexp nil
+                           :sort-by-component nil
+                           :reverse-sort nil
+                           :id-only nil
+                           :this-heading-only nil
+                           :include-date nil))
+  (org-update-dblock))
+
+;; NOTE 2024-03-30: This is how the autoload is done in org.el.
+;;;###autoload
+(eval-after-load 'org
+  '(progn
+     (org-dynamic-block-define "denote-backlinks" 
'denote-org-extras-dblock-insert-backlinks)))
+
+;;;###autoload
+(defun org-dblock-write:denote-backlinks (params)
+  "Function to update `denote-backlinks' Org Dynamic blocks.
+Used by `org-dblock-update' with PARAMS provided by the dynamic block."
+  (when-let* ((files (if (plist-get params :this-heading-only)
+                         (denote-org-extras--get-backlinks-for-heading 
(denote-org-extras--get-file-id-and-heading-id-or-context))
+                       (denote-link-return-backlinks))))
+    (let* ((sort (plist-get params :sort-by-component))
+           (reverse (plist-get params :reverse-sort))
+           (include-date (plist-get params :include-date))
+           (denote-excluded-directories-regexp (or (plist-get params 
:excluded-dirs-regexp)
+                                                   
denote-excluded-directories-regexp))
+           (files (denote-org-extras-dblock--maybe-sort-backlinks files sort 
reverse)))
+      (denote-link--insert-links files 'org (plist-get params :id-only) 
:no-other-sorting include-date)
+      (join-line)))) ; remove trailing empty line
+
+;;;;; Dynamic block to insert entire file contents
+
+(defun denote-org-extras-dblock--get-file-contents (file &optional 
no-front-matter add-links)
+  "Insert the contents of FILE.
+With optional NO-FRONT-MATTER as non-nil, try to remove the front
+matter from the top of the file.  If NO-FRONT-MATTER is a number,
+remove that many lines starting from the top.  If it is any other
+non-nil value, delete from the top until the first blank line.
+
+With optional ADD-LINKS as non-nil, first insert a link to the
+file and then insert its contents.  In this case, format the
+contents as a typographic list.  If ADD-LINKS is `id-only', then
+insert links as `denote-link' does when supplied with an ID-ONLY
+argument."
+  (when (denote-file-is-note-p file)
+    (with-temp-buffer
+      (when add-links
+        (insert
+         (format "- %s\n\n"
+                 (denote-format-link
+                  file
+                  (denote-get-link-description file)
+                  'org
+                  (eq add-links 'id-only)))))
+      (let ((beginning-of-contents (point)))
+        (insert-file-contents file)
+        (when no-front-matter
+          (delete-region
+           (if (natnump no-front-matter)
+               (progn (forward-line no-front-matter) (line-beginning-position))
+             (1+ (re-search-forward "^$" nil :no-error 1)))
+           beginning-of-contents))
+        (when add-links
+          (indent-region beginning-of-contents (point-max) 2)))
+      (buffer-string))))
+
+(defvar denote-org-extras-dblock-file-contents-separator
+  (concat "\n\n" (make-string 50 ?-) "\n\n\n")
+  "Fallback separator used by `denote-org-extras-dblock-add-files'.")
+
+(defun denote-org-extras-dblock--separator (separator)
+  "Return appropriate value of SEPARATOR for 
`denote-org-extras-dblock-add-files'."
+  (cond
+   ((null separator) "")
+   ((stringp separator) separator)
+   (t denote-org-extras-dblock-file-contents-separator)))
+
+(defun denote-org-extras-dblock-add-files (regexp &optional separator 
no-front-matter add-links sort-by-component reverse excluded-dirs-regexp 
exclude-regexp)
+  "Insert files matching REGEXP.
+
+Seaprate them with the optional SEPARATOR.  If SEPARATOR is nil,
+use the `denote-org-extras-dblock-file-contents-separator'.
+
+If optional NO-FRONT-MATTER is non-nil try to remove the front
+matter from the top of the file.  Do it by finding the first
+blank line, starting from the top of the buffer.
+
+If optional ADD-LINKS is non-nil, first insert a link to the file
+and then insert its contents.  In this case, format the contents
+as a typographic list.
+
+If optional SORT-BY-COMPONENT is a symbol among `denote-sort-components',
+sort files matching REGEXP by the corresponding Denote file name
+component.  If the symbol is not among `denote-sort-components',
+fall back to the default identifier-based sorting.
+
+If optional REVERSE is non-nil reverse the sort order.
+
+Optional EXCLUDED-DIRS-REGEXP is the `let' bound value of
+`denote-excluded-directories-regexp'.  When nil, the original value of
+that user option is used.
+
+Optional EXCLUDE-REGEXP is a more general way to exclude files whose
+name matches the given regular expression."
+  (let* ((denote-excluded-directories-regexp (or excluded-dirs-regexp 
denote-excluded-directories-regexp))
+         (files (denote-org-extras-dblock--files regexp sort-by-component 
reverse exclude-regexp))
+         (files-contents (mapcar
+                          (lambda (file) 
(denote-org-extras-dblock--get-file-contents file no-front-matter add-links))
+                          files)))
+    (insert (string-join files-contents (denote-org-extras-dblock--separator 
separator)))))
+
+;;;###autoload
+(defun denote-org-extras-dblock-insert-files (regexp sort-by-component)
+  "Create Org dynamic block to insert Denote files matching REGEXP.
+Sort the files according to SORT-BY-COMPONENT, which is a symbol
+among `denote-sort-components'."
+  (interactive
+   (list
+    (denote-files-matching-regexp-prompt)
+    (denote-sort-component-prompt))
+   org-mode)
+  (org-create-dblock (list :name "denote-files"
+                           :regexp regexp
+                           :not-regexp nil
+                           :excluded-dirs-regexp nil
+                           :sort-by-component sort-by-component
+                           :reverse-sort nil
+                           :no-front-matter nil
+                           :file-separator nil
+                           :add-links nil))
+  (org-update-dblock))
+
+;; NOTE 2024-03-30: This is how the autoload is done in org.el.
+;;;###autoload
+(eval-after-load 'org
+  '(progn
+     (org-dynamic-block-define "denote-files" 
'denote-org-extras-dblock-insert-files)))
+
+;;;###autoload
+(defun org-dblock-write:denote-files (params)
+  "Function to update `denote-files' Org Dynamic blocks.
+Used by `org-dblock-update' with PARAMS provided by the dynamic block."
+  (let* ((rx (denote-org-extras--parse-rx (plist-get params :regexp)))
+         (not-rx (denote-org-extras--parse-rx (plist-get params :not-regexp)))
+         (sort (plist-get params :sort-by-component))
+         (reverse (plist-get params :reverse-sort))
+         (block-name (plist-get params :block-name))
+         (separator (plist-get params :file-separator))
+         (no-f-m (plist-get params :no-front-matter))
+         (add-links (plist-get params :add-links))
+         (excluded-dirs (plist-get params :excluded-dirs-regexp)))
+    (when block-name (insert "#+name: " block-name "\n"))
+    (when rx (denote-org-extras-dblock-add-files rx separator no-f-m add-links 
sort reverse excluded-dirs not-rx)))
+  (join-line)) ; remove trailing empty line
+
+;;;; Insert files as headings
+
+(defun denote-org-extras-dblock--extract-regexp (regexp)
+  "Extract REGEXP from the buffer and trim it of surrounding spaces."
+  (string-trim
+   (save-excursion
+     (re-search-forward regexp nil :no-error)
+     (buffer-substring-no-properties (match-end 0) (line-end-position)))))
+
+(defun denote-org-extras-dblock--get-file-contents-as-heading (file add-links)
+  "Insert the contents of Org FILE, formatting the #+title as a heading.
+With optional ADD-LINKS, make the title link to the original file."
+  (when-let* (((denote-file-is-note-p file))
+              (identifier (denote-retrieve-filename-identifier file))
+              (file-type (denote-filetype-heuristics file))
+              ((eq file-type 'org)))
+    (with-temp-buffer
+      (let ((beginning-of-contents (point))
+            title
+            tags)
+        (insert-file-contents file)
+        (setq title (denote-org-extras-dblock--extract-regexp 
(denote--title-key-regexp file-type)))
+        (setq tags (denote-org-extras-dblock--extract-regexp 
(denote--keywords-key-regexp file-type)))
+        (delete-region (1+ (re-search-forward "^$" nil :no-error 1)) 
beginning-of-contents)
+        (goto-char beginning-of-contents)
+        (when (and title tags)
+          (if add-links
+              (insert (format "* [[denote:%s][%s]] %s\n\n" identifier title 
tags))
+            (insert (format "* %s %s\n\n" title tags)))
+          (org-align-tags :all))
+        (while (re-search-forward "^\\(*+?\\) " nil :no-error)
+          (replace-match (format "*%s " "\\1"))))
+      (buffer-string))))
+
+(defun denote-org-extras-dblock-add-files-as-headings (regexp &optional 
add-links sort-by-component reverse excluded-dirs-regexp exclude-regexp)
+  "Insert files matching REGEXP.
+
+If optional ADD-LINKS is non-nil, first insert a link to the file
+and then insert its contents.  In this case, format the contents
+as a typographic list.
+
+If optional SORT-BY-COMPONENT is a symbol among `denote-sort-components',
+sort files matching REGEXP by the corresponding Denote file name
+component.  If the symbol is not among `denote-sort-components',
+fall back to the default identifier-based sorting.
+
+If optional REVERSE is non-nil reverse the sort order.
+
+Optional EXCLUDED-DIRS-REGEXP is the `let' bound value of
+`denote-excluded-directories-regexp'.  When nil, the original value of
+that user option is used.
+
+Optional EXCLUDE-REGEXP is a more general way to exclude files whose
+name matches the given regular expression."
+  (let* ((denote-excluded-directories-regexp (or excluded-dirs-regexp 
denote-excluded-directories-regexp))
+         (files (denote-org-extras-dblock--files regexp sort-by-component 
reverse exclude-regexp))
+         (files-contents (mapcar
+                          (lambda (file)
+                            
(denote-org-extras-dblock--get-file-contents-as-heading file add-links))
+                          files)))
+    (insert (string-join files-contents))))
+
+;;;###autoload
+(defun denote-org-extras-dblock-insert-files-as-headings (regexp 
sort-by-component)
+  "Create Org dynamic block to insert Denote Org files matching REGEXP.
+
+Turn the #+title of each file into a top-level heading.  Then increment
+all original headings in the file by one, so that they become
+subheadings of what once was the #+title.
+
+Use the #+filetags of each file as tags for the top-level heading (what
+was the #+title).
+
+Sort the files according to SORT-BY-COMPONENT, which is a symbol
+among `denote-sort-components'.
+
+IMPORTANT NOTE: This dynamic block only works with Org files, because it
+has to assume the Org notation in order to insert each file's contents
+as its own heading."
+  (interactive
+   (list
+    (denote-files-matching-regexp-prompt)
+    (denote-sort-component-prompt))
+   org-mode)
+  (org-create-dblock (list :name "denote-files-as-headings"
+                           :regexp regexp
+                           :not-regexp nil
+                           :excluded-dirs-regexp nil
+                           :sort-by-component sort-by-component
+                           :reverse-sort nil
+                           :add-links nil))
+  (org-update-dblock))
+
+;; NOTE 2024-03-30: This is how the autoload is done in org.el.
+;;;###autoload
+(eval-after-load 'org
+  '(progn
+     (org-dynamic-block-define "denote-files-as-headings" 
'denote-org-extras-dblock-insert-files-as-headings)))
+
+;;;###autoload
+(defun org-dblock-write:denote-files-as-headings (params)
+  "Function to update `denote-files' Org Dynamic blocks.
+Used by `org-dblock-update' with PARAMS provided by the dynamic block."
+  (let* ((rx (denote-org-extras--parse-rx (plist-get params :regexp)))
+         (not-rx (denote-org-extras--parse-rx (plist-get params :not-regexp)))
+         (sort (plist-get params :sort-by-component))
+         (reverse (plist-get params :reverse-sort))
+         (block-name (plist-get params :block-name))
+         (add-links (plist-get params :add-links))
+         (excluded-dirs (plist-get params :excluded-dirs-regexp)))
+    (when block-name (insert "#+name: " block-name "\n"))
+    (when rx (denote-org-extras-dblock-add-files-as-headings rx add-links sort 
reverse excluded-dirs not-rx)))
+  (join-line)) ; remove trailing empty line
+
+(provide 'denote-org-extras)
+;;; denote-org-extras.el ends here


Reply via email to