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.
This article © 2024 by Christopher Howard is licensed under Attribution-ShareAlike 4.0 International.
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.