~abrahms/ox-gemini

ref: 577652feec99684cb9fac59660da27519081ddb0 ox-gemini/ox-gemini.el -rw-r--r-- 9.4 KiB
577652feJustin Abrahms checkdoc passes now 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
;;; ox-gemini.el --- Output gemini formatted documents from org-mode  -*- lexical-binding: t; -*-

;; Author: Justin Abrahms <justin@abrah.ms>
;; URL: https://git.sr.ht/~abrahms/ox-gemini
;; Keywords: lisp gemini
;; Version: 0
;; Package-Requires: ((emacs "26.1"))
;; SPDX-License-Identifier: GPL-3.0-or-later


;;; 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.

(require 'ox)
(require 'ox-publish)
(require 'ox-ascii)
(require 'cl-lib)


;; TODO:
;; Sublists aren't supported in gemini
;; There's a trailing space after inline code samples
;; If you don't have a title, it leaves a blank # in the gmi
;; If you link a file to an absolute path, the links break
;; bare links don't work (e.g. directly linking https://google.com
;; 

;;; Code:

(org-export-define-derived-backend 'gemini 'ascii
  :menu-entry
  '(?g "Export to Gemini"
       ((?b "To buffer"
	    (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)))))
  :translate-alist '((code . org-gemini-code-inline)
		     (headline . org-gemini-headline)
		     (link . org-gemini-link)
		     (section . org-gemini-section)
		     (src-block . org-gemini-code-block)
                     (item . org-gemini-item)
		     (template . org-gemini-template)))

(defun org-gemini-paragraph (paragraph _contents _info)
  "PARAGRAPH is the text of the paragraph."
  paragraph)

(defun org-gemini-item (_input contents _info)
  "CONTENTS is the text of the individual item."
  (format "* %s" contents))

(defun org-gemini-code-inline (input _contents info)
  "INPUT is either a 'src-block' or 'example-block' element.  INFO is a plist."
  ;; there's a bug here where there's a trailing space in the ``
  (format "`%s`" (org-export-format-code-default input info)))

(defun org-gemini-code-block (example-block _contents info)
  "EXAMPLE-BLOCK is a codeblock.  INFO is a plist."
  (org-remove-indentation
   (format "```\n%s```"
	   (org-export-format-code-default example-block info))))

(defun org-gemini--describe-links (links _width info)
  "Describe links is the footer-portion of the link data.

It's output just before each section.  LINKS is a list of each link.  INFO is a plist."
  (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))
	    (desc (org-element-contents link))
	    (anchor (org-export-data
		     (or desc (org-element-property :raw-link link))
		     info)))
       (format "=> %s %s\n" path anchor)))
   links ""))


(defun org-gemini-link (_link desc _info)
  "Simple link generation.

DESC is the link text

Note: the footer with the actual links are handled in
org-gemini--describe-links"
  (if (org-string-nw-p desc)
      (format "[%s]" desc)))


(defun org-gemini-section (section contents info)
  "Transcode a SECTION element from Org to GEMINI.
CONTENTS is the contents of the section.  INFO is a plist holding
contextual information."
  (let ((links
	 (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))))
    (org-remove-indentation
      (if (not (org-string-nw-p links)) contents
	(concat (org-element-normalize-string contents) "\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.

ELEMENT is an org-element.  TOC is whether to show the table of contents.  INFO is unimportant."
  (let ((number (org-element-property :level element))
	(text
	 (org-trim
	  (org-export-data
	   (if toc
	       (org-export-get-alt-title element info)
	     (org-element-property :title element))
	   info))))

    (format "%s %s" (make-string number ?#) text)))


(defun org-gemini-headline (headline contents info)
  "Transcode a HEADLINE element from Org to GEMINI.
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))
	   (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)
				   ""))))))
      ;; Deep subtree: export it as a list item.
      (if low-level
	  (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
		    ;; Contents, indented by length of bullet.
		    (org-ascii--indent-string body (length bullet))))
	;; Else: Standard headline.
	(concat title "\n" pre-blanks body)))))

(defun org-gemini-template (contents info)
  "Return complete document string after GEMINI conversion.
CONTENTS is the transcoded contents string.  INFO is a plist
holding export options."
  (concat
   ;; Build title block.
   (format "# %s\n" (org-export-data
                     (when (plist-get info :with-title) (plist-get info :title)) info))
   ;; Document's body.
   contents))



(defun org-gemini-export-to-buffer (&optional async subtreep visible-only body-only ext-plist)
  "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."
  (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)
  "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."
  (interactive)
  (let ((file (org-export-output-file-name ".gmi" subtreep)))
    (org-export-to-file 'gemini file
      async subtreep visible-only body-only ext-plist)))


(defun org-gemini-publish-to-gemini (plist filename pub-dir)
  "Publish an org file to a gemini file.

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))


(provide 'ox-gemini)
;;; ox-gemini.el ends here