forked from polymode/polymode
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpolymode-export.el
448 lines (394 loc) · 19.6 KB
/
polymode-export.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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
;;; polymode-export.el --- Exporting facilities for polymodes -*- lexical-binding: t -*-
;;
;; Copyright (C) 2013-2022 Free Software Foundation, Inc.
;; Author: Vitalie Spinu
;; URL: https://github.com/polymode/polymode
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; 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, 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Commentary:
;;
;;; Code:
(require 'polymode-core)
(require 'polymode-classes)
(eval-when-compile (require 'polymode-weave)) ;Silence "unknown slot from-to".
(defgroup polymode-export nil
"Polymode Exporters"
:group 'polymode)
(defcustom polymode-exporter-output-file-format "%s-exported"
"Format of the exported files.
%s is substituted with the current file name sans extension."
:type 'string)
(defclass pm-exporter (pm-root)
((from
:initarg :from
:initform '()
:type list
:custom list
:documentation
"Input exporter specifications.
This is an alist of elements of the form (id regexp doc
commmand) or (id . selector). ID is the unique identifier of
the spec. REGEXP is a regexp which, if matched on current
file name, implies that the current file can be exported
with this specification. DOC is a short help string shown
during interactive export. COMMAND is the exporter
command (string). It can contain the following format specs:
%i - input file (no dir)
%I - input file (full path)
%o - output file (no dir)
%O - output file (full path)
%b - output file (base name only)
%t - 4th element of the :to spec
When specification is of the form (id . selector), SELECTOR
is a function of variable arguments that accepts at least
one argument ACTION. ACTION is a symbol and can be one of
the following:
match - must return non-nil if this specification
applies to the file that current buffer is visiting,
or :nomatch if specification does not apply. This
selector can receive an optional file-name
argument. In that case the decision must be made
solely on that file and current buffer must be
ignored. This is useful for matching exporters to
weavers when exported file does not exist yet.
regexp - return a string which is used to match input
file name. If nil, `match' selector must return
non-nil value. This selector is ignored if `match'
returned non-nil.
doc - return documentation string
commmand - return a string with optional %i, %f,
etc. format specs as described above. It will be
passed to the processing :function.")
(to
:initarg :to
:initform '()
:type list
:custom list
:documentation
"
Output specifications alist. Each element is either a list
of the form (id ext doc t-spec) or a cons (id . selector).
In the former case EXT is an extension of the output file.
DOC is a short documentation string. t-spec is a string what
is substituted instead of %t in :from spec commmand.
`t-spec' can be a list of one element '(command), in which
case the whole :from spec command is substituted with
command from %t-spec.
When specification is of the form (id . selector), SELECTOR
is a function of variable arguments with first two arguments
being ACTION and ID of the specification. This function is
called in a buffer visiting input file. ACTION is a symbol
and can one of the following:
output-file - return an output file name or a list of file
names. Receives input-file as argument. If this
command returns nil, the output is built from input
file and value of 'output-ext command.
This selector can also return a function. This
function will be called in the callback or sentinel of
the weaving process after the weaving was
completed. This function should sniff the output of
the process for errors or file names. It must return a
file name, a list of file names or nil if no such
files have been detected.
ext - extension of output file. If nil and `output-file'
also returned nil, the exporter won't be able to
identify the output file and no automatic display or
preview will be available.
doc - return documentation string
command - return a string to be used instead of
the :from command. If nil, :from spec command is used.
t-spec - return a string to be substituted as %t :from
spec in :from command. If `command' selector returned
non-nil, this spec is ignored.")
(function
:initarg :function
:initform (lambda (command from to)
(error "Function not defined for this exporter"))
:type (or symbol function)
:documentation
"Function to process the commmand. Must take 3 arguments
COMMAND, FROM-ID and TO-ID and return an output file name or
a list of output file names. COMMAND is the 4th argument of
:from spec with all the formats substituted. FROM-ID is the
id of requested :from spec, TO-ID is the id of the :to
spec."))
"Root exporter class.")
(defclass pm-callback-exporter (pm-exporter)
((callback
:initarg :callback
:initform nil
:type (or symbol function)
:documentation
"Callback function to be called by function in :function
slot. Callback must return an output file name or a list of
output file-names. There is no default callback."))
"Class to represent asynchronous exporters.
:function slot must be a function with 4 arguments COMMAND,
CALLBACK, FROM-ID and TO-ID.")
(defclass pm-shell-exporter (pm-exporter)
((function
:initform 'pm-default-shell-export-function)
(sentinel
:initarg :sentinel
:initform 'pm-default-shell-export-sentinel
:type (or symbol function)
:documentation
"Sentinel function to be called by :function when a shell
call is involved. Sentinel should return the output file
name.")
(quote
:initarg :quote
:initform nil
:type boolean
:documentation "Non-nil when file arguments must be quoted
with `shell-quote-argument'."))
"Class to represent exporters that call external processes.")
(defun pm-default-shell-export-function (command sentinel from to)
"Run exporting COMMAND interactively to convert FROM to TO.
Run command in a buffer (in comint-shell-mode) so that it accepts
user interaction. This is a default function in all exporters
that call a shell command. SENTINEL is the process sentinel."
(pm--run-shell-command command sentinel "*polymode export*"
(concat "Exporting " from "-->" to " with command:\n\n "
command "\n\n")))
;;; METHODS
(cl-defgeneric pm-export (exporter from to &optional ifile)
"Process IFILE with EXPORTER.")
(cl-defmethod pm-export ((exporter pm-exporter) from to &optional ifile)
(pm--process-internal exporter from to ifile))
(cl-defmethod pm-export ((exporter pm-callback-exporter) from to &optional ifile)
(let ((cb (pm--wrap-callback exporter :callback ifile)))
(pm--process-internal exporter from to ifile cb)))
(cl-defmethod pm-export ((exporter pm-shell-exporter) from to &optional ifile)
(let ((cb (pm--wrap-callback exporter :sentinel ifile)))
(pm--process-internal exporter from to ifile cb (eieio-oref exporter 'quote))))
;; UI
(defvar pm--exporter-hist nil)
(defvar pm--export:from-hist nil)
(defvar pm--export:from-last nil)
(defvar pm--export:to-hist nil)
(defvar pm--export:to-last nil)
(declare-function polymode-set-weaver "polymode-weave")
(declare-function pm-weave "polymode-weave")
(defun polymode-export (&optional from to)
"Export current file.
FROM and TO are the ids of the :from and :to slots of the current
exporter. If the current exporter hasn't been set yet, set the
exporter with `polymode-set-exporter'. You can always change the
exporter manually by invoking `polymode-set-exporter'.
When FROM or TO are missing they are determined automatically
from the current exporter's specifications and file's
extension. If no appropriate export specification has been found,
look into current weaver and try to match weaver's output to
exporters input extension. When such combination is possible,
settle on weaving first and exporting the weaved output. When
none of the above worked, ask the user for `from' and `to' specs.
When called with prefix argument, ask for FROM and TO
interactively. See constructor function ‘pm-exporter’ for the
complete specification."
(interactive "P")
(cl-flet ((to-name.id (el) (let* ((ext (funcall (cdr el) 'ext (car el)))
(name (if ext
(format "%s (%s)" (funcall (cdr el) 'doc (car el)) ext)
(funcall (cdr el) 'doc (car el)))))
(cons name (car el))))
(from-name.id (el) (cons (funcall (cdr el) 'doc (car el)) (car el))))
(let* ((exporter (symbol-value (or (eieio-oref pm/polymode 'exporter)
(polymode-set-exporter t))))
(fname (file-name-nondirectory buffer-file-name))
(gprompt nil)
(case-fold-search t)
(from-opts (mapcar #'from-name.id (pm--selectors exporter :from)))
(from-id
(cond
;; A: guess from spec
((null from)
(or
;; 1. repeated export; don't ask
pm--export:from-last
;; 2. select :from entries which match to current context
(let ((matched (pm--matched-selectors exporter :from)))
(when matched
(if (> (length matched) 1)
(cdr (pm--completing-read "Multiple `from' specs matched. Choose one: "
(mapcar #'from-name.id matched)))
(caar matched))))
;; 3. guess from weaver and return a cons (weaver-id . exporter-id)
(let ((weaver (symbol-value (or (eieio-oref pm/polymode 'weaver)
(progn
(setq gprompt "Choose `from' spec: ")
(polymode-set-weaver))))))
(when weaver
;; fixme: weaver was not yet ported to selectors
;; fixme: currently only first match is returned
(let ((pair (cl-loop for w in (eieio-oref weaver 'from-to)
;; weaver input extension matches the filename
if (string-match-p (nth 1 w) fname)
return (cl-loop for el in (pm--selectors exporter :from)
;; input exporter extensnion matches weaver output extension
when (pm--selector-match el (concat "dummy." (nth 2 w)))
return (cons (car w) (car el))))))
(when pair
(message "Matching weaver found. Weaving to '%s' first." (car pair))
pair))))
;; 4. nothing matched; ask
(let* ((prompt (or gprompt "No `from' specs matched. Choose one: "))
(sel (pm--completing-read prompt from-opts nil t nil 'pm--export:from-hist)))
(cdr sel))))
;; B: C-u, force a :from spec
((equal from '(4))
(cdr (if (> (length from-opts) 1)
(pm--completing-read "Input type: " from-opts nil t nil 'pm--export:from-hist)
(car from-opts))))
;; C. string
((stringp from)
(if (assoc from (eieio-oref exporter 'from))
from
(error "Cannot find `from' spec '%s' in %s exporter"
from (eieio-object-name exporter))))
;; D. error
(t (error "'from' argument must be nil, universal argument or a string"))))
(to-opts (mapcar #'to-name.id (pm--selectors exporter :to)))
(to-id
(cond
;; A. guess from spec
((null to)
(or
;; 1. repeated export; don't ask and use first entry in history
(unless (equal from '(4))
pm--export:to-last)
;; 2. First export or C-u
(if (= (length to-opts) 1)
(cdar to-opts)
(cdr (pm--completing-read "Export to: " to-opts nil t nil 'pm--export:to-hist)))))
;; B. string
((stringp to)
(if (assoc to (eieio-oref exporter 'to))
to
(error "Cannot find output spec '%s' in %s exporter"
to (eieio-object-name exporter))))
;; C . Error
(t (error "'to' argument must be nil or a string")))))
(setq-local pm--export:from-last from-id)
(setq-local pm--export:to-last to-id)
(if (consp from-id)
;; run through weaver
(let ((pm--export-spec (cons (cdr from-id) to-id))
(pm--output-not-real t))
(pm-weave (symbol-value (eieio-oref pm/polymode 'weaver)) (car from-id)))
(pm-export exporter from-id to-id)))))
(defun polymode-set-exporter (&optional no-ask-if-1)
"Interactively set exporter for the current file.
If NO-ASK-IF-1 is non-nil, don't ask if there is only one exporter."
(interactive)
(unless pm/polymode
(error "No pm/polymode object found. Not in polymode buffer?"))
(let* ((weavers (delete-dups (pm--oref-with-parents pm/polymode :weavers)))
(exporters (pm--abrev-names
"pm-exporter/\\|-exporter"
(cl-delete-if-not
(lambda (el)
(or (pm--matched-selectors el :from)
;; FIXME: rewrite this abomination
;; Match weaver to the exporter.
(cl-loop for weaver in weavers
if (cl-loop for w in (eieio-oref (symbol-value weaver) 'from-to)
;; weaver input extension matches the filename
if (string-match-p (nth 1 w) buffer-file-name)
return (cl-loop for el in (pm--selectors (symbol-value el) :from)
;; input exporter extensnion matches weaver output extension
when (pm--selector-match el (concat "dummy." (nth 2 w)))
return t))
return t)))
(delete-dups (pm--oref-with-parents pm/polymode :exporters)))))
(sel (if exporters
(if (and no-ask-if-1 (= (length exporters) 1))
(car exporters)
(pm--completing-read "Choose exporter: " exporters nil t nil 'pm--exporter-hist))
(user-error "No valid exporters in current context")))
(out (intern (cdr sel))))
(setq pm--exporter-hist (delete-dups pm--exporter-hist))
(setq-local pm--export:from-last nil)
(setq-local pm--export:to-last nil)
(oset pm/polymode :exporter out)
out))
(defmacro polymode-register-exporter (exporter default &rest configs)
"Add EXPORTER to :exporters slot of all config objects in CONFIGS.
When DEFAULT is non-nil, also make EXPORTER the default exporter
for each polymode in CONFIGS."
`(dolist (pm ',configs)
(object-add-to-list (symbol-value pm) :exporters ',exporter)
(when ,default (oset (symbol-value pm) :exporter ',exporter))))
;;; GLOBAL EXPORTERS
(define-obsolete-variable-alias 'pm-exporter/pandoc 'poly-pandoc-exporter "v0.2")
(defcustom poly-pandoc-exporter
(pm-shell-exporter
:name "pandoc"
:from
'(;; ("json" "\\.json\\'" "JSON native AST" "pandoc %i -f json -t %t -o %o")
("markdown" "\\.md\\'" "pandoc's markdown" "pandoc %i -f markdown -t %t -o %o")
("markdown_strict" "\\.md\\'" "original markdown" "pandoc %i -f markdown_strict -t %t -o %o")
("markdown_phpextra" "\\.md\\'" "PHP markdown" "pandoc %i -f markdown_phpextra -t %t -o %o")
("markdown_phpextra" "\\.md\\'" "github markdown" "pandoc %i -f markdown_phpextra -t %t -o %o")
("textile" "\\.textile\\'" "Textile" "pandoc %i -f textile -t %t -o %o")
("rst" "\\.rst\\'" "reStructuredText" "pandoc %i -f rst -t %t -o %o")
("html" "\\.x?html?\\'" "HTML" "pandoc %i -f html -t %t -o %o")
("docbook" "\\.xml\\'" "DocBook" "pandoc %i -f docbook -t %t -o %o")
("mediawiki" "\\.wiki\\'" "MediaWiki" "pandoc %i -f mediawiki -t %t -o %o")
("latex" "\\.tex\\'" "LaTeX" "pandoc %i -f latex -t %t -o %o"))
:to
'(;; ("json" "json" "JSON version of native AST" "json")
("plain" "txt" "plain text" "plain")
("markdown" "md" "pandoc's extended markdown" "markdown")
("markdown_strict" "md" "original markdown" "markdown_strict")
("markdown_phpextra" "md" "PHP extended markdown" "markdown_phpextra")
("markdown_github" "md" "github extended markdown" "markdown_github")
("rst" "rst" "reStructuredText" "rst")
("html" "html" "XHTML 1" "html")
("html5" "html" "HTML 5" "html5")
("latex" "tex" "LaTeX" "latex")
("beamer" "tex" "LaTeX beamer" "beamer")
("context" "tex" "ConTeXt" "context")
("man" "man" "groff man" "man")
("mediawiki" "wiki" "MediaWiki markup" "mediawiki")
("textile" "textile" "Textile" "textile")
("org" "org" "Emacs Org-Mode" "org")
("texinfo" "info" "GNU Texinfo" "texinfo")
("docbook" "xml" "DocBook XML" "docbook")
("opendocument" "xml" "OpenDocument XML" "opendocument")
("odt" "odt" "OpenOffice text document" "odt")
("pdf" "pdf" "Portable Document Format" "latex")
("docx" "docx" "Word docx" "docx")
("epub" "epub" "EPUB book" "epub")
("epub3" "epub" "EPUB v3" "epub3")
("fb2" "fb" "FictionBook2 e-book" "fb2")
("asciidoc" "txt" "AsciiDoc" "asciidoc")
("slidy" "html" "Slidy HTML slide show" "slidy")
("slideous" "html" "Slideous HTML slide show" "slideous")
("dzslides" "html" "HTML5 slide show" "dzslides")
("s5" "html" "S5 HTML slide show" "s5")
("rtf" "rtf" "rich text format" "rtf"))
:function 'pm-default-shell-export-function
:sentinel 'pm-default-shell-export-sentinel)
"Pandoc exporter."
:type 'object)
(provide 'polymode-export)
;;; polymode-export.el ends here