Blogging with org-mode
[Blog Index] [RSS] [Prev: First post] [Next: Developing Java applications with Emacs using Nix]
Posted on 2024-08-17 in blog emacs org .
Table of Contents
Introduction
Emacs' org-mode is fantastic. I use it whenever I have to write something, be it notes or a letter or even code. I would like to use it to write blog posts, since it can generate pdfs, html, etc from documents.
I could just generate all blog posts by running org-export, but that
could get tedious and also depends on my entire emacs configuration. I
would be good to package all of my bespoke elisp plus all emacs
package dependencies into a single place and run it with emacs -Q
--batch
to keep everything nice and reproducible.
So, the requirements are roughly:
- A way to write blog posts as org-mode documents with minimal fuss
- with one post per file, and
- no mention of CSS,
- or anything hinting at the fact that this is going to be a blog post – this is to ensure that a future templating system can be created without having to migrate the documents.
- Can be built without having emacs open, perhaps by running emacs
in
--batch
mode- Note: it turns out that this is not without challenges, described later.
What even is this?
This document is
- my first blog post
- a literate program
- my static blog/website generator
Why would I want a single document to be all three of those things? Because I have no other ideas for a first blog post. Also, isn't it neat that this post is, in a way, an input to itself?
Alternatives considered
Why write my own templating engine instead of using something else? Because there is really nothing that makes satisfying the above requirements easy.
There are org-capture templates but that's mostly about speeding up workflows that involve data entry, and not for keeping the template and the data separate. As I understand it, if you want to modify an org-capture template you have to "backfill" it into all the existing captured documents.
There are macros, but those don't seem designed to take arguments that represent multiple paragraphs of text (the blog post).
I found noweb in the manual, but that only applies to src blocks. While it would be possible to write the entire blog in src blocks, that seems cumbersome. Noweb seems nice though, as it allows passing in arguments to templates.
So, I wrote a quirky little templating engine. It only handles
inserting template variables in a limited number of spaces: headings,
normal text, and keywords (stuff like #+TITLE
).
Dependencies
Some of these dependencies, such as nix-mode
, are needed to show
syntax highlighting for Nix code. There needs to be a better way to do
this, so that each blog post can have arbitrary syntax highlighting.
(require 'org) (require 'org-element) (require 'htmlize) (require 's) (require 'pp) ;; for syntax highlighting (require 'nix-mode) ;; for generating the RSS feed (require 'esxml)
Emacs configuration
I have some form of these configurations in my init.el
so I've
copied them here so this generator works how I want, because it runs
out of a fresh emacs (emacs -Q
).
org-babel shouldn't confirm evaluation
This prevents org-babel from blocking execution to ask the user for input.
(defun my-org-confirm-babel-evaluate (lang body) (not (or (string= lang "html") (string= lang "elisp")))) (setq org-confirm-babel-evaluate #'my-org-confirm-babel-evaluate)
org-mode export export configurations
(setq org-html-htmlize-output-type 'css) (setq org-src-fontify-natively t)
The static site generator
The templating engine
Org-mode doesn't have an obvious templating solution. That is, given a org document that has references to template variables, and a document that defines these variables, there seems to be no blessed way to combine these two documents to produce a rendered template.
Convenience function to parse files and strings
(defun webgen--parse-file (file-name) (with-temp-buffer (insert-file-contents file-name) (org-element-parse-buffer))) (defun webgen--parse-string (str) (with-temp-buffer (insert str) (org-element-parse-buffer)))
Templates and their inputs
The templating engine is a function that takes
- a parsed org-mode document, and
a list of tags which are
(symbol . value)
pairswhere
symbol
is the name of the template variable (and will appear in the previous argument as\var{symbol}
). These are the substitutions that must be made into the template to produce the rendered document.
Each org-element needs to be handled. However, this engine only handles occurrences of template variable parameters in the following org elements:
Headlines:
* heading \var{param}
Keywords:
This lets us do stuff like:
#+TITLE: \var{title}
\LaTeX fragments
\var{abc}
in a paragraph gets parsed as alatex-fragment
org element.* heading blah blah blah \var{content} blah blah
While those are the ways tags can be inserted into a template, here are the ways a tag can be supplied by a org-mode document (i.e. this blog post document!)
In a headline:
* heading :content: blah blah
Now, using
\var{content}
in the template would insertblah blah
in there.This takes some care to implement. Any subheadings are lifted up as many levels as the heading that has the tag (in that example,
:content:
).* heading :content: ** subheading blah blah
If this is interpolated with a template document that uses
\var{content}
, the following will be inserted:* subheading blah blah
- Special type-specific tags. Blog posts have title, date, categories, and so on. These all have bespoke lisp code to extract and insert into a template.
The template interpolator
Recall that template inputs are passed via an alist like
'((var1 . value1) (var2 . value2)
where values could be either strings or lists of org elements.
(defun webgen--get-tag-map (tree) (apply #'append (org-element-map tree '(headline) (lambda (x) (let* ((tags (org-element-property :tags x)) (level (org-element-property :level x)) (contents (org-element-copy (org-element-contents x))) (lifted (org-element-map contents '(headline) (lambda (subhead) (let ((subhead-level (org-element-property :level subhead))) (org-element-put-property subhead :level (- subhead-level level)))))) (newel (funcall #'org-element-create `(section nil ,@contents)))) (mapcar (lambda (x) (cons (intern x) newel)) tags))))))
This is the pattern that matches template variables in the template document:
(defconst webgen-variable-regexp "\\\\var{\\([^}]+\\)}")
Here is how a single element from the template document is
interpolated with tags extracted by webgen--get-tag-map
. Notice the
three cases, one for each type of element that can contain a template
variable reference.
(defun webgen--interpolate-element (el tags) (let ((type (org-element-type el))) (cond ((eq type 'keyword) (let* ((value (org-element-property :value el)) (new-value (webgen--replace-string-tags value tags))) (org-element-put-property el :value new-value))) ((eq type 'headline) (let* ((value (org-element-property :raw-value el)) (new-value (webgen--replace-string-tags value tags))) (org-element-put-property el :raw-value new-value))) ((eq type 'latex-fragment) (let* ((value (org-element-property :value el)) (tag (if (string-match (format "^%s$" webgen-variable-regexp) value) (intern (match-string 1 value)) nil)) (tag-content (alist-get tag tags))) (unless (null tag) (when (null tag-content) (error "Missing template variable content for %s" tag)) (org-element-set-element el (if (stringp tag-content) (org-element-create 'section nil tag-content) tag-content))))))))
Notice that it calls out to webgen--replace-string-tags
– this is a
simple interpolator that just operates on strings and uses regular
expressions to replace \var{name}
with their looked-up value, but
only if the value was a string.
(defun webgen--replace-string-tags (string tags) (s-replace-regexp webgen-variable-regexp (lambda (match) (let* ((tag (intern (substring match 5 -1))) (tag-content (alist-get tag tags))) (when (null tag-content) (error "Missing template variable content for %s" tag)) (if (stringp tag-content) tag-content (error "Non-string-valued variable content for %s" tag)))) string))
Map this over the whole tree, and we have the template interpolator.
(defun webgen--interpolate-template (tree tags) (org-element-map tree '(keyword headline latex-fragment) (lambda (el) (webgen--interpolate-element el tags))) tree)
Putting it all together, we have a simple-to-use function (alongside
webgen--parse-file
which is given below) for rendering templates
with their inputs, and exporting the result to HTML. This will be the
main entry point to all the code in this section.
(defun webgen--interpolate-and-export (content-data template-data extra-tags output-file) (let ((tags (webgen--get-tag-map content-data))) (with-temp-buffer (cl-loop for (var . val) in extra-tags do (setf (alist-get var tags) val)) (let ((interpolated (webgen--interpolate-template template-data tags))) (insert (org-element-interpret-data interpolated)) (org-export-to-file 'html output-file)))))
Example
To illustrate the usage of the template interpolator, consider the following example template:
* \var{title}
The date:
\var{date}
The content:
\var{body}
And an actual post file called post.org
:
#+TITLE: some title #+DATE: <some date> * content :body: This is the blog content.
The :body:
tag signifies that that section should be substituted
(without the headline) into the \var{body}
template variable.
Now, we can use the template interpolator function
webgen--interpolate-template
to generate a template with the inputs
filled in:
(let* ((example-template (webgen--parse-string (cadr (org-babel-lob--src-info "example-template")))) (example-input (webgen--parse-string (cadr (org-babel-lob--src-info "example-input")))) (tags (webgen--get-tag-map example-input))) ;; Add in the title and the date ;; Note: in this example they're hardcoded, but a real blog post ;; generator would pull them from the post somehow. (setf (alist-get 'title tags) "some title") (setf (alist-get 'date tags) "some date") ;; interpolate the template and return it as a string (org-element-interpret-data (webgen--interpolate-template example-template tags)))
* some title The date: some date The content: This is the blog content.
Directory structure
The templating engine is generic and doesn't really know about files (except when it exports to HTML), blogs, or indexes.
However, the static site generator uses .org files to store templates and template inputs. The directories that are used are arbitrary and configurable
|- blog |- post1.org |- post2.org |- post3.org |- common |- blog-template.org |- home-template.org |- homepage |- index.org |- about.org |- elisp |- webgen.el |- Makefile |- flake.nix |- ...other stuff...
Directories that the generator will manipulate
These variables can be overriden with dynamic scoping. That's how the CLI macro works.
(defvar webgen-blog-directory "./blog" "The directory where blog files are found.") (defvar webgen-homepage-directory "./homepage" "The directory where blog files are found.") (defvar webgen-common-directory "./common" "The directory where template or other common files are found.")
A regular expression to match content file names
(defconst webgen-file-name-regexp "^[^\\._].+\\.org$" "Regexp matching any content file name") (defun webgen--get-blog-files () (directory-files webgen-blog-directory t webgen-file-name-regexp))
Convenience functions to get file paths
This file path would be (webgen--get-blog-file "blogging-with-org")
.
(defun webgen--get-blog-file (basename &rest args) "Get the path to a blog file named BASENAME, relative to `webgen-blog-directory'. BASENAME should not have any extension like .org." (concat (file-name-as-directory webgen-blog-directory) basename "." (or (plist-get args :ext) "org"))) (defun webgen--get-common-file (basename &rest args) "Get the path to a common file named BASENAME, relative to `webgen-common-directory'. BASENAME should not have any extension like .org." (concat (file-name-as-directory webgen-common-directory) basename "." (or (plist-get args :ext) "org"))) (defun webgen--get-homepage-file (basename &rest args) "Get the path to a homepage file named BASENAME, relative to `webgen-homepage-directory'. BASENAME should not have any extension like .org." (concat (file-name-as-directory webgen-homepage-directory) basename "." (or (plist-get args :ext) "org")))
Blog post generator
To generate blog posts, we just need to add a little more to the example interpolation function. Specifically, some way of enumerating files and extracting metadata from them to generate blog posts and lists of blog posts.
Also a macro to set dynamically scoped variables
webgen-blog-directory
and webgen-common-directory
and others from
command-line arguments. Since this code is all executed in emacs batch
mode, this is one of the few reliable ways to get input from the user.
(defmacro webgen--read-common-cli-args (&rest body) `(let ((webgen-blog-directory (or (pop command-line-args-left) webgen-blog-directory)) (webgen-homepage-directory (or (pop command-line-args-left) webgen-homepage-directory)) (webgen-common-directory (or (pop command-line-args-left) webgen-common-directory))) ,@body))
The other way to get input is by using emacs --batch --eval
"expression"
but this method requires careful escaping of double
quotes that I don't really want to deal with. I can push that problem
off to my shell by reading from the command-line arguments.
The following function is the entry point for blog generation.
It looks in the blog directory for name.org
and looks in the
template directory for blogpost.org
. Then, it gets the metadata of
the post name.org
and makes a tag table with them. Then, it calls
the template interpolator.
(defun webgen-blog-generate (&optional metadata prev next) (webgen--read-common-cli-args (let* ((name (if (null metadata) (pop command-line-args-left) (blog-metadata-name metadata))) (main-file (webgen--get-blog-file name)) (template-file (webgen--get-common-file "blogpost")) (output-file (webgen--get-blog-file name :ext "html")) (main-data (webgen--parse-file main-file)) (template-data (webgen--parse-file template-file)) (sorted-metadatas (apply #'vector (webgen--get-sorted-metadatas-cached))) (num-posts (length sorted-metadatas)) (index (cl-find name (number-sequence 0 (- num-posts 1)) :test #'string-equal :key (lambda (x) (blog-metadata-name (aref sorted-metadatas x))))) (metadata (or metadata (aref sorted-metadatas index))) ;; Get the next and previous posts. Note that the indexes ;; seem flipped because the posts are sorted in descending ;; date order. (next (or next (if (= index 0) nil (aref sorted-metadatas (- index 1))))) (prev (or prev (if (= index (- num-posts 1)) nil (aref sorted-metadatas (+ index 1))))) (date (blog-metadata-date metadata)) (title (blog-metadata-title metadata)) (cats (blog-metadata-cats metadata)) (prevnext (with-temp-buffer (org-mode) (unless (null prev) (insert (format "[[./%s.html][[Prev: %s] ]]\n" (blog-metadata-name prev) (blog-metadata-title prev)))) (unless (null next) (insert (format "[[./%s.html][[Next: %s] ]]\n" (blog-metadata-name next) (blog-metadata-title next)))) (org-element-parse-buffer))) (extra-tags (list (cons 'date (format-time-string "%Y-%m-%d" date)) (cons 'title title) (cons 'prevnext prevnext) (cons 'cats (if (null cats) "no categories" (with-temp-buffer (cl-loop for cat in cats do (insert " " (webgen--relative-link (format "cat-%s" cat) cat))) (org-element-parse-buffer))))))) (webgen--interpolate-and-export main-data template-data extra-tags output-file))))
Here is the data structure that will be used to store blog post metadata.
(cl-defstruct blog-metadata "Information about a single blog post." (name nil :documentation "A short identifier for the blog post") (date nil :documentation "The date of publication") (title nil :documentation "The title of the blog post") (cats nil :documentation "The categories to which this blog post belongs") (mdate nil :documentation "The last modification date"))
It can be constructed with (make-blog-metadata :name "name" :date
...)
Here is how metadata is extracted from a blog post.
(defun webgen--get-buffer-metadata (name) (let* ((date (cadar (org-collect-keywords '("DATE")))) (mdate (cadar (org-collect-keywords '("MDATE")))) (title (cadar (org-collect-keywords '("TITLE")))) (cats (cadar (org-collect-keywords '("TAGS")))) (parsed-date (org-read-date nil t date nil)) (parsed-mdate (if (null mdate) parsed-date (org-read-date nil t mdate nil)))) (make-blog-metadata :name name :date parsed-date :mdate parsed-mdate :title title :cats (if (null cats) cats (string-split cats " "))))) (defun webgen--get-file-metadata (file-name &optional name) (with-temp-buffer (org-mode) (insert-file-contents file-name) (webgen--get-buffer-metadata (or name (file-name-base file-name))))) (defun webgen--get-metadata (name) (webgen--get-file-metadata (webgen--get-blog-file name) name))
For example, the following blog post in example.org
:
#+TITLE: blog post title #+DATE: <2021-01-17> Hello world.
has the metadata:
;; fake dates, but it's two numbers #s(blog-metadata "example" (1234 234) (1254 12) "blog post title" ("cat1" "cat2"))
Blog index generator
The following code is for generating various lists of posts. It may be
all posts, posts under a certain category (#+TAGS: category
), or in
the future posts authored by a certain individual.
To implement this, we will need a way to get a date-sorted list of blog posts' metadata.
(defun webgen--get-sorted-metadatas () (let ((metadatas (cl-loop for file in (webgen--get-blog-files) collect (webgen--get-file-metadata file)))) ;; Warning: This cannot possibly be a correct comparison ;; function as it ignores the timezones. (sort metadatas (lambda (x y) (> (car (blog-metadata-date x)) (car (blog-metadata-date y))))))) (defun webgen-blog-generate-sorted-metadata () (with-temp-buffer (insert (pp-to-string (webgen--get-sorted-metadatas))) (write-file (webgen--get-blog-file "metadata" :ext "dat")))) (defun webgen--get-sorted-metadatas-cached () (with-current-buffer (find-file-noselect (webgen--get-blog-file "metadata" :ext "dat")) (read (buffer-string))))
We will also need a function to get a mapping of categories to posts that fall under that category:
(defun webgen--get-cats-table () (let ((cats-table nil)) (cl-loop for metadata in (webgen--get-sorted-metadatas-cached) for name = (blog-metadata-name metadata) for cats = (blog-metadata-cats metadata) unless (null cats) do (cl-loop for cat in cats do (push name (alist-get cat cats-table)))) cats-table))
Convenience method for generating relative links to posts:
(defun webgen--relative-link (basename &optional displayname) (format "[[./%s.html][%s]]" basename (or displayname basename)))
To generate the index, we create a temporary buffer, turn on org-mode, and for each metadata in the sorted list that matches the category filter, output a single table row.
This function generates the template input and immediately
interpolates it into the blogindex
template.
(defun webgen--gen-index (name title &optional cat-filter) (with-temp-buffer (org-mode) (insert "* index :indextable:\n") (cl-loop for metadata in (webgen--get-sorted-metadatas-cached) for basename = (blog-metadata-name metadata) for date = (blog-metadata-date metadata) for title = (blog-metadata-title metadata) for cats = (blog-metadata-cats metadata) if (or (null cat-filter) (member cat-filter cats)) do (let* ((date-string (format-time-string "%Y-%m-%d" date)) (basename-link (webgen--relative-link basename)) (entry-format "| %s | %s | %s |\n")) (insert (format entry-format basename-link date-string title)))) ;; Interpolate and export to HTML. (webgen--interpolate-and-export (org-element-parse-buffer) (webgen--parse-file (webgen--get-common-file "blogindex")) `((title . ,title)) (webgen--get-blog-file name :ext "html"))))
And finally, the entry point: a function that generates an index for all posts and one for each category:
(defun webgen-blog-gen-all-indices () (webgen--read-common-cli-args ;; The main index (webgen--gen-index "index" "All posts") (let ((cats-table (webgen--get-cats-table))) ;; One index page for each category (cl-loop for (cat . _) in cats-table do (webgen--gen-index (format "cat-%s" cat) (format "Posts under category: %s" cat) cat)))))
Homepage generator
I also need a home page. This would have a nav bar which would have links to my blog, my about page, my resume, and whatever else.
(defun webgen-homepage-generate (&optional name) (webgen--read-common-cli-args (let* ((name (or name (pop command-line-args-left))) (template-data (webgen--parse-file (webgen--get-common-file "homepage"))) (input-data (webgen--parse-file (webgen--get-homepage-file name)))) (webgen--interpolate-and-export input-data template-data nil (webgen--get-homepage-file name :ext "html")))))
RSS feed generator
The RSS feed is a XML document that follows the RSS spec.
(defconst webgen-rss-date-format "%a, %d %b %Y %T %z")
Generate an RSS item object for a single blog post. Note that no XML
conversion happens yet– that will be done at the end with a call to
esxml-to-xml
.
(defun webgen--blog-post-rss-item (metadata) (let* ((name (blog-metadata-name metadata)) (title (blog-metadata-title metadata)) (date (blog-metadata-date metadata)) (link (concat "https://skulk.org/blog/" name ".html"))) `(item () (title () (raw-string ,title)) (description () (raw-string "A blog post")) (link () (raw-string ,link)) (pubDate () (raw-string ,(format-time-string webgen-rss-date-format date))) (guid () (raw-string ,link)))))
The entire channel is generated by settings some blog-level attributes, then adding all of the items generated by the previous function. This function takes advantage of the sorted-ness of the list to get the date of the first blog post.
(defun webgen--blog-post-rss-channel () (let* ((sorted-metadata (webgen--get-sorted-metadatas-cached)) (first-post (car (last sorted-metadata))) (pub-date (blog-metadata-date first-post))) `(channel () (title () (raw-string "Sid's blog")) (language () (raw-string "en-us")) (link () (raw-string "https://skulk.org/blog")) (description () (raw-string "A blog about software (and possibly other things)")) (generator () (raw-string "a-janky-elisp-static-site-generator")) (webMaster () (raw-string "kulkarnisidharth1@gmail.com (Sid Kulkarni)")) (managingEditor () (raw-string "kulkarnisidharth1@gmail.com (Sid Kulkarni)")) (pubDate () (raw-string ,(format-time-string webgen-rss-date-format pub-date))) (atom:link ((href . "https://skulk.org/blog/rss.xml") (rel . "self") (type . "application/rss+xml")) ()) ,@(mapcar #'webgen--blog-post-rss-item sorted-metadata))))
Now, pull it all together and write it to the output file.
(defun webgen-blog-generate-rss () (with-temp-buffer (insert (esxml-to-xml `(rss ((version . "2.0") (xmlns:atom . "http://www.w3.org/2005/Atom")) ,(webgen--blog-post-rss-channel)))) (write-file (webgen--get-blog-file "rss" :ext "xml"))))
Sidenote: scripting in org-mode
Writing elisp code that deals with org-mode can be slightly daunting. I was able to get a good grasp on it by writing a lot of stuff like
(require 'org-element) (with-temp-buffer ;; Start a trivial org-mode document (insert "* test heading") ;; Dump to string (pp-to-string ;; Parse the buffer into the org-mode document datastructure (org-element-parse-buffer)))
The result looks like the following:
(org-data (:standard-properties [1 1 1 15 15 0 nil org-data nil nil nil 3 15 nil #<buffer *temp*-827896> nil nil nil] :path nil :CATEGORY nil) (headline (:standard-properties [1 1 nil nil 15 0 (:title) first-section nil nil nil nil nil 1 #<buffer *temp*-827896> nil nil #0] :pre-blank 0 :raw-value "test heading" :title (#("test heading" 0 12 (:parent #1))) :level 1 :priority nil :tags nil :todo-keyword nil :todo-type nil :footnote-section-p nil :archivedp nil :commentedp nil)))
By reading a lot of these, as well as the org-element manual, I was able to cobble together a very simple templating engine.
Styling code blocks
To get org-src source blocks to export with each lexical element labeled with a CSS class, I'm using htmlize.
For a long time, it only generated weird black-and-white syntax coloring that didn't make any sense. I eventually found this stackoverflow question for which the top answer mentions that
- you have to generate the CSS separately if you're running in batch mode,
- and because
org-html-htmlize-generate-css
looks at the global font-lock configuration to figure out which CSS to generate, and since emacs in batch mode doens't even use font-lock, you have to trick it into using the proper fonts.
Here is the function for generating the CSS file:
(defun webgen-generate-css () (require 'font-lock) (advice-add 'face-attribute :override #'my-face-attribute) (org-html-htmlize-generate-css) (with-current-buffer "*html*" (write-file (webgen--get-blog-file "syntax" :ext "css"))))
Note the advice-add
– the idea is to replace face-attribute
(which htmlize
uses) with a different version that works in batch
mode. I'm not entirely sure why it looks for the highest lowest color
in a face, but it works so I'm not going to touch it for now.
Here is how the replacement, my-face-attribute
is defined.
Most of this code is copy/pasted from the stackoverflow answer, but I
had to change one thing to get around an error. I left a comment
describing the nature of the issue in the code (in the
my-face-attribute
function).
(unless (boundp 'maximal-integer) (defconst maximal-integer (lsh -1 -1) "Maximal integer value representable natively in emacs lisp.")) (defun face-spec-default (spec) "Get list containing at most the default entry of face SPEC. Return nil if SPEC has no default entry." (let* ((first (car-safe spec)) (display (car-safe first))) (when (eq display 'default) (list (car-safe spec))))) (defun face-spec-min-color (display-atts) "Get min-color entry of DISPLAY-ATTS pair from face spec." (let* ((display (car-safe display-atts))) (or (car-safe (cdr (assoc 'min-colors display))) maximal-integer))) (defun face-spec-highest-color (spec) "Search face SPEC for highest color. That means the DISPLAY entry of SPEC with class 'color and highest min-color value." (let ((color-list (cl-remove-if-not (lambda (display-atts) (when-let ((display (car-safe display-atts)) (class (and (listp display) (assoc 'class display))) (background (assoc 'background display))) (and (member 'light (cdr background)) (member 'color (cdr class))))) spec))) (cl-reduce (lambda (display-atts1 display-atts2) (if (> (face-spec-min-color display-atts1) (face-spec-min-color display-atts2)) display-atts1 display-atts2)) (cdr color-list) :initial-value (car color-list)))) (defun face-spec-t (spec) "Search face SPEC for fall back." (cl-find-if (lambda (display-atts) (eq (car-safe display-atts) t)) spec)) (defun my-face-attribute (face attribute &optional frame inherit) "Get FACE ATTRIBUTE from `face-user-default-spec' and not from `face-attribute'." (let* ((face-spec (face-user-default-spec ;; This used to be just `face' but sometimes that ;; ends up not being a symbol. Taking the car of ;; it is a guess. (if (consp face) (car face) face))) (display-attr (or (face-spec-highest-color face-spec) (face-spec-t face-spec))) (attr (cdr display-attr)) (val (or (plist-get attr attribute) (car-safe (cdr (assoc attribute attr)))))) ;; (message "attribute: %S" attribute) ;; for debugging (when (and (null (eq attribute :inherit)) (null val)) (let ((inherited-face (my-face-attribute face :inherit))) (when (and inherited-face (null (eq inherited-face 'unspecified))) (setq val (my-face-attribute inherited-face attribute))))) ;; (message "face: %S attribute: %S display-attr: %S, val: %S" face attribute display-attr val) ;; for debugging (or val 'unspecified)))
How this blog gets built
The exact details of how this blog goes from this org-mode file to a bunch of HTML files is out of scope. It involves Nix derivations and makefiles, but the details are truly not interesting.
Roughly, what happens is:
- This file gets processed by Emacs by loading it and running
M-x org-babel-tangle
. This spits out a file calledwebgen.el
- Emacs is called again with
webgen.el
and the functions defined above are called to generate the blog posts, indices, homepages, rss feeds, etc. This is typically done by a Makefile. - The resulting
.html
,.css
,.xml
and other output files are copied to the actual web hosting directory.
Test cases
Interpolation test cases
(defun webgen--get-src-block (name) (cadr (alist-get name org-babel-library-of-babel))) (defun webgen--interpolation-test (template-src-name input-src-name extra-tags expected-src-name &optional just-write-outputs) (org-babel-lob-ingest) (let* ((template-data (webgen--parse-string (webgen--get-src-block template-src-name))) (input-data (webgen--parse-string (webgen--get-src-block input-src-name))) (expected (webgen--get-src-block expected-src-name)) (tags (append extra-tags (webgen--get-tag-map input-data))) (result (org-element-interpret-data (webgen--interpolate-template template-data tags)))) (if just-write-outputs (print result) (if (string= expected result) (print "passed") (set-buffer (get-buffer-create "*webgen-test-expected*" t)) (erase-buffer) (insert expected) (set-buffer (get-buffer-create "*webgen-test-result*" t)) (erase-buffer) (insert result) (diff (get-buffer "*webgen-test-expected*") (get-buffer "*webgen-test-result*"))))))
Test template
* content \var{symbol}
\var{symbol}
\var{paragraph}
* header ** nested header *** even more nested header :paragraph: this is some paragraph **** It goes even deeper.
* content value value this is some paragraph * It goes even deeper.
(webgen--interpolation-test
'test-template-1
'test-input-1
'((symbol . "value"))
'test-expected-1)
#<window 35 on *Diff*>
[Blog Index] [RSS] [Prev: First post] [Next: Developing Java applications with Emacs using Nix]