A Function for Rendering an Index

I keep an A6 pocket notebook on me at all times - usually a plain leuchtterm. Over the past three or four notebooks, I've developed the habit of keeping an index. This started as an index in the back of the notebook but, after finding the index useful a couple of times while writing in my digital notes, I eventually ended up typing up the index in a file dedicated to keeping the indices.

I use emacs' org-mode for my digital notes. Emacs has been my main text editor for several years now, and I've recently been trying to get more comfortable with emacs lisp. As a mini project, I decided a few days ago that instead of manually typing out an org-mode table, I might want to consider an easier to type format and have some code that wrangled the table for me instead. So without further ado, here's the code.

(defun sjm/index-table (data)
  "Generate an org-mode table based on input data in the format '((<notebook> <num> <topic> ...) ...) for use generating a notebook index."
  (let ((table-strings (seq-map
                        (lambda (notebook-data)
                          (let* ((notebook-name (car notebook-data))
                                 (notebook-index (cdr notebook-data))
                                 ;; for each item in index, add the page number to a hash table where the
                                 ;; keys are keywords and the values are a list of page numbers.
                                 ;;
                                 ;; Then, once we have that we can build a table using it.
                                 (table-data (seq-reduce
                                              (lambda (table page)
                                                (let ((page-num (car page))
                                                      (keywords (cdr page)))
                                                  (seq-reduce
                                                   (lambda (table keyword)
                                                     (if-let (pages (gethash keyword table))
                                                         (puthash keyword (cons page-num pages) table)
                                                       (puthash keyword (cons page-num '()) table))
                                                     table)
                                                   keywords
                                                   table)))
                                              notebook-index
                                              (make-hash-table))))

                            (let ((table-string ""))
                              (maphash (lambda (keyword pages)
                                         (print keyword)
                                         (print pages)
                                         (set
                                          'table-string
                                          (concat table-string "\n"
                                                  "| "
                                                  (cond ((symbolp keyword) (symbol-name keyword))
                                                        ((stringp keyword) keyword))
                                                  " | "
                                                  (string-join (seq-map #'number-to-string (reverse pages)) ",")
                                                  " | "
                                                  notebook-name
                                                  " |")))
                                       table-data)
                              table-string)))
                        data)))
    (apply #'concat table-strings)))

This code is admittedly a bit of a mess - but I'm trying to be forgiving as I both learn elisp and the paradigms around working with code for modifying a text editor. I often say to my colleagues "Make it work, make it Right, make it fast" (which I believe I've taken from Kent Beck). This code is at the 'make it work' phase, for sure. I don't really know what "elisp Right" is, yet. Personally, I'm most uncomfortable with the mutation of the table-string variable using set to concat row after row.

The input format is classically lispy, It's a list of pairs, where each pair has as it's car a string describing a notebook name, and as it's cdr another list, where there's an entry per page, starting with a page number, and then with a series of symbols or strings that are the items being indexed. An example

'(("My Notebook" .
   ((1 elisp software)
    (2 "The Moon" elisp))))

This would result in an org-mode table that looked something like

| elisp    | 1,2 | My Notebook | 
| software |   1 | My Notebook | 
| The Moon |   2 | My Notebook | 

I have the table include the name of the notebook on every single row of the table because I regularly search this file, and this way I can see the notebook name on the line along with my search term immediately - so I don't need to go hunting for context. Having the name on every row also lets me alphabetise by the first column's contents without losing track of each row's origin. This way I can see if the same terms appear across notebooks.

I'm really happy with how this works right now - I have a data block in an org file with the format above, which then gets passed into the index-table function. When I modify the data block, I rerun the function and get a new table and my text editor note taking experience is a little nicer, because I don't have to tab my way through a table cell by cell typing the same page number over and over. A success.

Subscribe to Everything

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe