393 lines
17 KiB
EmacsLisp
393 lines
17 KiB
EmacsLisp
|
|
(require 'polymode-core)
|
|||
|
|
(require 'polymode-classes)
|
|||
|
|
|
|||
|
|
(defgroup polymode-export nil
|
|||
|
|
"Polymode Exporters"
|
|||
|
|
:group 'polymode)
|
|||
|
|
|
|||
|
|
(defcustom polymode-exporter-output-file-format "%s"
|
|||
|
|
"Format of the exported files.
|
|||
|
|
%s is substituted with the current file name sans extension."
|
|||
|
|
:group 'polymode-export
|
|||
|
|
: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 that accepts at least
|
|||
|
|
one argument ACTION. 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' 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. 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 (lambda (&optional rest)
|
|||
|
|
(error "No callback defined for this exporter."))
|
|||
|
|
:type (or symbol function)
|
|||
|
|
:documentation
|
|||
|
|
"Callback function to be called by :function. There is no
|
|||
|
|
default callback. Callback must return the output file
|
|||
|
|
name."))
|
|||
|
|
"Class to represent asynchronous exporters.")
|
|||
|
|
|
|||
|
|
(defclass pm-shell-exporter (pm-exporter)
|
|||
|
|
((function
|
|||
|
|
:initform 'pm-default-shell-export-function)
|
|||
|
|
(sentinel
|
|||
|
|
:initarg :sentinel
|
|||
|
|
:initform 'pm-default-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.
|
|||
|
|
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"
|
|||
|
|
(pm--run-shell-command command sentinel "*polymode export*"
|
|||
|
|
(concat "Exporting " from "-->" to " with command:\n\n "
|
|||
|
|
command "\n\n")))
|
|||
|
|
|
|||
|
|
|
|||
|
|
;;; METHODS
|
|||
|
|
|
|||
|
|
(defgeneric pm-export (exporter from to &optional ifile)
|
|||
|
|
"Process IFILE with EXPORTER.")
|
|||
|
|
|
|||
|
|
(defmethod pm-export ((exporter pm-exporter) from to &optional ifile)
|
|||
|
|
(pm--process-internal exporter from to ifile))
|
|||
|
|
|
|||
|
|
(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)))
|
|||
|
|
|
|||
|
|
(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 (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 interactively with C-u argument, ask for FROM and TO
|
|||
|
|
interactively. See class `pm-exporter' for the complete
|
|||
|
|
specification."
|
|||
|
|
(interactive "P")
|
|||
|
|
(cl-flet ((to-name.id (el) (let* ((ext (funcall (cdr el) 'ext))
|
|||
|
|
(name (if ext
|
|||
|
|
(format "%s (%s)" (funcall (cdr el) 'doc) ext)
|
|||
|
|
(funcall (cdr el) 'doc))))
|
|||
|
|
(cons name (car el))))
|
|||
|
|
(from-name.id (el) (cons (funcall (cdr el) 'doc) (car el))))
|
|||
|
|
(let* ((exporter (symbol-value (or (oref pm/polymode :exporter)
|
|||
|
|
(polymode-set-exporter))))
|
|||
|
|
(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 file
|
|||
|
|
(let ((matched (cl-loop for el in (pm--selectors exporter :from)
|
|||
|
|
when (pm--selector-match (cdr el))
|
|||
|
|
collect (from-name.id el))))
|
|||
|
|
(when matched
|
|||
|
|
(if (> (length matched) 1)
|
|||
|
|
(cdr (pm--completing-read "Multiple `from' specs matched. Choose one: " matched))
|
|||
|
|
(cdar matched))))
|
|||
|
|
|
|||
|
|
;; 3. guess from weaver and return a cons (weaver-id . exporter-id)
|
|||
|
|
(let ((weaver (symbol-value (or (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 (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 (cdr 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
|
|||
|
|
(format "No `from' specs matched. Choose one: "
|
|||
|
|
(file-name-nondirectory fname) (eieio-object-name-string exporter))))
|
|||
|
|
(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 (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
|
|||
|
|
(cdr (pm--completing-read "Export to: " to-opts nil t nil 'pm--export:to-hist))))
|
|||
|
|
|
|||
|
|
;; B. string
|
|||
|
|
((stringp to)
|
|||
|
|
(if (assoc to (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 (oref pm/polymode :weaver)) (car from-id)))
|
|||
|
|
(pm-export exporter from-id to-id)))))
|
|||
|
|
|
|||
|
|
(defun polymode-set-exporter ()
|
|||
|
|
"Interactively set exporter for the current file."
|
|||
|
|
(interactive)
|
|||
|
|
(unless pm/polymode
|
|||
|
|
(error "No pm/polymode object found. Not in polymode buffer?"))
|
|||
|
|
(let* ((exporters (pm--abrev-names
|
|||
|
|
(delete-dups (pm--oref-with-parents pm/polymode :exporters))
|
|||
|
|
"pm-exporter/"))
|
|||
|
|
(sel (pm--completing-read "Choose exporter: " exporters nil t nil 'pm--exporter-hist))
|
|||
|
|
(out (intern (cdr sel))))
|
|||
|
|
(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 defaultp &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 ,defaultp (oset (symbol-value pm) :exporter ',exporter))))
|
|||
|
|
|
|||
|
|
|
|||
|
|
;;; GLOBAL EXPORTERS
|
|||
|
|
(defcustom pm-exporter/pandoc
|
|||
|
|
(pm-shell-exporter "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")
|
|||
|
|
("doocbook" "\\.xml\\'" "DocBook" "pandoc %i -f doocbook -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")
|
|||
|
|
("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-export-sentinel)
|
|||
|
|
"Pandoc exporter"
|
|||
|
|
:group 'polymode-export
|
|||
|
|
:type 'object)
|
|||
|
|
|
|||
|
|
(provide 'polymode-export)
|