💾 Archived View for librehacker.com › gemlog › starlog › 20241213-1.gmi captured on 2024-12-17 at 10:17:35. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

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