emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/denote 8957eb6720 001/355: Add initial version of denot


From: ELPA Syncer
Subject: [elpa] externals/denote 8957eb6720 001/355: Add initial version of denote.el
Date: Sun, 26 Jun 2022 23:57:54 -0400 (EDT)

branch: externals/denote
commit 8957eb6720f2a6d41d4aa7865fac99aca823f4ae
Author: Protesilaos Stavrou <info@protesilaos.com>
Commit: Protesilaos Stavrou <info@protesilaos.com>

    Add initial version of denote.el
---
 denote.el | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 291 insertions(+)

diff --git a/denote.el b/denote.el
new file mode 100644
index 0000000000..7d0d3249bd
--- /dev/null
+++ b/denote.el
@@ -0,0 +1,291 @@
+;;; denote.el --- Unassuming Sidenotes of Little Significance -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2022  Protesilaos Stavrou
+
+;; Author: Protesilaos Stavrou <info@protesilaos.com>
+;; URL: https://git.sr.ht/~protesilaos/denote
+;; Version: 0.1.0
+;; Package-Requires: ((emacs "27.1"))
+
+;; 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:
+;;
+;; "Denote" is the familiar word, though it also is a play on to the
+;; "note" concept.  Plus, we can come up with acronyms like:
+;;
+;; * Don't Ever Note Only The Ephemeral
+;; * Denote Everything Neatly; Omit The Excesses
+;;
+;; But I'll let you get back to work.  Don't Escape or Neglect your
+;; Obligations, Tasks, Engagements...
+
+;;; Code:
+
+(defgroup denote ()
+  "Simple tool for plain text notes."
+  :group 'files)
+
+;;; User options
+
+(defcustom denote-directory (expand-file-name "~/Documents/notes/")
+  "Directory for storing personal notes."
+  :group 'denote
+  :type 'directory)
+
+(defcustom denote-known-keywords
+  '("emacs" "philosophy" "politics" "economics")
+  "List of strings with predefined keywords for `denote-new-note'.
+
+The implicit assumption is that a keyword is a single word.  If
+you need a keyword to be multiple words long, use underscores to
+separate them.  Do not use hyphens or other characters, as those
+are assumed to demarcate distinct keywords."
+  :group 'denote
+  :type '(repeat string))
+
+(defcustom denote-infer-keywords t
+  "Whether to infer keywords.
+
+When non-nil, search the file names of existing notes in
+`denote-directory' for their keyword field and extract the
+entries as \"inferred keywords\".  These are combined with
+`denote-known-keywords' and are presented as completion
+candidated while using `denote-new-note' interactively.
+
+If nil, refrain from inferring keywords.  The aforementioned
+completion prompt only shows the `denote-known-keywords'."
+  :group 'denote
+  :type 'boolean)
+
+(defcustom denote-sort-keywords t
+  "Whether to sort keywords in new files.
+
+When non-nil, the keywords of `denote-new-note' are sorted with
+`string-lessp' regardless of the order they were inserted at the
+minibuffer prompt.
+
+If nil, show the keywords in their given order."
+  :group 'denote
+  :type 'boolean)
+
+;;; Main variables
+
+;; TODO 2022-06-04: Can we make the entire file name format a defcustom?
+
+(defconst denote-id "%Y%m%d_%H%M%S"
+  "Format of ID prefix of a note's filename.")
+
+(defconst denote-id-regexp "\\([0-9_]+\\{15\\}\\)"
+  "Regular expression to match `denote-id'.")
+
+(defconst denote-keyword-regexp "\\(--\\)\\([0-9A-Za-z_+]*\\)\\(--\\)"
+  "Regular expression to match `denote-keywords'.")
+
+;;;; File name helpers
+
+(defun denote--directory ()
+  "Valid name format for `denote-directory'."
+  (file-name-as-directory denote-directory))
+
+(defun denote--extract (regexp str &optional group)
+  "Extract REGEXP from STR, with optional regexp GROUP."
+  (when group
+    (unless (and (integerp group) (> group 0))
+      (error "`%s' is not a positive integer" group)))
+  (with-temp-buffer
+    (insert str)
+    (when (re-search-forward regexp nil t -1)
+      (match-string (or group 1)))))
+
+(defvar denote--punctuation-regexp "[][{}!@#$%^&*()_=+'\"?,.\|;:~`‘’“”]*"
+  "Regular expression of punctionation that should be removed.")
+
+(defun denote--slug-no-punct (str)
+  "Convert STR to a file name slug."
+  (replace-regexp-in-string denote--punctuation-regexp "" str))
+
+(defun denote--slug-hyphenate (str)
+  "Replace spaces with hyphens in STR.
+Also replace multiple hyphens with a single one and remove any
+trailing hyphen."
+  (replace-regexp-in-string
+   "-$" ""
+   (replace-regexp-in-string
+    "-\\{2,\\}" "-"
+    (replace-regexp-in-string "--+\\|\s+" "-" str))))
+
+(defun denote--sluggify (str)
+  "Make STR an appropriate file name slug."
+  (downcase (denote--slug-hyphenate (denote--slug-no-punct str))))
+
+;;;; Keywords
+
+(defun denote--directory-files ()
+  "List `denote-directory' files, assuming flat directory."
+  (seq-remove
+   (lambda (file)
+     ;; TODO: generalise this for more VC backends?  Which ones?
+     (or (string-match-p "\\.git" file)
+         (file-directory-p file)))
+   (directory-files (denote--directory) nil 
directory-files-no-dot-files-regexp t)))
+
+(defun denote--keywords-in-files ()
+  "Produce list of keywords in `denote--directory-files'."
+  (delq nil (mapcar
+             (lambda (x)
+               (denote--extract
+                (concat denote-id-regexp denote-keyword-regexp) x 3))
+             (denote--directory-files))))
+
+(defun denote--inferred-keywords ()
+  "Extract keywords from `denote--directory-files'."
+  (let ((sequence (denote--keywords-in-files)))
+    (mapcan (lambda (s)
+              (split-string s "+" t))
+            sequence)))
+
+(defun denote-keywords ()
+  "Combine `denote--inferred-keywords' with `denote-known-keywords'."
+  (delete-dups (append (denote--inferred-keywords) denote-known-keywords)))
+
+(defvar denote--keyword-history nil
+  "Minibuffer history of inputted keywords.")
+
+(defun denote--keywords-crm (keywords)
+  "Use `completing-read-multiple' for KEYWORDS."
+  (completing-read-multiple
+   "File keyword: " keywords
+   nil nil nil 'denote--keyword-history))
+
+(defun denote--keywords-prompt ()
+  "Prompt for one or more keywords.
+In the case of multiple entries, those are separated by the
+`crm-sepator', which typically is a comma.  In such a case, the
+output is sorted with `string-lessp'."
+  (let ((choice (denote--keywords-crm (denote-keywords))))
+    (cond
+     ((null choice)
+      "")
+     ((= (length choice) 1)
+      (car choice))
+     ((if denote-sort-keywords
+          (sort choice #'string-lessp)
+        choice)))))
+
+(defun denote--keywords-combine (keywords)
+  "Format KEYWORDS output of `denote--keywords-prompt'."
+  (if (and (> (length keywords) 1)
+           (not (stringp keywords)))
+      (mapconcat #'downcase keywords "+")
+    keywords))
+
+(defun denote--keywords-capitalize (keywords)
+  "`capitalize' KEYWORDS output of `denote--keywords-prompt'."
+  (if (and (> (length keywords) 1)
+           (not (stringp keywords)))
+      (mapconcat #'capitalize keywords ", ")
+    (capitalize keywords)))
+
+(defun denote--keywords-add-to-history (keywords)
+  "Append KEYWORDS to `denote--keyword-history'."
+  (if-let ((listed (listp keywords))
+           (length (length keywords)))
+      (cond
+       ((and listed (= length 1))
+        (car keywords))
+       ((and listed (> length 1))
+        (mapc (lambda (kw)
+                (add-to-history 'denote--keyword-history kw))
+              (delete-dups keywords))))
+    (add-to-history 'denote--keyword-history keywords)))
+
+;;;; New note
+
+(defun denote--format-file (path id keywords slug)
+  "Helper for `denote-new-note' to format file names.
+PATH, ID, KEYWORDS, SLUG are expected to be supplied by
+`denote-new-note': they will all be converted into a single
+string."
+  (let ((kws (if denote-infer-keywords
+                 (denote--keywords-combine keywords)
+               keywords)))
+    (format "%s%s--%s--%s.org" path id kws slug)))
+
+(defun denote--file-meta-header (title date keywords filename id)
+  "Front matter for new notes.
+
+TITLE, DATE, KEYWORDS, FILENAME, ID are all strings which are
+ provided by `denote-new-note'."
+  (let ((kw (denote--keywords-capitalize keywords)))
+    (concat "#+title:     " title     "\n"
+            "#+date:      " date      "\n"
+            "#+keywords:  " kw        "\n"
+            "#+orig_name: " filename  "\n"
+            "#+orig_id:   " id        "\n\n")))
+
+(defun denote--path (title keywords)
+  "Return path to new file with TITLE and KEYWORDS."
+  (denote--format-file
+   (file-name-as-directory denote-directory)
+   (format-time-string denote-id)
+   keywords
+   (denote--sluggify title)))
+
+(defun denote--prepare-note (title keywords)
+  "Use TITLE and KEYWORDS to prepare new note file."
+  (let ((filename (denote--path title keywords)))
+    (with-current-buffer (find-file filename)
+      (insert
+       (denote--file-meta-header
+        title (format-time-string "%F") keywords filename
+        (format-time-string denote-id))))))
+
+(defvar denote--title-history nil
+  "Minibuffer history of `denote--title-prompt'.")
+
+(defun denote--title-prompt ()
+  "Read file title for `denote-new-note'."
+  (read-string "File title: " nil 'denote--title-history))
+
+;;;###autoload
+(defun denote (title keywords)
+  "Create new note with the appropriate metadata and file name.
+
+This command first prompts for a file TITLE and then for one or
+more KEYWORDS (separated by the `crm-separator', typically a
+comma).  The latter supports completion though any arbitrary
+string can be inserted.
+
+Completion candidates are those of `denote-known-keywords'.  If
+`denote-infer-keywords' is non-nil, then keywords in existing
+file names are also provided as candidates.
+
+When `denote-sort-keywords' is non-nil, keywords are sorted
+alphabetically."
+  (declare (interactive-only t))
+  (interactive
+   (list
+    (denote--title-prompt)
+    (denote--keywords-prompt)))
+  (denote--prepare-note title keywords)
+  (denote--keywords-add-to-history keywords))
+
+;; TODO 2022-06-04: Integrate with org-capture.  Is it possible without
+;; a lot of extra code?  How?
+
+(provide 'denote)
+;;; denote.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]