Ivory Siege Tower

mobile construct built of thoughts and parentheses

Org-Roam-Blog Engine Package

orb project_index source

Org-Roam-Blog is my elisp package that projects specific parts of the Org-Roam knowledgebase in a form of static website. The idea was to have a declarative way to define index website sections consisting of individual entry pages, all of that within org-roam itself. To push that idea to an extreme, I've been developing the package itself in a "literate programming" paradigm (using emacs' org-babel code blocks support). Consequently, Org-Roam-Blog definitions became the first published section of my SiegeTower homepage :)

The current snapshot of the Org-Roam-Blog can be found on GitHub.

Here is a topmost node of the org-roam-blog package "literate" codebase. It holds customizable variables' definitions, and imports and glues together all the other elisp source files for the package, namely:

  • Org-Roam-Blog Utils - various utilities for nodes' content processing and what not

  • Org-Roam-Blog Index - processing of indexes - queries for materials that are grouped and dispatched together, as a subsection of a site.

  • Org-Roam-Blog Site - definitions for creation of a static site instance as a collection of several material index specifications.

This is a serious but also a fun-first project. Although it serves me well for supporting the Tower, and was fun to write, there are still some problems and misconceptions about it:

  • Though the code is functional it is not - yet - a proper emacs package. I utilize a rust orgize library as a default org -> html exporter. Therefore, I'll have to learn how to ship elisp package together with rust dynamic module. I know this is possible ofc. I just need to figure out how. Later.

  • The code is in development and very fluid and raw...

    • I didn't want (and still try to avoid) using elisp's EIEIO object system for this project. Thus "methods" of those few structs I use are just functions attached to their fields. Works for me, allows to customize default methods (e.g. entry node to context exporters of an index), but looks a bit awkward.

    • After I've been working on it for a while it became clear to me that the project organization is at best an alpha prototype. The worst hack I'm having now is having an org-roam-blog-g global context object that points to a website instance being processed at runtime and also collects all entry references for a site during the export (see below). It appeared as argumets to context processors and node text preprocessors when it was already too painful to modify their arglists. In the end, it doesn't feel that bad given this overall exporter from Org-Roam is essentially single-threaded...

  • Although internally I sometimes short-cut reference to package as orb, I know it's neither only nor the first "orb" tied to org-roam. Where collisions will occur I'm gonna put the full org-roam-blog prefix.

§ Variables

Communication of the website state through the global variable, named G similar as in one web framework I vaguely remember. That does feel like a hack.

  ;; FIXME: this seems redundant. I can use the `site' instance for
  ;; global context, just carrying a global var pointing to it.
  (cl-defstruct (org-roam-blog-global-context
                 (:constructor org-roam-blog-global-context--create)
                 (:copier nil))
    "Structure of `org-roam-blog-g' global context object for a
  website being staged in the runtime."
    (site nil)
    (entry-registry nil))


  (defvar org-roam-blog-g (org-roam-blog-global-context--create)
    "Global context object for a website being processed.")


  (defsubst org-roam-blog-g-site-get ()
    "Get the website instance from the global context."
    (org-roam-blog-global-context-site org-roam-blog-g))


  (defsubst org-roam-blog-g-site-set (obj)
    "Set the website instance from the global context to OBJ."
    (setf (org-roam-blog-global-context-site org-roam-blog-g)
          obj))


  (defsubst org-roam-blog-g-entries-get ()
    "Get the website entry registry from the global context."
    (org-roam-blog-global-context-entry-registry org-roam-blog-g))


  (defsubst org-roam-blog-g-entries-set (obj)
    "Set the website entry registry from the global context to OBJ."
    (setf (org-roam-blog-global-context-entry-registry org-roam-blog-g)
          obj))


  (defsubst org-roam-blog-g-reset ()
    "Erases the Org Roam Blog global context."
    (org-roam-blog-g-site-set nil)
    (org-roam-blog-g-entries-set nil))

I don't wanna bother with EIEIO at the moment, that's why I build with structs and lambdas attached to the structs.

