Reading Metadata

Menu

Introduction

These functions complement org-mode publishing features by adding functions to ready metadata from all the Org Mode files in a directory.

The metadata can then be used to build blog index pages, sitemaps, etc, using the literate programming features Org Mode provides.

Variables

Post dir and pathname separator

(defvar posts-dir "notes"
  "Where do the posts live, relative to the site root?")

(defvar dir-separator "/"
  "The path separator")

Iterators

Replicate some utilities taken from Ruby. There are various libraries implementing similar functions, among which dash and seq. We stick to seq.

(defun flatten (lst)
  (labels ((rflatten (lst1 acc)
             (dolist (el lst1)
               (if (listp el)
                   (setf acc (rflatten el acc))
                   (push el acc)))
             acc))
    (reverse (rflatten lst nil))))

(defun compact (list)
  (seq-filter (lambda (x) x) list))

(defun uniq (list)
  (uniq-ll list nil))

(defun uniq-ll (list accumulator)
  (if (not list)
      (reverse accumulator)
    (let ( (el (car list)) )
      (uniq-ll (cdr list) (if (member el accumulator) accumulator (cons el accumulator))))))

(defun extract (prop assoc-list)
  "Extract all the values of property prop from the association list assoc-list"
  (mapcar (lambda (x) (cdr (assoc prop x))) assoc-list))

Functions to extract properties from a file

(defun org-property-list-to-assoc-list (properties)
  "Make a plist into an association list"
  (mapcar (lambda (x) (cons (org-element-property :key x)
                            (org-element-property :value x)))
          properties))

(defun org-global-props (&optional property buffer)
  "Get the plists of global org properties of current buffer."
  (unless property
    (setq property "\\(TITLE\\|AUTHOR\\|DATE\\|DESCRIPTION\\|CATEGORY\\|KEYWORDS\\)"))
  (with-current-buffer (or buffer (current-buffer))
    (org-element-map (org-element-parse-buffer)
        'keyword
      (lambda (el)
        (when (string-match property (org-element-property :key el)) el)))))


(defun file-metadata (filename &optional directory root-directory root-url properties-regexp)
  "Get the metadata of file `filename`.

Optional argument `properties-regexp` overrides the default list
of properties to select from the Org Mode file.

Optional arguments `directory`, `root-directory` and `root-url`
determine how URLs are generated for the file.

More in details:

- `directory` determines the relative URLs: paths are generated
  relative to the value of `directory`

- `root-directory` determines the absolute URLs: paths are
  generated from this directory, assumes as the root;
  `root-directory` should point to the root of your website
  sources

- `root-url` is prepended to `root-directory`, if specified.

Thus, if we have

   A -> B -> C -> file.org

   (cd 'A/B/C')
   (file-metadata 'C.org' 'B' 'A' 'https://example.com) ->
      RELATIVE_URL C/file.html
      ABSOLUTE_URL https://example.com/B/C"
  (let* ( (dir (or directory (file-name-directory filename)))
          (root-dir (or root-directory (file-name-directory filename)))

          (basename (file-name-nondirectory filename))
          (relname  (file-relative-name filename dir))
          (absname  (file-relative-name filename root-dir))

          (relurl   (replace-regexp-in-string "\\.org$" ".html" relname)) 
          (absurl   (concat (or root-url "") "/" (replace-regexp-in-string "\\.org$" ".html" absname))) )
    (with-temp-buffer
      (insert-file-contents filename)
      (setq props (org-property-list-to-assoc-list (org-global-props)))
      (append
       (list (cons "FILENAME" filename)
             (cons "BASENAME" basename)
             (cons "RELATIVE" relname)
             (cons "ABSOLUTE" absname)
             (cons "IS_POST"  (is-post? filename))
             (cons "CATEGORY_FROM_FILE" (pathname-to-category filename))
             (cons "RELATIVE_URL" relurl)
             (cons "ABSOLUTE_URL" absurl)
             (cons "DATE_ISO8601" (format-date-iso8601 (cdr (assoc "DATE" props)))))
       props)
    )))

(defun files-metadata (directory &optional root-directory root-url)
  "Get all org mode files in a directory and return their properties.

Optional argument `root-directory` and `root-url` define how
paths and urls are computed.

See the documentation of file-metadata for more details."
  (let* ( (files (directory-files-recursively directory "\\.org$"))
          (filenames (interesting-files files)) )
    (mapcar (lambda (x) (file-metadata x directory root-directory root-url)) filenames)))

(defun files-metadata-dirlist (list &optional root-directory root-url)
  "Apply files-metadata to a list of directories."
  (if list
      (append (files-metadata (car list))
              (files-metadata-dirlist (cdr list)))
    nil))

Grouping

(defun group-by-year (metadata)
  (seq-group-by
   (lambda (x) (decoded-time-year (parse-time-string (cdr (assoc "DATE" x)))))
   metadata))

(defun group-by-category (metadata)
  (seq-group-by
   (lambda (x) (cdr (assoc "CATEGORY_FROM_FILE" x)))
   metadata))

(defun sort-group (group)
  (sort group (lambda (x y) (> (car x) (car y)))))

(defun sort-group-string (group)
  (sort group (lambda (x y) (string< (car x) (car y)))))

(defun group-to-html (group)
  (mapconcat
   'identity
   (mapcar
    (lambda (x)
      (concat
       (format "<h2>%s</h2>\n" (car x))
       (format "<ul class=\"post-list\">\n")
       (mapconcat 'identity (mapcar 'entry-to-html (cdr x)) " ")
       (format "</ul>\n\n")))
    group)
   " "))

(defun entry-to-html (entry)
   (format "<li class=\"post\" data-keywords=\"%s\">
     <span class=\"post-date\">%s</span>
     <span class=\"post-title\"><a href=\"%s\">%s</a></span>
   </li>\n"
           ""
           (format-date (cdr (assoc "DATE" entry)))
           (cdr (assoc "RELATIVE_URL" entry))
           (cdr (assoc "TITLE" entry))))

General Purpose Fnuctions

;; time is required or encode-time will fail
(defun format-date (date-string)
  (let* ( (date (parse-time-string (concat date-string "10:00")))
          (encoded (encode-time date)) )
    (format-time-string "%A, %B %d, %Y" encoded)))

;; time is required or encode-time will fail
(defun format-date-iso8601 (date-string)
  (if date-string
      (let* ( (date (parse-time-string (concat date-string " 10:00")))
              (encoded (encode-time date)) )
        (format-time-string "%Y-%m-%dT%H%M%S%z" encoded))
    (format-time-string "%Y-%m-%dT%H%M%S%z")))