-
Notifications
You must be signed in to change notification settings - Fork 0
/
el2md.el
366 lines (330 loc) · 12.5 KB
/
el2md.el
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
;;; el2md.el --- Convert commentary section of elisp files to markdown. -*- lexical-binding:t -*-
;; Author: Jade Michael Thornton
;; Version: 1.0.2
;; URL: https://gitlab.com/thornjad/el2md
;;; Commentary:
;; This package converts the _Commentary_ section in an Elisp module to text
;; files in the Markdown language. It supports most of Markdown, including some
;; headings, code blocks, text styles, lists, etc.
;; What is converted:
;;
;; Everything between the `Commentary:' and `Code:' markers are included in the
;; generated text. In addition, the title and _some_ metadata is also included.
;; How to write comments:
;;
;; The general rule of thumb is that the Elisp module should be written
;; using plain text, as they always have been written. However, some things are
;; recognized. A single line ending with a colon is considered a _heading_. If
;; this line is at the start of a comment block, it is considered a main (level
;; 2) heading. Otherwise it is considered a (level 3) subheading. Note that if
;; the line precedes a bullet list or code, it will not be treated as a
;; subheading.
;;
;; Use Markdown formatting:
;;
;; It is possible to use markdown syntax in the text, like _this_, and **this**.
;;
;; Conventions:
;;
;; The following conventions are used when converting Elisp comments to
;; Markdown:
;;
;; * Code blocks using either the Markdown convention by indenting the block
;; with four extra spaces, or by starting a paragraph with a `('.
;;
;; * In Elisp comments, a reference to `code' (backquote - quote) will be
;; converted to Markdown (backquote - backquote).
;;
;; * In Elisp comments, bullets in lists are typically separated by empty lines.
;; In the converted text, the empty lines are removed, as required by
;; Markdown.
;;
;; Example:
;;
;;
;; ;; This is a heading:
;; ;;
;; ;; Bla bla bla ...
;;
;; ;; This is another heading:
;; ;;
;; ;; This is a paragraph!
;; ;;
;; ;; A subheading:
;; ;;
;; ;; Another paragraph.
;; ;;
;; ;; This line is _not_ as a subheading:
;; ;;
;; ;; * A bullet in a list
;; ;;
;; ;; * Another bullet.
;; Usage:
;;
;; To generate the markdown representation of the current buffer to a temporary
;; buffer, use:
;;
;; M-x el2md-view-buffer RET
;;
;; To write the markdown representation of the current buffer to a file, use:
;;
;; M-x el2md-write-file RET name-of-file RET
;;
;; In sites like GitLab, if a file named readme.md exists in the root directory
;; of an repository, it is displayed when viewing the repository. To generate a
;; readme.md file, in the same directory as the current buffer, use:
;;
;; M-x el2md-write-readme RET
;; Post processing:
;;
;; To post-process the output, add a function to `el2md-post-convert-hook'. The
;; functions in the hook should accept one argument, the output stream
;; (typically the destination buffer). When the hook is run current buffer is
;; the source buffer.
;; Batch mode:
;;
;; You can run el2md in batch mode. The function `el2md-write-readme' can be
;; called directly using the `-f' option. The others can be accessed with the
;; `--eval' form.
;;
;; For example,
;;
;; emacs -batch -l el2md.el my-file.el -f el2md-write-readme
;; Known problems:
;;
;; - The end of the _Commentary_ section includes the first comment in the code
;; if there's no empty line before the _Code_ comment.
;;; License:
;;
;; `el2md' was originally forked from `el2markdown' by Anders Lindgren. It has
;; since been improved, but carries the same license as the original work.
;;
;; Copyright (c) 2020 Jade Michael Thornton
;; Copyright (c) 2013 Anders Lindgren
;;
;; 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, version 3. 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.
;;; Code:
;; The `{{{' and `}}}' and sequences are used by the package
;; `folding.el'.
(defvar el2md-empty-comment "^;;+ *\\(\\({{{\\|}}}\\).*\\)?$"
"Regexp of lines that should be considered empty.")
(defvar el2md-translate-keys-within-markdown-quotes nil
"When non-nil, match key sequences found between backquotes.
By default, this package only converts things quoted using
backquote and quote, which is the standard elisp way to quote
things in comments.")
(defvar el2md-keys '("RET" "TAB")
"List of keys that sould be translated to <key>...</key>.")
(defvar el2md-post-convert-hook nil
"Hook that is run after a buffer has been converted to Markdown.
The functions in the hook should accept one argument, the output
stream (typically the destination buffer). When the hook is run
current buffer is the source buffer.")
(defun el2md-convert ()
"Print comments section of current buffer as Markdown.
After conversion, `el2md-post-convert-hook' is called. The
functions in the hook should accept one argument, the output
stream (typically the destination buffer). When the hook is run
current buffer is the source buffer."
(save-excursion
(goto-char (point-min))
(el2md-convert-title)
(el2md-convert-formal-information)
(el2md-skip-to-commentary)
(while
(el2md-convert-section))
(terpri)
(princ "---")
(terpri)
(let ((file-name (buffer-file-name))
(from ""))
(if file-name
(setq from (concat " from `"
(file-name-nondirectory file-name)
"`")))
(princ (concat
"Converted" from
" by "
"[_el2md_](https://gitlab.com/thornjad/el2md)."))
(terpri))
(run-hook-with-args 'el2md-post-convert-hook standard-output)))
(defun el2md-skip-empty-lines ()
(while (and (bolp) (eolp) (not (eobp)))
(forward-line)))
;; Some packages place lincense blocks in the commentary section,
;; ignore them.
(defun el2md-skip-license ()
"Skip license blocks."
(when (looking-at "^;;; \\(License\\|Copying\\):[ \t]*$")
(forward-line)
(while (not (or (eobp)
(looking-at "^;;;")))
(forward-line))))
(defun el2md-translate-string (string)
(let ((res "")
(end-quote (if el2md-translate-keys-within-markdown-quotes
"[`']"
"'")))
(while (string-match (concat "`\\([^`']*\\)" end-quote) string)
(setq res (concat res (substring string 0 (match-beginning 0))))
(let ((content (match-string 1 string))
(beg "`")
(end "`"))
(setq string (substring string (match-end 0)))
(when (save-match-data
(let ((case-fold-search nil))
(string-match (concat "^\\([SCM]-[^`']+\\|"
(regexp-opt el2md-keys)
"\\)$") content)))
(setq beg "<kbd>")
(setq end "</kbd>"))
(setq res (concat res beg content end))))
(concat res string)))
(defun el2md-convert-title ()
(when (looking-at ";;+ \\(.*\\)\\.el --+ \\(.*\\)$")
(let ((package-name (match-string-no-properties 1))
(title (match-string-no-properties 2)))
(when (string-match " *-\\*-.*-\\*-" title)
(setq title (replace-match "" nil nil title)))
(el2md-emit-header 1 (concat package-name " - " title))
(forward-line))))
(defun el2md-convert-formal-information ()
(save-excursion
(goto-char (point-min))
(let ((limit (save-excursion
(re-search-forward "^;;; Commentary:$" nil t))))
(when limit
(el2md-convert-formal-information-item "Author")
(el2md-convert-formal-information-item "Version")
(el2md-convert-formal-information-item "URL" 'link)
(terpri)))))
(defun el2md-convert-formal-information-item (item &optional link)
(when (re-search-forward (concat "^;;+ " item ": *\\(.*\\)") nil t)
(let ((s (match-string-no-properties 1)))
(if link
(setq s (concat "[" s "](" s ")")))
(princ (concat "_" item ":_ " s "<br>"))
(terpri))))
(defun el2md-skip-to-commentary ()
(if (re-search-forward ";;; Commentary:$" nil t)
(forward-line)))
(defun el2md-convert-section ()
(el2md-skip-empty-lines)
(el2md-skip-license)
(if (or (looking-at "^;;; Code:$")
(eobp))
nil
(let ((p (point)))
(el2md-emit-rest-of-comment)
(not (eq p (point))))))
(defun el2md-emit-header (count title)
(princ (make-string count ?#))
(princ " ")
;; Strip trailing ".".
(let ((len nil))
(while (progn
(setq len (length title))
(and (not (equal len 0))
(eq (elt title (- len 1)) ?.)))
(setq title (substring title 0 (- len 1)))))
(princ (el2md-translate-string title))
(terpri)
(terpri))
(defun el2md-is-at-bullet-list ()
"Non-nil when next non-empty comment line is a bullet list."
(save-excursion
(while (looking-at "^;;$")
(forward-line))
;; When more then 4 spaces, the line is a code block.
(looking-at ";;+ \\{0,4\\}[-*]")))
(defun el2md-emit-rest-of-comment ()
(let ((first t))
(while (looking-at "^;;")
;; Skip empty lines.
(while (looking-at el2md-empty-comment)
(forward-line))
(if (and (looking-at ";;+ \\(.*\\):$")
(save-excursion
(save-match-data
(forward-line)
(looking-at el2md-empty-comment)))
;; When preceding code or bullet list, don't treat as
;; sub-header.
(or first
(not (save-excursion
(save-match-data
(forward-line)
(while (looking-at el2md-empty-comment)
(forward-line))
(or (el2md-is-at-bullet-list)
(looking-at ";;+ *(")
(looking-at ";;+ ")))))))
;; Header
(progn
(el2md-emit-header (if first 2 3)
(match-string-no-properties 1))
(forward-line 2))
;; Section of text. (Things starting with a parenthesis is
;; assumes to be code.)
(let ((is-code (looking-at ";;+ *("))
(is-bullet (el2md-is-at-bullet-list)))
(while (looking-at ";;+ ?\\(.+\\)$")
(if is-code
(princ " "))
(princ (el2md-translate-string
(match-string-no-properties 1)))
(terpri)
(forward-line))
;; Insert empty line between sections of code (unless
;; between bullet lists.)
(if (and is-bullet
(el2md-is-at-bullet-list))
nil
(terpri))))
(setq first nil))))
;;;###autoload
(defun el2md-view-buffer ()
"Convert comment section to markdown and display in temporary buffer."
(interactive)
(with-output-to-temp-buffer "*el2md*"
(el2md-convert)))
;;;###autoload
(defun el2md-write-file (&optional file-name overwrite-without-confirm)
"Convert comment section to markdown and write to FILE-NAME."
(interactive
(let ((suggested-name (and (buffer-file-name)
(concat (file-name-sans-extension
(buffer-file-name))
".md"))))
(list (read-file-name "Write markdown file: "
default-directory
suggested-name
nil
(file-name-nondirectory suggested-name)))))
(unless file-name
(setq file-name (concat (buffer-file-name) ".md")))
(let ((buffer (current-buffer))
(orig-buffer-file-coding-system buffer-file-coding-system))
(with-temp-buffer
;; Inherit the file coding from the buffer being converted.
(setq buffer-file-coding-system orig-buffer-file-coding-system)
(let ((standard-output (current-buffer)))
(with-current-buffer buffer
(el2md-convert))
;; Note: Must set `require-final-newline' inside
;; `with-temp-buffer', otherwise the value will be overridden by
;; the buffers local value.
(let ((require-final-newline nil))
(write-file file-name (not overwrite-without-confirm)))))))
;;;###autoload
(defun el2md-write-readme ()
"Generate readme.md, designed to be used in batch mode."
(interactive)
(el2md-write-file "readme.md" noninteractive))
(provide 'el2md)
;;; el2md.el ends here