Converting Org Links to Gemini Links in Any Buffer (publ. 2024-12-13)
I like writing my gemlog posts directly in gemtext format, rather than writing them in Org format, and then converting to Gemini, like some people do. Gemtext has a pleasant, simple markup that is natural to write in, and that looks good in Gemini mode.
Sometimes though, especially when writing up my "News and Links Digests", I copy and paste a lot of Org Links from my references collection, which is an Org document.
One trick would be to convert the Org document over to gemini, using emacs-ox-gemini, and then copy the links out of there, or some slight variation of that. But I didn't like the extra steps involved in that approach.
Another approach I tried was writing a function that used replace-regexp. This generally worked, but it relied on regular expressions. The problem with that is that the Org link format uses (potentially) escaped characters, since the ?\[, ?\], and ?\\ characters ("[", "]", and "\") might potentially need to be included in the link string or link description string. Regular expressions do not handle escaped characters very well. See this thread:
Regular expressions and user-escaped characters
I just wasn't happy with the knowledge that potentially my conversion function could choke on some valid Org link line. So over a few lunchbreaks, I came up with an approach that parses like a state machine. It has been a while since my computer science classes, so I'm not quite sure if technically it is a state machine, but I think it works like one. Here is the core function that does the actually parsing:
(defun decompose-org-link--with-state (str)
"The core functionality of `decompose-org-link'. Separated out for debugging \
purposes."
(t-transduce
(t-scan
(lambda (accum c)
(cl-flet ((malformed () (throw 'malformed t)))
(let ((link (cl-first accum))
(desc (cl-second accum))
(mode (cl-third accum))
(escaping (cl-fourth accum)))
(cl-case c
(?\[ (if (not escaping)
(cond ((equal mode :empty-start)
(list link desc :inner-boxes-start nil))
((equal mode :inner-boxes-start)
(list link desc :link nil))
((equal mode :inner-boxes-middle)
(list link desc :desc nil))
(t (malformed)))
(cond ((equal mode :link)
(list (concat link (string c)) desc mode nil))
((equal mode :desc)
(list link (concat desc (string c)) mode nil))
(t (malformed)))))
(?\] (if (not escaping)
(cond ((equal mode :link)
(list link desc :inner-boxes-middle nil))
((equal mode :desc)
(list link desc :inner-boxes-end nil))
((or (equal mode :inner-boxes-middle)
(equal mode :inner-boxes-end))
(list link desc :done nil))
(t (malformed)))
(cond ((equal mode :link)
(list (concat link (string c)) desc mode nil))
((equal mode :desc)
(list link (concat desc (string c)) mode nil))
(t (malformed)))))
(?\\ (if (not escaping)
(cond ((or (equal mode :link) (equal mode :desc))
(list link desc mode :escaping))
(t (malformed)))
(cond ((equal mode :link)
(list (concat link "\\") desc mode nil))
((equal mode :desc)
(list link (concat desc "\\") mode nil))
(t (malformed)))))
(t (if escaping
(cond ((equal mode :link)
(list (concat link "\\" (string c)) desc mode nil))
((equal mode :desc)
(list link (concat desc "\\" (string c)) mode nil))
(t (malformed)))
(cond ((equal mode :link)
(list (concat link (string c)) desc mode nil))
((equal mode :desc)
(list link (concat desc (string c)) mode nil))
(t (malformed)))))))))
'("" "" :empty-start nil))
#'t-last
str))
There is definitely room for optimization there. But it works fast enough for human use. I imagine there is a better approach that just using "concat" and "string" to append a character to the end of a string, but I haven't looked into it yet. And I think there are a few places where logic could be simplified.
Here is a more convenient wrapper function for that:
(defun decompose-org-link (str)
"Takes an org link, as a string, separates out the link and the \
description, and returns them in a list of two elements. The string \
must be in valid org link format and must not include any extra \
characters before or after the link. A 'malformed condition will be \
thrown if these criteria are not met."
(let ((state (decompose-org-link--with-state str)))
(if (not (equal (third state) :done))
(throw 'malformed t)
(list (car state) (cadr state)))))
Here is the function that applies the parsing code to each line in the buffer:
(defun org-to-gemini-links-in-buffer ()
"Converts all org links in a buffer to gemini links. However, to be \
converted, the org link must be the only text on a line other than \
whitespace and eol characters, that is, content that would be trimmed \
off by `string-trim-right'."
(interactive)
(goto-char (point-min))
(while (not (eql (point) (point-max)))
(let* ((line (buffer-substring
(point)
(pos-eol)))
(stripped-line (string-trim-right line))
(eol-chars (substring line (length stripped-line))))
(catch 'malformed
(let* ((results (decompose-org-link stripped-line))
(link (cl-first results))
(desc (cl-second results)))
(delete-region (point) (pos-eol))
(insert (concat "=> " link " " desc) eol-chars))))
(forward-line)))
So, how I use this is to just copy and paste org links into my gemlog post, whereever I want them. And then right before publishing, I run M-x org-to-gemini-links-in-buffer, in the same buffer as my gemlog post.
Copyright
This article © 2024 by Christopher Howard is licensed under Attribution-ShareAlike 4.0 International.
CC BY-SA 4.0 Deed
The elisp code in this article is © 2024 by Christopher Howard, and is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
The elisp code in this article is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
Licenses