From c54c2066943ce53ff60457c7e79d530685ef2f0a Mon Sep 17 00:00:00 2001 From: Jethro Kuan Date: Sun, 23 Feb 2020 17:11:49 +0800 Subject: [PATCH] (feature) add templating functionality via `org-roam-template` (#165) This allows users to customize the filename, and the content of the template. --- doc/configuration.md | 35 +++++++++++++-- org-roam.el | 105 +++++++++++++++++++------------------------ 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 1a4f6d9..ee59186 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -69,10 +69,37 @@ Org files in all of its main commands (`org-roam-insert`, `org-roam-find-file`). Hence, having any unique file name is a decent option, and the default workflow uses the timestamp as the filename. -The format of the filename is controlled by the function -`org-roam-file-name-function`, which defaults to a format like -`YYYYMMDDHHMMSS_title_here.org`. You may choose to define your own -function to change this. +Org-roam provides templating functionality via `org-roam-templates`. +`org-roam-templates` maps a template string key to a template. Each +template consists of two parts: (1) a function that takes the title, +and generates a filename. (2) the template content. The templated +content accepts two special fields: `${title}` and `${slug}`, which +are substituted with the title and slug respectively. Org-roam ships +with the default template, which inserts the title of the note. + +Here's an example of customizing templates: + +```emacs-lisp +(defun jethro/org-roam-title-private (title) + (let ((timestamp (format-time-string "%Y%m%d%H%M%S" (current-time))) + (slug (org-roam--title-to-slug title))) + (format "private-%s_%s" timestamp slug))) + +(setq org-roam-templates + (list (list "default" (list :file #'org-roam--file-name-timestamp-title + :content "#+SETUPFILE:./hugo_setup.org +#+HUGO_SECTION: zettels +#+HUGO_SLUG: ${slug} +#+TITLE: ${title}")) + (list "private" (list :file #'jethro/org-roam-title-private + :content "#+TITLE: ${title}")))) +``` + +Here, I define a file-name function `jethro/org-roam-title-private`, +which forms titles like `private-20200202000000-note_name`. The +content string is simply the title. For the default template, I have +extended it to include more boilerplate content for publishing +purposes. If you wish to be prompted to change the file name on creation, set `org-roam-filename-noconfirm` to `nil`: diff --git a/org-roam.el b/org-roam.el index 8b420e7..62e326c 100644 --- a/org-roam.el +++ b/org-roam.el @@ -100,10 +100,6 @@ If nil, always ask for filename." :type 'boolean :group 'org-roam) -(defcustom org-roam-autopopulate-title t "Whether to autopopulate the title." - :type 'boolean - :group 'org-roam) - (defcustom org-roam-buffer-width 0.33 "Width of `org-roam' buffer." :type 'number :group 'org-roam) @@ -210,7 +206,7 @@ If called interactively, then PARENTS is non-nil." "Return all org-roam files." (org-roam--find-files (file-truename org-roam-directory))) -(defun org-roam--make-new-file-path (id &optional absolute) +(defun org-roam--new-file-path (id &optional absolute) "Make new file path from identifier `ID'. If `ABSOLUTE', return an absolute file-path. Else, return a relative file-path." @@ -253,42 +249,35 @@ It uses TITLE and the current timestamp to form a unique title." (format "%s_%s" timestamp slug))) ;;; Creating org-roam files -(defun org-roam--populate-title (file &optional title) - "Populate title line for FILE using TITLE, if provided. -If not provided, derive the title from the file name." - (let ((title (or title - (-> file - (file-name-nondirectory) - (file-name-sans-extension) - (split-string "_") - (string-join " ") - (s-titleize))))) - (write-region - (concat - "#+TITLE: " - title - "\n\n") - nil file nil))) +(defvar org-roam-templates + (list (list "default" (list :file #'org-roam--file-name-timestamp-title + :content "#+TITLE: ${title}"))) + "Templates to insert for new files in org-roam.") -(defun org-roam--make-file (file-path &optional title) - "Create an org-roam file at FILE-PATH, optionally setting the TITLE attribute." - (if (file-exists-p file-path) - (error (format "Aborting, file already exists at %s" file-path)) - (make-empty-file file-path t) - (if org-roam-autopopulate-title - (org-roam--populate-title file-path title)) - (save-excursion - (with-current-buffer (find-file-noselect file-path) - (org-roam--update-cache))))) - -(defun org-roam--new-file-named (slug) - "Create a new file named `SLUG'. -`SLUG' is the short file name, without a path or a file extension." - (interactive "sNew filename (without extension): ") - (let ((file-path (org-roam--make-new-file-path slug t))) - (unless (file-exists-p file-path) - (org-roam--make-file file-path)) - (find-file file-path))) +(defun org-roam--make-new-file (title &optional template-key) + (unless org-roam-templates + (user-error "No templates defined")) + (let (template) + (if template-key + (setq template (cadr (assoc template-key org-roam-templates))) + (if (= (length org-roam-templates) 1) + (setq template (car org-roam-templates)) + (setq template + (cadr (assoc (completing-read "Template: " org-roam-templates) + org-roam-templates))))) + (let (file-name-fn file-path) + (fset 'file-name-fn (plist-get template :file)) + (setq file-path (org-roam--new-file-path (file-name-fn title) t)) + (if (file-exists-p file-path) + file-path + (make-empty-file file-path t) + (write-region + (s-format (plist-get template :content) + 'aget + (list (cons "title" title) + (cons "slug" (org-roam--title-to-slug title)))) + nil file-path nil) + file-path)))) (defun org-roam--get-new-id (title) "Return a new ID, given the note TITLE." @@ -297,16 +286,11 @@ If not provided, derive the title from the file name." proposed-slug (read-string "Enter ID (without extension): " proposed-slug))) - (file-path (org-roam--make-new-file-path new-slug t))) + (file-path (org-roam--new-file-path new-slug t))) (if (file-exists-p file-path) (user-error "There's already a file at %s") new-slug))) -(defun org-roam-new-file () - "Quickly create a new file, using the current timestamp." - (interactive) - (org-roam--new-file-named (format-time-string "%Y%m%d%H%M%S" (current-time)))) - ;;; Inserting org-roam links (defun org-roam-insert (prefix) "Find an org-roam file, and insert a relative org link to it at point. @@ -326,14 +310,12 @@ If PREFIX, downcase the title before insertion." (title (completing-read "File: " completions nil nil region-text)) (region-or-title (or region-text title)) (absolute-file-path (or (cadr (assoc title completions)) - (org-roam--make-new-file-path (org-roam--get-new-id title) t))) + (org-roam--make-new-file title))) (current-file-path (-> (or (buffer-base-buffer) (current-buffer)) (buffer-file-name) (file-truename) (file-name-directory)))) - (unless (file-exists-p absolute-file-path) - (org-roam--make-file absolute-file-path title)) (when region ;; Remove previously selected text. (goto-char (car region)) (delete-char (- (cdr region) (car region)))) @@ -353,10 +335,7 @@ If PREFIX, downcase the title before insertion." (org-roam--find-all-files))) (title-or-slug (completing-read "File: " completions)) (absolute-file-path (or (cadr (assoc title-or-slug completions)) - (org-roam--make-new-file-path - (org-roam--get-new-id title-or-slug) t)))) - (unless (file-exists-p absolute-file-path) - (org-roam--make-file absolute-file-path title-or-slug)) + (org-roam--make-new-file title-or-slug)))) (find-file absolute-file-path))) (defun org-roam--get-roam-buffers () @@ -462,21 +441,31 @@ This is equivalent to removing the node from the graph." (org-roam--maybe-update-buffer :redisplay t))) ;;; Org-roam daily notes + +(defun org-roam--file-for-time (time) + "Create and find file for TIME." + (let* ((org-roam-templates (list (list "daily" (list :file (lambda (title) title) + :content "#+TITLE: ${title}"))))) + (org-roam--make-new-file (format-time-string "%Y-%m-%d" time) "daily"))) + (defun org-roam-today () - "Create the file for today." + "Create and find file for today." (interactive) - (org-roam--new-file-named (format-time-string "%Y-%m-%d" (current-time)))) + (let ((path (org-roam--file-for-time (current-time)))) + (find-file path))) (defun org-roam-tomorrow () - "Create the file for tomorrow." + "Create and find the file for tomorrow." (interactive) - (org-roam--new-file-named (format-time-string "%Y-%m-%d" (time-add 86400 (current-time))))) + (let ((path (org-roam--file-for-time (time-add 86400 (current-time))))) + (find-file path))) (defun org-roam-date () "Create the file for any date using the calendar." (interactive) (let ((time (org-read-date nil 'to-time nil "Date: "))) - (org-roam--new-file-named (format-time-string "%Y-%m-%d" time)))) + (let ((path (org-roam--file-for-time time))) + (find-file path)))) ;;; Org-roam buffer (define-derived-mode org-roam-backlinks-mode org-mode "Backlinks"