2020-11-12 07:35:20 +00:00
|
|
|
;;; ox-gemini.el --- Output gemini formatted documents from org-mode -*- lexical-binding: t; -*-
|
|
|
|
|
|
|
|
;; Author: Justin Abrahms <justin@abrah.ms>
|
2023-05-14 11:02:10 +00:00
|
|
|
;; Maintainer: Lucien Cartier-Tilet <lucien@phundrak.com>
|
|
|
|
;; URL: https://labs.phundrak.com/phundrak/ox-gemini
|
|
|
|
;; Version: 0.2.0
|
|
|
|
;; Keywords: lisp org-mode
|
2020-11-12 07:35:20 +00:00
|
|
|
;; Package-Requires: ((emacs "26.1"))
|
2020-11-25 17:03:23 +00:00
|
|
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
2020-11-12 07:35:20 +00:00
|
|
|
|
2023-05-14 11:02:10 +00:00
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
|
|
|
|
;; This program 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.
|
|
|
|
|
|
|
|
;; This program 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.
|
|
|
|
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2020-11-12 07:35:20 +00:00
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
;;
|
|
|
|
;; There's a web-alternative that's similar to the gopher protocol
|
|
|
|
;; named 'gemini'. You can find more about it at
|
|
|
|
;; https://gemini.circumlunar.space/ This package serves as an
|
|
|
|
;; org-mode export backend in order to build those types of
|
|
|
|
;; document-oriented sites.
|
|
|
|
|
2020-11-08 00:19:04 +00:00
|
|
|
(require 'ox)
|
|
|
|
(require 'ox-publish)
|
|
|
|
(require 'ox-ascii)
|
|
|
|
(require 'cl-lib)
|
2022-01-10 14:58:50 +00:00
|
|
|
(require 'url-util)
|
2020-11-08 00:19:04 +00:00
|
|
|
|
2020-11-12 07:35:20 +00:00
|
|
|
;;; Code:
|
|
|
|
|
2020-11-08 00:19:04 +00:00
|
|
|
(org-export-define-derived-backend 'gemini 'ascii
|
|
|
|
:menu-entry
|
|
|
|
'(?g "Export to Gemini"
|
|
|
|
((?b "To buffer"
|
2021-08-13 15:09:02 +00:00
|
|
|
(lambda (a s v b)
|
|
|
|
(org-gemini-export-to-buffer a s v b nil)))
|
|
|
|
(?f "To file"
|
|
|
|
(lambda (a s v b)
|
|
|
|
(org-gemini-export-to-file a s v b nil)))))
|
2022-02-04 14:51:00 +00:00
|
|
|
:translate-alist '((quote-block . org-gemini-quote-block)
|
|
|
|
(code . org-gemini-code-inline)
|
2022-01-16 03:40:58 +00:00
|
|
|
(export-block . org-gemini-export-block)
|
2021-08-11 13:52:51 +00:00
|
|
|
(paragraph . org-gemini-paragraph)
|
2021-08-13 15:09:02 +00:00
|
|
|
(headline . org-gemini-headline)
|
|
|
|
(link . org-gemini-link)
|
|
|
|
(section . org-gemini-section)
|
|
|
|
(src-block . org-gemini-code-block)
|
2020-11-08 05:53:56 +00:00
|
|
|
(item . org-gemini-item)
|
2022-02-04 18:54:19 +00:00
|
|
|
(template . org-gemini-template)
|
|
|
|
(table . org-gemini-table)))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
2021-08-11 13:52:51 +00:00
|
|
|
(defun org-gemini-paragraph (_paragraph contents _info)
|
|
|
|
"CONTENTS is the text of the paragraph."
|
2021-08-17 09:04:17 +00:00
|
|
|
(concat (replace-regexp-in-string "\n" " " contents)
|
2021-08-11 13:52:51 +00:00
|
|
|
"\n"))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
2022-02-04 14:51:00 +00:00
|
|
|
(defun org-gemini-item (item contents info)
|
2022-04-18 14:33:45 +00:00
|
|
|
"Generate a Gemtext item from the org CONTENTS.
|
|
|
|
CONTENTS is the text of the individual item.
|
|
|
|
ITEM is the parsed-org element with all properties."
|
2022-02-04 14:51:00 +00:00
|
|
|
(concat "* "
|
2022-02-04 18:54:19 +00:00
|
|
|
;; vv Code from ox-md! vv
|
2023-05-14 10:21:38 +00:00
|
|
|
(pcase (org-element-property :checkbox item)
|
|
|
|
(`on "[X] ")
|
|
|
|
(`trans "[-] ")
|
|
|
|
(`off "[ ] "))
|
|
|
|
(let ((tag (org-element-property :tag item)))
|
|
|
|
(and tag (format "%s :: " (org-export-data tag info))))
|
2022-02-04 18:54:19 +00:00
|
|
|
;; ^^ ^^
|
2022-02-04 14:51:00 +00:00
|
|
|
contents))
|
2020-11-08 05:53:56 +00:00
|
|
|
|
2021-01-02 15:17:55 +00:00
|
|
|
(defun org-gemini-quote-block (_input contents _info)
|
|
|
|
"CONTENTS is the text of the quote."
|
2022-01-10 06:33:40 +00:00
|
|
|
(replace-regexp-in-string
|
|
|
|
"^" "> "
|
|
|
|
(replace-regexp-in-string "\n\\'" "" contents)))
|
2021-01-02 15:17:55 +00:00
|
|
|
|
2020-11-25 16:55:19 +00:00
|
|
|
(defun org-gemini-code-inline (input _contents info)
|
2022-04-18 14:33:45 +00:00
|
|
|
"Generate an inline code in Gemtext from the parsed INPUT.
|
2023-05-11 09:23:11 +00:00
|
|
|
INPUT is either a \\='src-block\\=' or \\='example-block\\='
|
|
|
|
element. INFO is a plist."
|
2020-11-08 00:19:04 +00:00
|
|
|
;; there's a bug here where there's a trailing space in the ``
|
2023-05-14 11:34:37 +00:00
|
|
|
(format "`%s`" (replace-regexp-in-string (rx (* whitespace) eot)
|
|
|
|
""
|
|
|
|
(org-export-format-code-default input info))))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
2022-01-10 14:58:14 +00:00
|
|
|
(defun org-gemini-code-block (src-block _contents info)
|
|
|
|
"SRC-BLOCK is a codeblock. INFO is a plist."
|
2022-01-15 23:49:52 +00:00
|
|
|
(let ((name (or (caaar (org-element-property :caption src-block))
|
|
|
|
(org-element-property :name src-block))))
|
2022-01-10 14:58:14 +00:00
|
|
|
(org-remove-indentation
|
|
|
|
(format "```%s\n%s```"
|
|
|
|
(or name "")
|
|
|
|
(org-export-format-code-default src-block info)))))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
2020-11-25 16:55:19 +00:00
|
|
|
(defun org-gemini--describe-links (links _width info)
|
2020-11-25 17:03:30 +00:00
|
|
|
"Describe links is the footer-portion of the link data.
|
|
|
|
|
2022-01-16 03:40:58 +00:00
|
|
|
It's output just before each section. LINKS is a list of each link.
|
|
|
|
INFO is a plist."
|
2021-08-11 08:04:33 +00:00
|
|
|
(concat
|
|
|
|
(mapconcat
|
|
|
|
(lambda (link)
|
|
|
|
(let* ((raw-path (org-element-property :raw-link link))
|
|
|
|
(link-type (org-element-property :type link))
|
|
|
|
(is-org-file-link (and (string= "file" link-type)
|
|
|
|
(string= ".org" (downcase (file-name-extension raw-path ".")))))
|
|
|
|
(path (if is-org-file-link
|
|
|
|
(concat (file-name-sans-extension (org-element-property :path link)) ".gmi")
|
|
|
|
raw-path))
|
2021-08-13 15:09:02 +00:00
|
|
|
(desc (org-element-contents link))
|
2023-05-14 11:02:10 +00:00
|
|
|
(anchor (replace-regexp-in-string (regexp-quote "\n")
|
|
|
|
" "
|
|
|
|
(org-export-data
|
|
|
|
(or desc (org-element-property :raw-link link))
|
|
|
|
info))))
|
2022-01-10 14:58:50 +00:00
|
|
|
(format "=> %s %s\n" (url-encode-url path) anchor)))
|
2021-08-11 08:04:33 +00:00
|
|
|
links "")
|
|
|
|
(when (car links)
|
|
|
|
"\n")))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
|
|
|
|
2020-11-25 16:55:19 +00:00
|
|
|
(defun org-gemini-link (_link desc _info)
|
2020-11-12 07:35:20 +00:00
|
|
|
"Simple link generation.
|
|
|
|
|
2020-11-25 17:03:30 +00:00
|
|
|
DESC is the link text
|
|
|
|
|
2020-11-12 07:35:20 +00:00
|
|
|
Note: the footer with the actual links are handled in
|
2021-08-16 16:49:47 +00:00
|
|
|
`org-gemini--describe-links'."
|
2020-11-08 00:19:04 +00:00
|
|
|
(if (org-string-nw-p desc)
|
|
|
|
(format "[%s]" desc)))
|
|
|
|
|
|
|
|
|
|
|
|
(defun org-gemini-section (section contents info)
|
2020-11-08 00:20:34 +00:00
|
|
|
"Transcode a SECTION element from Org to GEMINI.
|
2020-11-08 00:19:04 +00:00
|
|
|
CONTENTS is the contents of the section. INFO is a plist holding
|
|
|
|
contextual information."
|
2022-01-10 18:30:46 +00:00
|
|
|
;; CONTENTS is nil when the section has no text but it has properties setted.
|
|
|
|
(let ((contents-str (or contents "")) ;; ensure that contents is a string.
|
|
|
|
(links
|
2021-08-13 15:09:02 +00:00
|
|
|
(and (plist-get info :ascii-links-to-notes)
|
|
|
|
;; Take care of links in first section of the document.
|
|
|
|
(not (org-element-lineage section '(headline)))
|
|
|
|
(org-gemini--describe-links
|
|
|
|
(org-ascii--unique-links section info)
|
|
|
|
(org-ascii--current-text-width section info)
|
|
|
|
info))))
|
2020-11-08 00:19:04 +00:00
|
|
|
(org-remove-indentation
|
2023-05-14 10:47:43 +00:00
|
|
|
(if (not (org-string-nw-p links)) contents-str
|
|
|
|
(concat (org-element-normalize-string contents-str) "\n\n" links))
|
|
|
|
;; Do not apply inner margin if parent headline is low level.
|
|
|
|
(let ((headline (org-export-get-parent-headline section)))
|
|
|
|
(if (or (not headline) (org-export-low-level-p headline info)) 0
|
|
|
|
(plist-get info :ascii-inner-margin))))))
|
|
|
|
|
|
|
|
(defun org-gemini--build-title (element info _text-width &optional _underline _notags toc)
|
|
|
|
"Build a title heading.
|
2020-11-25 17:03:30 +00:00
|
|
|
|
2022-01-16 03:40:58 +00:00
|
|
|
ELEMENT is an org-element. TOC is whether to show the table of contents.
|
|
|
|
INFO is unimportant."
|
2020-11-08 00:19:04 +00:00
|
|
|
(let ((number (org-element-property :level element))
|
2021-08-13 15:09:02 +00:00
|
|
|
(text
|
|
|
|
(org-trim
|
|
|
|
(org-export-data
|
|
|
|
(if toc
|
|
|
|
(org-export-get-alt-title element info)
|
|
|
|
(org-element-property :title element))
|
|
|
|
info))))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
|
|
|
(format "%s %s" (make-string number ?#) text)))
|
|
|
|
|
|
|
|
|
|
|
|
(defun org-gemini-headline (headline contents info)
|
2020-11-08 00:20:34 +00:00
|
|
|
"Transcode a HEADLINE element from Org to GEMINI.
|
2020-11-08 00:19:04 +00:00
|
|
|
CONTENTS holds the contents of the headline. INFO is a plist
|
|
|
|
holding contextual information."
|
|
|
|
;; Don't export footnote section, which will be handled at the end
|
|
|
|
;; of the template.
|
|
|
|
(unless (org-element-property :footnote-section-p headline)
|
|
|
|
(let* ((low-level (org-export-low-level-p headline info))
|
2021-08-13 15:09:02 +00:00
|
|
|
(width (org-ascii--current-text-width headline info))
|
|
|
|
;; Export title early so that any link in it can be
|
|
|
|
;; exported and seen in `org-ascii--unique-links'.
|
|
|
|
(title (org-gemini--build-title headline info width (not low-level)))
|
|
|
|
;; Blank lines between headline and its contents.
|
|
|
|
;; `org-ascii-headline-spacing', when set, overwrites
|
|
|
|
;; original buffer's spacing.
|
|
|
|
(pre-blanks
|
|
|
|
(make-string (or (car (plist-get info :ascii-headline-spacing))
|
|
|
|
(org-element-property :pre-blank headline)
|
|
|
|
0)
|
|
|
|
?\n))
|
|
|
|
(links (and (plist-get info :ascii-links-to-notes)
|
|
|
|
(org-gemini--describe-links
|
|
|
|
(org-ascii--unique-links headline info) width info)))
|
|
|
|
;; Re-build contents, inserting section links at the right
|
|
|
|
;; place. The cost is low since build results are cached.
|
|
|
|
(body
|
|
|
|
(if (not (org-string-nw-p links)) contents
|
|
|
|
(let* ((contents (org-element-contents headline))
|
|
|
|
(section (let ((first (car contents)))
|
|
|
|
(and (eq (org-element-type first) 'section)
|
|
|
|
first))))
|
|
|
|
(concat (and section
|
|
|
|
(concat (org-element-normalize-string
|
|
|
|
(org-export-data section info))
|
|
|
|
"\n\n"))
|
|
|
|
links
|
|
|
|
(mapconcat (lambda (e) (org-export-data e info))
|
|
|
|
(if section (cdr contents) contents)
|
|
|
|
""))))))
|
2020-11-08 00:19:04 +00:00
|
|
|
;; Deep subtree: export it as a list item.
|
|
|
|
(if low-level
|
2021-08-13 15:09:02 +00:00
|
|
|
(let* ((bullets (cdr (assq (plist-get info :ascii-charset)
|
|
|
|
(plist-get info :ascii-bullets))))
|
|
|
|
(bullet
|
|
|
|
(format "%c "
|
|
|
|
(nth (mod (1- low-level) (length bullets)) bullets))))
|
|
|
|
(concat bullet title "\n" pre-blanks
|
2022-04-17 19:06:05 +00:00
|
|
|
;; In Gemtext, text should not be indentend. Otherwise,
|
|
|
|
;; source code blocks, links, and other line types would not
|
|
|
|
;; be interpreted by clients because of the initial spacing.
|
|
|
|
body))
|
2021-08-13 15:09:02 +00:00
|
|
|
;; Else: Standard headline.
|
|
|
|
(concat title "\n" pre-blanks body)))))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
|
|
|
(defun org-gemini-template (contents info)
|
2020-11-08 00:20:34 +00:00
|
|
|
"Return complete document string after GEMINI conversion.
|
2020-11-08 00:19:04 +00:00
|
|
|
CONTENTS is the transcoded contents string. INFO is a plist
|
|
|
|
holding export options."
|
2021-08-11 07:57:46 +00:00
|
|
|
(let ((title (org-export-data (when (plist-get info :with-title)
|
|
|
|
(plist-get info :title))
|
|
|
|
info)))
|
|
|
|
(concat
|
|
|
|
(unless (string= title "")
|
|
|
|
(format "# %s\n\n" title))
|
|
|
|
contents)))
|
2020-11-08 00:19:04 +00:00
|
|
|
|
|
|
|
(defun org-gemini-export-to-buffer (&optional async subtreep visible-only body-only ext-plist)
|
2020-11-12 07:35:20 +00:00
|
|
|
"Export an org file to a new buffer.
|
|
|
|
|
|
|
|
A non-nil optional argument ASYNC means the process should happen
|
|
|
|
asynchronously. The resulting buffer should be accessible
|
|
|
|
through the `org-export-stack' interface.
|
|
|
|
|
|
|
|
When optional argument SUBTREEP is non-nil, export the sub-tree
|
|
|
|
at point, extracting information from the headline properties
|
|
|
|
first.
|
|
|
|
|
|
|
|
When optional argument VISIBLE-ONLY is non-nil, don't export
|
|
|
|
contents of hidden elements.
|
|
|
|
|
|
|
|
When optional argument BODY-ONLY is non-nil, strip title and
|
|
|
|
table of contents from output.
|
|
|
|
|
|
|
|
EXT-PLIST, when provided, is a property list with external
|
|
|
|
parameters overriding Org default settings, but still inferior to
|
|
|
|
file-local settings."
|
2020-11-08 00:19:04 +00:00
|
|
|
(interactive)
|
|
|
|
(org-export-to-buffer 'gemini "*Org Gemini Export*" async subtreep visible-only body-only ext-plist (lambda () (text-mode))))
|
|
|
|
|
|
|
|
|
|
|
|
(defun org-gemini-export-to-file (&optional async subtreep visible-only body-only ext-plist)
|
2020-11-12 07:35:20 +00:00
|
|
|
"Export an org file to a gemini file.
|
|
|
|
|
|
|
|
A non-nil optional argument ASYNC means the process should happen
|
|
|
|
asynchronously. The resulting buffer should be accessible
|
|
|
|
through the `org-export-stack' interface.
|
|
|
|
|
|
|
|
When optional argument SUBTREEP is non-nil, export the sub-tree
|
|
|
|
at point, extracting information from the headline properties
|
|
|
|
first.
|
|
|
|
|
|
|
|
When optional argument VISIBLE-ONLY is non-nil, don't export
|
|
|
|
contents of hidden elements.
|
|
|
|
|
|
|
|
When optional argument BODY-ONLY is non-nil, strip title and
|
|
|
|
table of contents from output.
|
|
|
|
|
|
|
|
EXT-PLIST, when provided, is a property list with external
|
|
|
|
parameters overriding Org default settings, but still inferior to
|
|
|
|
file-local settings."
|
2020-11-08 00:19:04 +00:00
|
|
|
(interactive)
|
|
|
|
(let ((file (org-export-output-file-name ".gmi" subtreep)))
|
|
|
|
(org-export-to-file 'gemini file
|
|
|
|
async subtreep visible-only body-only ext-plist)))
|
|
|
|
|
|
|
|
|
2020-11-08 05:54:04 +00:00
|
|
|
(defun org-gemini-publish-to-gemini (plist filename pub-dir)
|
2020-11-12 07:35:20 +00:00
|
|
|
"Publish an org file to a gemini file.
|
2020-11-08 05:54:04 +00:00
|
|
|
|
|
|
|
FILENAME is the filename of the Org file to be published. PLIST
|
|
|
|
is the property list for the given project. PUB-DIR is the
|
|
|
|
publishing directory.
|
|
|
|
|
|
|
|
Return output file name."
|
|
|
|
(org-publish-org-to
|
|
|
|
'gemini filename ".gmi" plist pub-dir))
|
|
|
|
|
2022-01-16 03:40:58 +00:00
|
|
|
(defun org-gemini-export-block (export-block _contents _info)
|
|
|
|
"Transcode a EXPORT-BLOCK element from Org to Markdown.
|
|
|
|
CONTENTS is nil. INFO is a plist holding contextual information."
|
2022-02-04 14:51:00 +00:00
|
|
|
(when (member (org-element-property :type export-block)
|
|
|
|
'("GEMINI" "GMI" "GEMTEXT"))
|
2022-01-16 03:40:58 +00:00
|
|
|
(org-remove-indentation (org-element-property :value export-block))))
|
|
|
|
|
2022-02-04 18:54:19 +00:00
|
|
|
(defun org-gemini-table (table contents info)
|
2022-04-18 14:33:45 +00:00
|
|
|
"Generate a Gemtext table from the parsed Org.
|
|
|
|
Use the `org-ascii-table' but surrounded by backticks.
|
|
|
|
Parameters: TABLE is the parsed org-element table. CONTENTS is the text with
|
|
|
|
properties. INFO is a plist with export options."
|
2022-02-04 18:54:19 +00:00
|
|
|
(let ((name (or (caaar (org-element-property :caption table))
|
|
|
|
(org-element-property :name table))))
|
2022-04-18 14:33:45 +00:00
|
|
|
(format "```%s\n%s\n```\n"
|
|
|
|
(or name "")
|
|
|
|
(org-ascii-table table contents info))))
|
|
|
|
|
2020-11-08 05:54:04 +00:00
|
|
|
|
2020-11-08 00:19:04 +00:00
|
|
|
(provide 'ox-gemini)
|
2020-11-12 07:35:20 +00:00
|
|
|
;;; ox-gemini.el ends here
|