Creating Lists of Tagged Posts using Emacs Lisp
One of the features I want for this blog is the ability for “project” type posts which are persistently updated to have links to all historic posts which pertain to the evolution of that project over time. There isn’t built in support for post tagging in projects exported with org-publish
so this was a perfect time for me to practice my Emacs lisp (lisp) coding.
I first looked over the implementation of tagging posts in org-static-blog
which is a very simple lisp static blog generator. Here are the relevant functions:
(defcustom org-static-blog-posts-directory "~/Documents/website/blog/" "Directory where published ORG files are stored. When publishing, posts are rendered as HTML, and included in the index, archive, tags, and RSS feed." :group 'org-static-blog) (defun org-static-blog-get-post-filenames () "Returns a list of all posts." (directory-files org-static-blog-posts-directory t ".*\\.org$" nil)) (defun org-static-blog-get-title (post-filename) "Extract the `#+title:` from POST-FILENAME." (let ((case-fold-search t)) (with-temp-buffer (insert-file-contents post-filename) (goto-char (point-min)) (search-forward-regexp "^\\#\\+title:[ ]*\\(.+\\)$") (match-string 1)))) (defun org-static-blog-get-tags (post-filename) "Extract the `#+filetags:` from POST-FILENAME as list of strings." (let ((case-fold-search t)) (with-temp-buffer (insert-file-contents post-filename) (goto-char (point-min)) (if (search-forward-regexp "^\\#\\+filetags:[ ]*\\(.+\\)$" nil t) (split-string (match-string 1)))))) (defun org-static-blog-get-tag-tree () "Return an association list of tags to filenames. e.g. `(('foo' 'file1.org' 'file2.org') ('bar' 'file2.org'))`" (let ((tag-tree '())) (dolist (post-filename (org-static-blog-get-post-filenames)) (let ((tags (org-static-blog-get-tags post-filename))) (dolist (tag tags) (if (assoc-string tag tag-tree t) (push post-filename (cdr (assoc-string tag tag-tree t))) (push (cons tag (list post-filename)) tag-tree))))) tag-tree))
The first thing to notice is that this org-static-blog
uses regexp to extract information for the keywords at the start of an org file (we’re interested in the #+title:
and #+date:
and #+filetags:
keywords).
While this certainly works, I think it makes more sense and is perhaps more instructive and robust to use let org parse the buffer with org-element-parse-buffer
into the Abstract Syntax Tree (ASST) representing the org buffer and retrieve our keywords by mapping and reducing across this AST.
I used this StackExchange answer as the basis for creating the following function:
(defun my-org-get-keyword (post-filename keyword) "Extract the value of `#+keyword:` from post-filename." (let ((case-fold-search t)) (with-temp-buffer (insert-file-contents post-filename) (org-element-property :value (car (org-element-map (org-element-parse-buffer) 'keyword (lambda (el) (when (string-match keyword (org-element-property :key el)) el))))))))
Which wen we call it with an org file asking for the title we get:
(my-org-get-keyword "~/Documents/website/blog/2019-02-06-creating-tag-post-lists.org" "title")
Creating Lists of Tagged Posts using Emacs Lisp
I then combined my-org-get-keyword
with a modified version of the org-static-blog-get-tag-tree
function to get the tag tree for my blog:
(setq my-blog-posts-directory "~/Documents/website/blog/") (defun my-blog-get-post-filenames () "Returns a list of all posts." (reverse (seq-filter (lambda (el) (not (string-match ".*index\\.org$" el))) (directory-files my-website-blog-dir t ".*\\.org$" nil)))) (defun my-blog-get-tag-tree () "Return an association list of tags to filenames. e.g. `(('foo' 'file1.org' 'file2.org') ('bar' 'file2.org'))`" (let ((tag-tree '())) (dolist (post-filename (my-blog-get-post-filenames)) (let* ((tags-str (my-org-get-keyword post-filename "filetags")) (tags (if tags-str (split-string tags-str)))) (dolist (tag tags) (if (assoc-string tag tag-tree t) (push post-filename (cdr (assoc-string tag tag-tree t))) (push (cons tag (list post-filename)) tag-tree))))) tag-tree))
Calling my-blog-get-tree
gives us:
(my-blog-get-tag-tree)
elisp | 2019-02-06-creating-tag-post-lists.org | |||
emacs | 2019-02-06-creating-tag-post-lists.org | 2019-02-05-starting-blogging.org | 2019-02-05-ox-slimhtml.org | |
css | 2019-01-01-cuss.org | |||
blog | 2019-02-06-creating-tag-post-lists.org | 2019-02-05-starting-blogging.org | 2019-02-05-ox-slimhtml.org | 2019-01-01-css.org |
org-mode | 2019-02-06-creating-tag-post-lists.org | 2019-02-05-starting-blogging.org | 2019-02-05-ox-slimhtml.org | 2019-01-01-css.org |
Now I just need to filter this tag-tree
for only the tag I want:
(defun my-blog-filenames-for-tag (tag) (reverse (cdr (seq-find (lambda (el) (string= (car el) tag)) (my-blog-get-tag-tree)))))
Which gives us
(my-blog-filenames-for-tag "emacs")
2019-02-06-creating-tag-post-lists.org | 2019-02-05-starting-blogging.org | 2019-02-05-ox-slimhtml.org |
Finally I need a function to format this list of post filenames into an org list of formatted org links to those files with their date and title taking care to use relative links (otherwise on export, a link from an org file in one directory to an org file in another directory will be broken).
(defun my-blog-posts-to-list (post-filenames) (seq-reduce (lambda (acc el) (concat acc "\n- " el)) (seq-map (lambda (el) (org-make-link-string (concat "file:" (file-relative-name el)) (concat "/" (my-org-get-keyword el "date") "/ " (my-org-get-keyword el "title")))) post-filenames) ""))
And voilĂ ! We have our formatted list of links to all the posts tagged “blog”:
(my-blog-posts-to-list (my-blog-filenames-for-tag "blog"))
- 2018-02-06 Creating Lists of Tagged Posts using Emacs Lisp
- 2018-02-05 Beginning Blogging with
org-publish
- 2019-02-05 Brief foray into
ox-slimhtml
- 2019-02-05 Creating the CSS for this website