§ Custom Variables

  (defcustom org-roam-blog-local-sync-command "rsync -a --delete"
    "Shell command for local folder synchronization during site export."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-html-fn-property "ORB_HTML_FN"
    "Header property of designating alternative HTMLizer functions."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-html-src-property "ORB_HTML_SRC"
    "Header property pointing to an alternative pregenerated HTML markup for a node."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-toc-level-property "TOC_LEVEL"
    "TOC level header property. Headlines of this level or higher will appear in contents."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-default-date-property "ADDED"
    "Default header property of the Org item headers used for sorting in Indexes."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-default-entry-dir-name "items"
    "Name of default subdirectory containing entries for an index."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-index-filename-prefix "index"
    "Name of default filename prefix for an index."
    :group 'org-roam-blog
    :type  'string)

Media files exporter settings:

  (defcustom org-roam-blog-default-media-dir-name "media"
    "Name of default subdirectory containing media files for an index."
    :group 'org-roam-blog
    :type  'string)


  (defcustom org-roam-blog-media-image-extensions '("png" "jpg" "jpeg" "gif" "svg")
    "List of extensions for image files for media exporting."
    :group 'org-roam-blog
    :type  'list)

  
  ;; FIXME: this setting should be site-customizeable
  (defcustom org-roam-blog-media-image-inline-format-string 
    "#+begin_export html\n<figure class=\"image\">\n<img class=\"lightense\" src=\"%s\">\n</figure>\n#+end_export"
    "Format string that will envelop media image file URL link."
    :group 'org-roam-blog
    :type  'string)

Regular expressions for anchor links processing:

  (defcustom org-roam-blog-anchor-regex "\\[\\[#[0-9a-zA-Z-]+\\]\\[§\\]\\]"
    "Regex used to find anchor link in a title of a headline."
    :group 'org-roam-blog
    :type 'string)


  (defcustom org-roam-blog-sure-headline-regex "^*+[ ]+"
    "Regex used to ensure the headline is true and not just a bold text
    (that would not include internal whitespace)."
    ;; NOTE: still buggy.
    ;; Parsing in `org-roam-blog--toc-to-context' breaks on lines that start with bold text.
    :group 'org-roam-blog
    :type 'string)

Formatters for anchor links processing:

  (defvar org-roam-blog-anchor-id-format "%s-%s"
    "title-idx. Just to be sure it is unified.")


  (defcustom org-roam-blog-anchor-format
    (format " [[#%s][§]]" org-roam-blog-anchor-id-format)
    "Default formatter for the heading anchor."
    :group 'org-roam-blog
    :type 'string)

The following is a regex to find the beginning of node textual content.

I could not quite make the emacs lisp regex to actually skip all the ^\s*:.*$ and ^\s*#.*$ groups right at the beginning of the node content, for drawer specs and comments correspondingly. Maybe refer to it at some point later.

  (defcustom org-roam-blog-outline-content-start-regexp ":END:.*\n"
    "Regex used to find the beginning of node's content."
    :group 'org-roam-blog
    :type 'string)

§ Dynamic Module Loading

This section finds the path to the package location and provides for loading a pre-built Rust dynamic module shipped alongside.

  (defcustom org-roam-blog-dynmod-arch "x86_64"
    "Specifies architecture for which the rust dynamic module was built."
    :group 'org-roam-blog
    :type 'string)

  ;; Base path for the package
  (defconst org-roam-blog-base (file-name-directory load-file-name))

  ;; the following loads my experimental Rust dynamic module:
  (defun org-roam-blog-load-dynamic-module ()
    (module-load
     (expand-file-name (format "orb_dynmod_%s.so"
                               org-roam-blog-dynmod-arch)
                       org-roam-blog-base)))
  ;; a call of that function `my-org-dynmod/org-to-html' for
  ;; `org-roam-blog-utils'

§ Package Definition

In the end, state the package, its version and dependencies with the define-package form as instructed in the Emacs docs for packages with multiple files.

The following form will be tangled to a separate org-roam-blog-pkg.el:

  ;; Define the package
  (define-package "org-roam-blog" "0.0.1"
    "A static site publishing facility for Org Roam"
    '(('org-roam "2.0.0")
      ('unidecode "0.2")
      ('f "0.20.0")
      ('ht "2.3")
      ('mustache "0.24")
      ('simple-httpd "1.5.1")))