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