diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b2b2ab..5e16a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.1.3 (TBD) +### New Features +* [#182][gh-182] Support file name aliases via `#+ROAM_ALIAS`. + ### Features * [#165][gh-165] Add templating functionality via `org-roam-templates`. @@ -93,6 +96,7 @@ Mostly a documentation/cleanup release. [gh-142]: https://github.com/jethrokuan/org-roam/pull/142 [gh-143]: https://github.com/jethrokuan/org-roam/pull/143 [gh-165]: https://github.com/jethrokuan/org-roam/pull/165 +[gh-182]: https://github.com/jethrokuan/org-roam/pull/182 # Local Variables: # eval: (auto-fill-mode -1) diff --git a/org-roam-utils.el b/org-roam-utils.el index c34b886..d7458d9 100644 --- a/org-roam-utils.el +++ b/org-roam-utils.el @@ -35,6 +35,7 @@ (require 'org) (require 'org-element) +(require 'ob-core) ;for org-babel-parse-header-arguments (require 'subr-x) (require 'cl-lib) @@ -127,15 +128,56 @@ ITEM is of the form: (:from from-path :to to-path :properties (:content preview- (puthash p-from (list props) contents-hash) (puthash p-to contents-hash backward)))))) -(defun org-roam--extract-title () - "Extract the title from `BUFFER'." - (org-element-map - (org-element-parse-buffer) - 'keyword - (lambda (kw) - (when (string= (org-element-property :key kw) "TITLE") - (org-element-property :value kw))) - :first-match t)) +(defun org-roam--extract-global-props (props) + "Extract PROPS from the current buffer." + (let ((buf (org-element-parse-buffer)) + (res '())) + (dolist (prop props) + (let ((p (org-element-map + buf + 'keyword + (lambda (kw) + (when (string= (org-element-property :key kw) prop) + (org-element-property :value kw))) + :first-match t))) + (setq res (cons (cons prop p) res)))) + res)) + +(defun org-roam--aliases-str-to-list (str) + "Function to transform string STR into list of alias titles. + +This snippet is obtained from ox-hugo: +https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131" + (when (stringp str) + (let* ((str (org-trim str)) + (str-list (split-string str "\n")) + ret) + (dolist (str-elem str-list) + (let* ((format-str ":dummy '(%s)") ;The :dummy key is discarded in the `lst' var below. + (alist (org-babel-parse-header-arguments (format format-str str-elem))) + (lst (cdr (car alist))) + (str-list2 (mapcar (lambda (elem) + (cond + ((symbolp elem) + (symbol-name elem)) + (t + elem))) + lst))) + (setq ret (append ret str-list2)))) + ret))) + +(defun org-roam--extract-titles () + "Extract the titles from current buffer. + +Titles are obtained via the #+TITLE property, or aliases +specified via the #+ROAM_ALIAS property." + (let* ((props (org-roam--extract-global-props '("TITLE" "ROAM_ALIAS"))) + (aliases (cdr (assoc "ROAM_ALIAS" props))) + (title (cdr (assoc "TITLE" props))) + (alias-list (org-roam--aliases-str-to-list aliases))) + (if title + (cons title alias-list) + alias-list))) (defun org-roam--build-cache (dir) "Build the org-roam caches in DIR." @@ -156,8 +198,8 @@ ITEM is of the form: (:from from-path :to to-path :properties (:content preview- (dolist (file org-roam-files) (with-temp-buffer (insert-file-contents file) - (when-let ((title (org-roam--extract-title))) - (puthash file title file-titles))) + (when-let ((titles (org-roam--extract-titles))) + (puthash file titles file-titles))) org-roam-files)) (list :directory dir diff --git a/org-roam.el b/org-roam.el index bca126d..7a64a7e 100644 --- a/org-roam.el +++ b/org-roam.el @@ -243,14 +243,18 @@ as a unique key." (f-descendant-of-p (file-truename path) (file-truename org-roam-directory))))) -(defun org-roam--get-title-from-cache (file) - "Return title of `FILE' from the cache." +(defun org-roam--get-titles-from-cache (file) + "Return titles and aliases of `FILE' from the cache." (or (gethash file (org-roam--titles-cache)) (progn (unless (org-roam--cache-initialized-p) (user-error "The Org-Roam caches aren't built! Please run org-roam--build-cache-async")) nil))) +(defun org-roam--get-title-from-cache (file) + "Return the title of `FILE' from the cache." + (car (org-roam--get-titles-from-cache file))) + (defun org-roam--find-all-files () "Return all org-roam files." (org-roam--find-files (file-truename org-roam-directory))) @@ -271,12 +275,17 @@ If `ABSOLUTE', return an absolute file-path. Else, return a relative file-path." (file-relative-name absolute-file-path (file-truename org-roam-directory))))) -(defun org-roam--get-title-or-slug (file-path) - "Convert `FILE-PATH' to the file title, if it exists. Else, return the path." - (or (org-roam--get-title-from-cache file-path) - (-> file-path - (file-relative-name (file-truename org-roam-directory)) - (file-name-sans-extension)))) +(defun org-roam--path-to-slug (path) + "Return a slug from PATH." + (-> path + (file-relative-name (file-truename org-roam-directory)) + (file-name-sans-extension))) + +(defun org-roam--get-title-or-slug (path) + "Convert `PATH' to the file title, if it exists. Else, return the path." + (if-let (titles (org-roam--get-titles-from-cache path)) + (car titles) + (org-roam--path-to-slug path))) (defun org-roam--title-to-slug (title) "Convert TITLE to a filename-suitable slug." @@ -352,13 +361,10 @@ If PREFIX, downcase the title before insertion." (region-text (when region (buffer-substring-no-properties (car region) (cdr region)))) - (completions (mapcar (lambda (file) - (list (org-roam--get-title-or-slug file) - file)) - (org-roam--find-all-files))) + (completions (org-roam--get-title-path-completions)) (title (completing-read "File: " completions nil nil region-text)) (region-or-title (or region-text title)) - (absolute-file-path (or (cadr (assoc title completions)) + (absolute-file-path (or (cdr (assoc title completions)) (org-roam--make-new-file title))) (current-file-path (-> (or (buffer-base-buffer) (current-buffer)) @@ -376,14 +382,23 @@ If PREFIX, downcase the title before insertion." region-or-title)))))) ;;; Finding org-roam files +(defun org-roam--get-title-path-completions () + "Return a list of cons pairs for titles to absolute path of Org-roam files." + (let ((files (org-roam--find-all-files)) + (res '())) + (dolist (file files) + (if-let (titles (org-roam--get-titles-from-cache file)) + (dolist (title titles) + (setq res (cons (cons title file) res))) + (setq res (cons (cons (org-roam--path-to-slug file) file) res)))) + res)) + (defun org-roam-find-file () "Find and open an org-roam file." (interactive) - (let* ((completions (mapcar (lambda (file) - (list (org-roam--get-title-or-slug file) file)) - (org-roam--find-all-files))) + (let* ((completions (org-roam--get-title-path-completions)) (title-or-slug (completing-read "File: " completions)) - (absolute-file-path (or (cadr (assoc title-or-slug completions)) + (absolute-file-path (or (cdr (assoc title-or-slug completions)) (org-roam--make-new-file title-or-slug)))) (find-file absolute-file-path))) @@ -399,7 +414,7 @@ If PREFIX, downcase the title before insertion." (interactive) (let* ((roam-buffers (org-roam--get-roam-buffers)) (names-and-buffers (mapcar (lambda (buffer) - (cons (or (org-roam--get-title-from-cache + (cons (or (org-roam--get-title-or-slug (buffer-file-name buffer)) (buffer-name buffer)) buffer)) @@ -473,11 +488,11 @@ This is equivalent to removing the node from the graph." ;; Step 2: Remove from the title cache (remhash file (org-roam--titles-cache)))) -(defun org-roam--update-cache-title () +(defun org-roam--update-cache-titles () "Insert the title of the current buffer into the cache." - (when-let ((title (org-roam--extract-title))) + (when-let ((titles (org-roam--extract-titles))) (puthash (file-truename (buffer-file-name (current-buffer))) - title + titles (org-roam--titles-cache)))) (defun org-roam--update-cache () @@ -485,7 +500,7 @@ This is equivalent to removing the node from the graph." (save-excursion (org-roam--clear-file-from-cache) ;; Insert into title cache - (org-roam--update-cache-title) + (org-roam--update-cache-titles) ;; Insert new items (let ((items (org-roam--parse-content))) (dolist (item items) diff --git a/tests/roam-files/alias.org b/tests/roam-files/alias.org new file mode 100644 index 0000000..c1ef924 --- /dev/null +++ b/tests/roam-files/alias.org @@ -0,0 +1,2 @@ +#+ROAM_ALIAS: "a1" "a 2" +#+TITLE: t1 diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index e871497..a56994a 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -76,67 +76,6 @@ ;;; Tests (describe "org-roam--build-cache-async" - (it "initializes correctly" - (org-roam--test-init) - (expect (org-roam--cache-initialized-p) :to-be nil) - (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 0) - (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 0) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 0) - - (org-roam--build-cache-async) - (sleep-for 3) ;; Because it's async - - ;; Caches should be populated - (expect (org-roam--cache-initialized-p) :to-be t) - (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 4) - (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 5) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 5) - - ;; Forward cache - (let ((f1 (gethash (abs-path "f1.org") (org-roam--forward-links-cache))) - (f2 (gethash (abs-path "f2.org") (org-roam--forward-links-cache))) - (nested-f1 (gethash (abs-path "nested/f1.org") - (org-roam--forward-links-cache))) - (nested-f2 (gethash (abs-path "nested/f2.org") - (org-roam--forward-links-cache))) - (expected-f1 (list (abs-path "nested/f1.org") - (abs-path "f2.org"))) - (expected-nested-f1 (list (abs-path "nested/f2.org") - (abs-path "f1.org"))) - (expected-nested-f2 (list (abs-path "nested/f1.org")))) - - (expect f1 :to-have-same-items-as expected-f1) - (expect f2 :to-be nil) - (expect nested-f1 :to-have-same-items-as expected-nested-f1) - (expect nested-f2 :to-have-same-items-as expected-nested-f2)) - - ;; Backward cache - (let ((f1 (hash-table-keys (gethash (abs-path "f1.org") - (org-roam--backward-links-cache)))) - (f2 (hash-table-keys (gethash (abs-path "f2.org") - (org-roam--backward-links-cache)))) - (nested-f1 (hash-table-keys(gethash (abs-path "nested/f1.org") - (org-roam--backward-links-cache)))) - (nested-f2 (hash-table-keys (gethash (abs-path "nested/f2.org") - (org-roam--backward-links-cache)))) - (expected-f1 (list (abs-path "nested/f1.org"))) - (expected-f2 (list (abs-path "f1.org"))) - (expected-nested-f1 (list (abs-path "nested/f2.org") - (abs-path "f1.org"))) - (expected-nested-f2 (list (abs-path "nested/f1.org")))) - (expect f1 :to-have-same-items-as expected-f1) - (expect f2 :to-have-same-items-as expected-f2) - (expect nested-f1 :to-have-same-items-as expected-nested-f1) - (expect nested-f2 :to-have-same-items-as expected-nested-f2)) - - ;; Titles Cache - (expect (gethash (abs-path "f1.org") (org-roam--titles-cache)) :to-equal "File 1") - (expect (gethash (abs-path "f2.org") (org-roam--titles-cache)) :to-equal "File 2") - (expect (gethash (abs-path "nested/f1.org") (org-roam--titles-cache)) :to-equal "Nested File 1") - (expect (gethash (abs-path "nested/f2.org") (org-roam--titles-cache)) :to-equal "Nested File 2") - (expect (gethash (abs-path "no-title.org") (org-roam--titles-cache)) :to-be nil))) - -(describe "org-roam--build-cache-async-multi" (it "initializes correctly" (org-roam--clear-cache) (org-roam--test-multi-init) @@ -152,7 +91,7 @@ (expect (org-roam--cache-initialized-p) :to-be t) (expect (hash-table-count (org-roam--forward-links-cache)) :to-be 4) (expect (hash-table-count (org-roam--backward-links-cache)) :to-be 5) - (expect (hash-table-count (org-roam--titles-cache)) :to-be 5) + (expect (hash-table-count (org-roam--titles-cache)) :to-be 6) ;; Forward cache (let ((f1 (gethash (abs-path "f1.org") @@ -197,13 +136,15 @@ ;; Titles Cache (expect (gethash (abs-path "f1.org") - (org-roam--titles-cache)) :to-equal "File 1") + (org-roam--titles-cache)) :to-equal (list "File 1")) (expect (gethash (abs-path "f2.org") - (org-roam--titles-cache)) :to-equal "File 2") + (org-roam--titles-cache)) :to-equal (list "File 2")) (expect (gethash (abs-path "nested/f1.org") - (org-roam--titles-cache)) :to-equal "Nested File 1") + (org-roam--titles-cache)) :to-equal (list "Nested File 1")) (expect (gethash (abs-path "nested/f2.org") - (org-roam--titles-cache)) :to-equal "Nested File 2") + (org-roam--titles-cache)) :to-equal (list "Nested File 2")) + (expect (gethash (abs-path "alias.org") + (org-roam--titles-cache)) :to-equal (list "t1" "a1" "a 2")) (expect (gethash (abs-path "no-title.org") (org-roam--titles-cache)) :to-be nil) @@ -264,16 +205,16 @@ ;; Titles Cache (expect (gethash (abs-path "mf1.org") (org-roam--titles-cache)) - :to-equal "Multi-File 1") + :to-equal (list "Multi-File 1")) (expect (gethash (abs-path "mf2.org") (org-roam--titles-cache)) - :to-equal "Multi-File 2") + :to-equal (list "Multi-File 2")) (expect (gethash (abs-path "nested/mf1.org") (org-roam--titles-cache)) - :to-equal "Nested Multi-File 1") + :to-equal (list "Nested Multi-File 1")) (expect (gethash (abs-path "nested/mf2.org") (org-roam--titles-cache)) - :to-equal "Nested Multi-File 2") + :to-equal (list "Nested Multi-File 2")) (expect (gethash (abs-path "no-title.org") (org-roam--titles-cache)) :to-be nil)))) @@ -326,7 +267,7 @@ (rename-file (abs-path "f1.org") (abs-path "new_f1.org")) ;; Cache should be cleared of old file - (expect (gethash (abs-path "f1.org") (org-roam--forward-links-cache)) :to-be nil) + (expect (gethash (abs-path "f1.org") (org-roam--forward-links-cache)) :to-be nil) (expect (->> (org-roam--backward-links-cache) (gethash (abs-path "nested/f1.org")) (hash-table-keys) @@ -338,12 +279,12 @@ (expect (->> (org-roam--forward-links-cache) (gethash (abs-path "new_f1.org")) (member (abs-path "nested/f1.org"))) :not :to-be nil) + ;; Links are updated (expect (with-temp-buffer (insert-file-contents (abs-path "nested/f1.org")) (buffer-string)) :to-match (regexp-quote "[[file:../new_f1.org][File 1]]"))) - (it "f1 -> f1 with spaces" (rename-file (abs-path "f1.org") (abs-path "f1 with spaces.org"))