branch: externals/denote commit 3df88a05d0dea7590811b7468537df62ee564660 Author: Protesilaos Stavrou <i...@protesilaos.com> Commit: Protesilaos Stavrou <i...@protesilaos.com>
Make linking aware of file types (extends 299b897) I think this is a cool feature, especially the ability to read the relevant file in order to use the correct link format. --- README.org | 36 ++++++++++++---- denote-link.el | 129 +++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 127 insertions(+), 38 deletions(-) diff --git a/README.org b/README.org index 661e31dd46..f308b471ca 100644 --- a/README.org +++ b/README.org @@ -225,20 +225,40 @@ Denote has a basic linking facility to quickly establish connections between notes. The command ~denote-link~ prompts for a file name in the ~denote-directory~ (only regular files are considered, not directories). It then retrieves the path of the given note, inserts it at point using -the link notation of Org, and creates a backlink entry in the target -file. +the appropriate link notation, and creates a backlink entry in the +target file (again using the appropriate notation). + +What constitutes "appropriate link notation" depends on the file type of +the given entry per ~denote-file-type~ ([[#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d][The file naming scheme]]). For +example when linking from an Org file to a Markdown file, the link in +the former will follow Org syntax while the backlink in the latter will +use that of Markdown. Org links use =[[file:TARGET][DESCRIPTION]]=, +those of Markdown are =[DESCRIPTION](file:TARGET)=, while for plain text +we implement our own scheme of =<TYPE: TARGET> [DESCRIPTION]=, where +=TYPE= is either =LINK= or =BACKLINK= (capitalization in the latter two +is literal, because plain text lacks other means of emphasis). + +Plain text links can benefit from Emacs' notion of "future history", +else its ability to read the thing at point for relevant commands. With +point over the =TARGET=, =M-x find-file= followed by =M-n= will fill the +path to that file (this also works with point over just the identifier +of a note). Backlinks are recorded at the end of a note under the heading with the -title =Denote backlinks=. Users should not edit this part manually: it -is controlled by Denote, such as to delete duplicate links (in the -future it might also handle stuff like alphabetic sorting). +title =Denote backlinks=. Users should not edit the note below this +part manually: it is controlled by Denote, such as to delete duplicate +links (in the future it might also handle stuff like alphabetic +sorting). + +The section with the backlinks is formatted according to the note's file +type. #+vindex: denote-link-insert-functions #+findex: denote-link-backlink The special hook ~denote-link-insert-functions~ is called after a link -is created. It accepts two arguments for the target file and the origin -of the current link. The function ~denote-link-backlink~ provides an -example for advanced users. +is created. It accepts two arguments for the target file and the +formatted backlink to the original file. The function +~denote-link-backlink~ provides an example for advanced users. #+findex: denote-link-clear-stale-backlinks Backlinks that no longer point to available notes can be removed from diff --git a/denote-link.el b/denote-link.el index e07d31e968..64619be262 100644 --- a/denote-link.el +++ b/denote-link.el @@ -24,7 +24,46 @@ ;;; Commentary: ;; -;; Links for denote. +;; Denote has a basic linking facility to quickly establish connections +;; between notes. The command `denote-link' prompts for a file name in the +;; `denote-directory' (only regular files are considered, not directories). +;; It then retrieves the path of the given note, inserts it at point using +;; the appropriate link notation, and creates a backlink entry in the +;; target file (again using the appropriate notation). +;; +;; What constitutes "appropriate link notation" depends on the file type +;; of the given entry per `denote-file-type' (see "The file naming +;; scheme" in the manual). For example when linking from an Org file to +;; a Markdown file, the link in the former will follow Org syntax while +;; the backlink in the latter will use that of Markdown. Org links use +;; `[[file:TARGET][DESCRIPTION]]', those of Markdown are +;; `[DESCRIPTION](file:TARGET)', while for plain text we implement our +;; own scheme of `<TYPE: TARGET> [DESCRIPTION]', where `TYPE' is either +;; `LINK' or `BACKLINK' (capitalization in the latter two is literal, +;; because plain text lacks other means of emphasis). +;; +;; Plain text links can benefit from Emacs' notion of "future history", +;; else its ability to read the thing at point for relevant commands. With +;; point over the `TARGET', `M-x find-file' followed by `M-n' will fill the +;; path to that file (this also works with point over just the identifier +;; of a note). +;; +;; Backlinks are recorded at the end of a note under the heading with the +;; title `Denote backlinks'. Users should not edit the note below this +;; part manually: it is controlled by Denote, such as to delete duplicate +;; links (in the future it might also handle stuff like alphabetic +;; sorting). +;; +;; The section with the backlinks is formatted according to the note's file +;; type. +;; +;; The special hook `denote-link-insert-functions' is called after a link +;; is created. It accepts two arguments for the target file and the +;; formatted backlink to the original file. The function +;; `denote-link-backlink' provides an example for advanced users. +;; +;; Backlinks that no longer point to available notes can be removed from +;; the current buffer with the command `denote-link-clear-stale-backlinks'. ;;; Code: @@ -39,55 +78,75 @@ (defcustom denote-link-insert-functions (list #'denote-link-backlink) "Functions that run after `denote-link'. -Each function accepts a TARGET-FILE and an ORIGIN-LINK argument. -Both are supplied by `denote-link'." +Each function accepts a TARGET file and a BACKLINK argument. +Both are supplied by `denote-link'. Advanced users are +encouraged to study `denote-link-backlink' for how those +arguments are used." :type 'hook :group 'denote-link) ;;;; Link to note -(defun denote-link--find-key-value-pair (regexp) - "Produce a cons cell from REGEXP by searching the file." +(defun denote-link--find-value (regexp) + "Return value from REGEXP by searching the file." (goto-char (point-min)) (re-search-forward regexp) - (cons (match-string-no-properties 1) - (match-string-no-properties 2))) + (match-string-no-properties 3)) -(defconst denote-link--title-regexp "^\\(#\\+title:\\)[\s\t]+\\(.*\\)" +(defconst denote-link--title-regexp "^\\(#\\+\\)?\\(title:\\)[\s\t]+\\(.*\\)" "Regular expression for title key and value.") -(defconst denote-link--identifier-regexp "^\\(#\\+identifier:\\)[\s\t]+\\(.*\\)" +(defconst denote-link--identifier-regexp "^\\(#\\+\\)?\\(identifier:\\)[\s\t]+\\(.*\\)" "Regular expression for filename key and value.") -(defconst denote-link--link-format "[[file:%s][%s (%s)]]" +(defconst denote-link--link-format-org "[[file:%s][%s (%s)]]" "Format of Org link to note.") -(defconst denote-link--backlink-format "[[file:%s][backlink: %s (%s)]]" +(defconst denote-link--backlink-format-org "[[file:%s][backlink: %s (%s)]]" "Format of Org backlink to note.") +(defconst denote-link--link-format-md "[%2$s (%3$s)](file:%1$s)" + "Format of Markdown link to note.") + +(defconst denote-link--backlink-format-md "[backlink: %2$s (%3$s)](file:%1$s)" + "Format of Markdown backlink to note.") + +(defconst denote-link--link-format-txt "<LINK: %s> [NAME %s (%s)]" + "Format of plain text link to note.") + +(defconst denote-link--backlink-format-txt "BACKLINK: <%s> [NAME %s (%s)]" + "Format of plain text backlink to note.") + (defconst denote-link--backlink-regexp "\\[\\[file:\\(.*?\\)\\]\\[backlink: \\(.*?\\) (\\(.*?\\))\\]\\]" - "Regexp of `denote-link--backlink-format'.") + "Regexp of `denote-link--backlink-format-org'.") (defun denote-link--retrieve-value (note regexp) "Return REGEXP value from NOTE." (let ((default-directory (denote-directory))) (with-temp-buffer (insert-file-contents-literally note) - (denote-link--find-key-value-pair regexp)))) + (denote-link--find-value regexp)))) (defun denote-link--read-file-prompt () "Prompt for regular file in variable `denote-directory'." (read-file-name "Select note: " (denote-directory) - nil t nil #'file-regular-p)) ; Includes backup files. Maybe we can remove them? - -(defun denote-link--format-link (file &optional backlink) - "Format link to FILE. + nil t nil #'file-regular-p)) + +(defun denote-link--file-type-format (file &optional backlink) + "Return link pattern based on FILE format. +With optional BACKLINK, return a backlink pattern" + (pcase (file-name-extension file) + ("md" (if backlink denote-link--backlink-format-md denote-link--link-format-md)) + ("txt" (if backlink denote-link--backlink-format-txt denote-link--link-format-txt)) + (_ (if backlink denote-link--backlink-format-org denote-link--link-format-org)))) ; Includes backup files. Maybe we can remove them? + +(defun denote-link--format-link (file pattern) + "Prepare link to FILE using PATTERN. With optional BACKLINK, format it as a backlink." (let* ((dir (denote-directory)) - (file-id (cdr (denote-link--retrieve-value file denote-link--identifier-regexp))) + (file-id (denote-link--retrieve-value file denote-link--identifier-regexp)) (file-path (file-name-completion file-id dir)) - (file-title (cdr (denote-link--retrieve-value file denote-link--title-regexp))) - (pattern (if backlink denote-link--backlink-format denote-link--link-format))) + (file-title (denote-link--retrieve-value file denote-link--title-regexp))) (format pattern file-path file-title file-id))) ;;;###autoload @@ -95,28 +154,38 @@ With optional BACKLINK, format it as a backlink." "Create Org link to TARGET note in variable `denote-directory'. Run `denote-link-insert-functions' afterwards." (interactive (list (denote-link--read-file-prompt))) - (let* ((target-link (denote-link--format-link target)) - (origin-link (denote-link--format-link (buffer-file-name) :backlink))) - (insert target-link) - (run-hook-with-args 'denote-link-insert-functions target origin-link))) + (let* ((origin (buffer-file-name)) + (link (denote-link--format-link target (denote-link--file-type-format origin))) + (backlink (denote-link--format-link origin (denote-link--file-type-format target :backlink)))) + (insert link) + (run-hook-with-args 'denote-link-insert-functions target backlink))) (defconst denote-link-backlink-heading "Denote backlinks" "String of the backlink's heading. This heading is appended to a file when another links to it.") -(defun denote-link-backlink (target-file origin-link) - "Insert ORIGIN-LINK to TARGET-FILE." +(defun denote-link--format-backlinks-heading (heading) + "Format HEADING for backlinks." + (let* ((ext (file-name-extension (buffer-file-name))) + (markup (if (string= ext "org") "*" "#")) + (warning "Do not edit past this line; this is for denote.el and related.") + (comment (if (string= ext "org") + (format "# %s" warning) + (format "<!-- %s -->" warning)))) + (format "%s\n%s %s\n\n" comment markup heading))) + +(defun denote-link-backlink (target backlink) + "Insert BACKLINK to TARGET file." (let ((default-directory (denote-directory)) (heading denote-link-backlink-heading) heading-point) - (with-current-buffer (find-file-noselect target-file) + (with-current-buffer (find-file-noselect target) (goto-char (point-max)) (unless (save-excursion (setq heading-point (re-search-backward heading nil t))) (unless (denote--line-regexp-p 'empty 0) (newline)) - (insert - (format "* %s\n%s\n\n" heading "# Do not edit; this is for denote.el and related"))) - (insert (format "- %s\n" origin-link)) + (insert (denote-link--format-backlinks-heading heading))) + (insert (format "- %s\n" backlink)) ;; delete duplicate links (when heading-point (delete-duplicate-lines heading-point (point-max) nil nil t)))))