This is still the header! Main site

Writing HTML in Markdown but actually not

2025/02/13

I recently started using Obsidian the note-taking app. (It's likely that there will be a post on it at some point.) One of the nice things about it that is somewhat hard to overlook is... it's a lot easier to inject links and formatting, compared to our approach of writing HTML in HTML. So... can we do better?

Well, as it turns out, we can!

Links

The fancier success story is the automatic post inserter, whose development consisted of

          here is our task.

We're looking at an rss feed! here is the converted xml:

((feed ((xml:lang . "en") (xmlns . "http://www.w3.org/2005/Atom")) "
    " (link ((href . "https://simonsafar.com/index_list.xml") (rel . "self"))) "
    " (link ((href . "https://simonsafar.com") (rel . "alternate") (type . "text/html") (hreflang . "en"))) "
    " (title nil "Simon Safar") "
    " (subtitle nil "All recent entries from simonsafar.com") "
    " (id nil "https://simonsafar.com/index_list.xml") "
    " (updated nil "2025-02-02T14:29:25.000016-08:00") "
    " (entry nil "
        " (title ((type . "html")) "Against Against Windows") "
        " (link ((href . "https://simonsafar.com/2025/against_against_windows/"))) "
        " (id nil "https://simonsafar.com/2025/against_against_windows/") "
        " (author nil "
            " (name nil "Simon Safar") "
        ") "
        " (published nil "2025-02-01T16:00:00.000000-08:00") "
        " (updated nil "2025-02-01T16:00:00.000000-08:00") "
    ") "
    " (entry nil "
        " (title ((type . "html")) "Why Passwords Still Even") "
        " (link ((href . "https://simonsafar.com/2025/passwords_still/"))) "
        " (id nil "https://simonsafar.com/2025/passwords_still/") "
        " (author nil "
            " (name nil "Simon Safar") "
        ") "
        " (published nil "2025-01-21T16:00:00.000000-08:00") "
        " (updated nil "2025-01-21T16:00:00.000000-08:00") "
    ") "

          
.. and it continues like that. (we... probably shouldn't care about all the stupid newlines.)
It's sitting in the buffer named index.xml.

Can we:
* parse up this buffer
* extract pairs of post hrefs and titles
* remove the server name part, leaving just /2025/etc
* launch ido-completing-read on it so that users can pick based on sub-strings
* insert an actual link with the uri and the text into the current buffer once done.

What came out of this was basically doing the job perfectly, with only tiny adjustments needed. (See code in the appendix.)


Formatting

A possible way to do this is in post-self-insert-hook; this is a function that runs after each "normal" key you hit. (Normally they just run the command self-insert-command, which looks at what key it was invoked by & inserts it. Not even typing letters into a buffer skips the normal Emacs way of going through Lisp!)

(defun htmlize-md--post-self-insert ()
  "Function to run after self-insert-command."
  (when (and (memq last-command-event '(?* ?\] ?\n ?\s ?_ ?`))
             (not (looking-back (rx "*" (not "*"))
                              (max (- (point) 2) (point-min)))))
    (htmlize-md--convert-syntax)))
          

And then... you can match regexes on what you have seen before, replacing them with their HTML versions:

(defun htmlize-md--convert-syntax ()
  "Convert markdown-like syntax to HTML elements.
Tries different patterns in sequence and stops at the first match."
  (interactive)
  (let ((line-start (line-beginning-position))
        (line-end (line-end-position)))
    (cond
     ;; Don't process unfinished emphasis
     ((looking-back (rx "**" (one-or-more (not "*")) "*"))
      t)

     ;; Emphasis: **text** -> text
     ((looking-back (rx "**"
                        (group (one-or-more (not "*")))
                        "**")
                    line-start)
      (let ((text (match-string 1)))
        (replace-match (concat "" text "") t t)))

     ;; Italic: *text* -> text
     ((looking-back (rx "*"
                       (group (one-or-more (not "*")))
                       "*")
                   line-start)
      (let ((text (match-string 1)))
        (replace-match (concat "" text "") t t)))
; ... etc
          

This has also been mostly written by Claude Sonnet; it had some trouble though with the part where if you have two stars to open a bold, emphasized, surrounded-by-two-stars block, you shouldn't jump once you see one star to close it (as *italics*); also, keeping in your head where you're supposed to have the cursor at in the buffer is not one of their strengths, apparently.

(Might become my benchmark question for upcoming models!)

If you just skipped reading the code (which... I often do in such cases): one of the cool parts is rx, the regex translator library. You can write stuff like

(rx "*"
    (group (one-or-more (not "*")))
    "*")
          

instead of


"\\*\\([^*]+\\)\\*"
          

(I will also refrain commenting on readability & urge the reader to assess instead.)

Appendix

The post inserter thing


(defun ssafar-insert-blog-link ()
  "Parse blog entries from XML buffer and let user select one to insert as a link."
  (interactive)
  (let* ((xml-data (xml-parse-file "h:/radiance_dev/index_list.xml"))
         (feed (car xml-data))
         (entries (xml-get-children feed 'entry))
         ;; Create alist of (title . href) pairs
         (entry-pairs (mapcar (lambda (entry)
                               (let* ((title (car (last (car (xml-get-children entry 'title)))))
                                      (href (cdr (assq 'href 
                                                     (xml-node-attributes 
                                                      (car (xml-get-children entry 'link))))))
                                      ;; Strip the domain part
                                      (path (replace-regexp-in-string "https://simonsafar\\.com" "" href)))
                                 (cons title path)))
                             entries))
         ;; Use ido to select an entry
         (selected-title (ido-completing-read "Choose post: " 
                                            (mapcar #'car entry-pairs)))
         ;; Get the corresponding href
         (selected-href (cdr (assoc selected-title entry-pairs))))
    ;; Insert markdown link at point
    (insert (format "%s" selected-href selected-title))))
          

... how you I make that video?

ScreenToGif; it's really cool. (Being able to simply drop a "video" tag into a blog post is also a benefit of this HTML.)

Also, the entire thing is a 200 kB h264 video. Actual GIFs of this length would probably take at least megabytes. Isn't modern video compression nice?