Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

449 lines
20 KiB

  1. ;;; polymode-export.el --- Exporting facilities for polymodes -*- lexical-binding: t -*-
  2. ;;
  3. ;; Copyright (C) 2013-2019, Vitalie Spinu
  4. ;; Author: Vitalie Spinu
  5. ;; URL: https://github.com/polymode/polymode
  6. ;;
  7. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  8. ;;
  9. ;; This file is *NOT* part of GNU Emacs.
  10. ;;
  11. ;; This program is free software; you can redistribute it and/or
  12. ;; modify it under the terms of the GNU General Public License as
  13. ;; published by the Free Software Foundation; either version 3, or
  14. ;; (at your option) any later version.
  15. ;;
  16. ;; This program is distributed in the hope that it will be useful,
  17. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  19. ;; General Public License for more details.
  20. ;;
  21. ;; You should have received a copy of the GNU General Public License
  22. ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
  23. ;;
  24. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  25. ;;
  26. ;;; Commentary:
  27. ;;
  28. ;;; Code:
  29. (require 'polymode-core)
  30. (require 'polymode-classes)
  31. (defgroup polymode-export nil
  32. "Polymode Exporters"
  33. :group 'polymode)
  34. (defcustom polymode-exporter-output-file-format "%s-exported"
  35. "Format of the exported files.
  36. %s is substituted with the current file name sans extension."
  37. :group 'polymode-export
  38. :type 'string)
  39. (defclass pm-exporter (pm-root)
  40. ((from
  41. :initarg :from
  42. :initform '()
  43. :type list
  44. :custom list
  45. :documentation
  46. "Input exporter specifications.
  47. This is an alist of elements of the form (id regexp doc
  48. commmand) or (id . selector). ID is the unique identifier of
  49. the spec. REGEXP is a regexp which, if matched on current
  50. file name, implies that the current file can be exported
  51. with this specification. DOC is a short help string shown
  52. during interactive export. COMMAND is the exporter
  53. command (string). It can contain the following format specs:
  54. %i - input file (no dir)
  55. %I - input file (full path)
  56. %o - output file (no dir)
  57. %O - output file (full path)
  58. %b - output file (base name only)
  59. %t - 4th element of the :to spec
  60. When specification is of the form (id . selector), SELECTOR
  61. is a function of variable arguments that accepts at least
  62. one argument ACTION. ACTION is a symbol and can be one of
  63. the following:
  64. match - must return non-nil if this specification
  65. applies to the file that current buffer is visiting,
  66. or :nomatch if specification does not apply. This
  67. selector can receive an optional file-name
  68. argument. In that case the decision must be made
  69. solely on that file and current buffer must be
  70. ignored. This is useful for matching exporters to
  71. weavers when exported file does not exist yet.
  72. regexp - return a string which is used to match input
  73. file name. If nil, `match' selector must return
  74. non-nil value. This selector is ignored if `match'
  75. returned non-nil.
  76. doc - return documentation string
  77. commmand - return a string with optional %i, %f,
  78. etc. format specs as described above. It will be
  79. passed to the processing :function.")
  80. (to
  81. :initarg :to
  82. :initform '()
  83. :type list
  84. :custom list
  85. :documentation
  86. "
  87. Output specifications alist. Each element is either a list
  88. of the form (id ext doc t-spec) or a cons (id . selector).
  89. In the former case EXT is an extension of the output file.
  90. DOC is a short documentation string. t-spec is a string what
  91. is substituted instead of %t in :from spec commmand.
  92. `t-spec' can be a list of one element '(command), in which
  93. case the whole :from spec command is substituted with
  94. command from %t-spec.
  95. When specification is of the form (id . selector), SELECTOR
  96. is a function of variable arguments with first two arguments
  97. being ACTION and ID of the specification. This function is
  98. called in a buffer visiting input file. ACTION is a symbol
  99. and can one of the following:
  100. output-file - return an output file name or a list of file
  101. names. Receives input-file as argument. If this
  102. command returns nil, the output is built from input
  103. file and value of 'output-ext command.
  104. This selector can also return a function. This
  105. function will be called in the callback or sentinel of
  106. the weaving process after the weaving was
  107. completed. This function should sniff the output of
  108. the process for errors or file names. It must return a
  109. file name, a list of file names or nil if no such
  110. files have been detected.
  111. ext - extension of output file. If nil and `output-file'
  112. also returned nil, the exporter won't be able to
  113. identify the output file and no automatic display or
  114. preview will be available.
  115. doc - return documentation string
  116. command - return a string to be used instead of
  117. the :from command. If nil, :from spec command is used.
  118. t-spec - return a string to be substituted as %t :from
  119. spec in :from command. If `command' selector returned
  120. non-nil, this spec is ignored.")
  121. (function
  122. :initarg :function
  123. :initform (lambda (command from to)
  124. (error "Function not defined for this exporter"))
  125. :type (or symbol function)
  126. :documentation
  127. "Function to process the commmand. Must take 3 arguments
  128. COMMAND, FROM-ID and TO-ID and return an output file name or
  129. a list of output file names. COMMAND is the 4th argument of
  130. :from spec with all the formats substituted. FROM-ID is the
  131. id of requested :from spec, TO-ID is the id of the :to
  132. spec."))
  133. "Root exporter class.")
  134. (defclass pm-callback-exporter (pm-exporter)
  135. ((callback
  136. :initarg :callback
  137. :initform nil
  138. :type (or symbol function)
  139. :documentation
  140. "Callback function to be called by function in :function
  141. slot. Callback must return an output file name or a list of
  142. output file-names. There is no default callback."))
  143. "Class to represent asynchronous exporters.
  144. :function slot must be a function with 4 arguments COMMAND,
  145. CALLBACK, FROM-ID and TO-ID.")
  146. (defclass pm-shell-exporter (pm-exporter)
  147. ((function
  148. :initform 'pm-default-shell-export-function)
  149. (sentinel
  150. :initarg :sentinel
  151. :initform 'pm-default-shell-export-sentinel
  152. :type (or symbol function)
  153. :documentation
  154. "Sentinel function to be called by :function when a shell
  155. call is involved. Sentinel should return the output file
  156. name.")
  157. (quote
  158. :initarg :quote
  159. :initform nil
  160. :type boolean
  161. :documentation "Non-nil when file arguments must be quoted
  162. with `shell-quote-argument'."))
  163. "Class to represent exporters that call external processes.")
  164. (defun pm-default-shell-export-function (command sentinel from to)
  165. "Run exporting COMMAND interactively to convert FROM to TO.
  166. Run command in a buffer (in comint-shell-mode) so that it accepts
  167. user interaction. This is a default function in all exporters
  168. that call a shell command. SENTINEL is the process sentinel."
  169. (pm--run-shell-command command sentinel "*polymode export*"
  170. (concat "Exporting " from "-->" to " with command:\n\n "
  171. command "\n\n")))
  172. ;;; METHODS
  173. (cl-defgeneric pm-export (exporter from to &optional ifile)
  174. "Process IFILE with EXPORTER.")
  175. (cl-defmethod pm-export ((exporter pm-exporter) from to &optional ifile)
  176. (pm--process-internal exporter from to ifile))
  177. (cl-defmethod pm-export ((exporter pm-callback-exporter) from to &optional ifile)
  178. (let ((cb (pm--wrap-callback exporter :callback ifile)))
  179. (pm--process-internal exporter from to ifile cb)))
  180. (cl-defmethod pm-export ((exporter pm-shell-exporter) from to &optional ifile)
  181. (let ((cb (pm--wrap-callback exporter :sentinel ifile)))
  182. (pm--process-internal exporter from to ifile cb (eieio-oref exporter 'quote))))
  183. ;; UI
  184. (defvar pm--exporter-hist nil)
  185. (defvar pm--export:from-hist nil)
  186. (defvar pm--export:from-last nil)
  187. (defvar pm--export:to-hist nil)
  188. (defvar pm--export:to-last nil)
  189. (declare-function polymode-set-weaver "polymode-weave")
  190. (declare-function pm-weave "polymode-weave")
  191. (defun polymode-export (&optional from to)
  192. "Export current file.
  193. FROM and TO are the ids of the :from and :to slots of the current
  194. exporter. If the current exporter hasn't been set yet, set the
  195. exporter with `polymode-set-exporter'. You can always change the
  196. exporter manually by invoking `polymode-set-exporter'.
  197. When FROM or TO are missing they are determined automatically
  198. from the current exporter's specifications and file's
  199. extension. If no appropriate export specification has been found,
  200. look into current weaver and try to match weaver's output to
  201. exporters input extension. When such combination is possible,
  202. settle on weaving first and exporting the weaved output. When
  203. none of the above worked, ask the user for `from' and `to' specs.
  204. When called with prefix argument, ask for FROM and TO
  205. interactively. See constructor function pm-exporter for the
  206. complete specification."
  207. (interactive "P")
  208. (cl-flet ((to-name.id (el) (let* ((ext (funcall (cdr el) 'ext (car el)))
  209. (name (if ext
  210. (format "%s (%s)" (funcall (cdr el) 'doc (car el)) ext)
  211. (funcall (cdr el) 'doc (car el)))))
  212. (cons name (car el))))
  213. (from-name.id (el) (cons (funcall (cdr el) 'doc (car el)) (car el))))
  214. (let* ((exporter (symbol-value (or (eieio-oref pm/polymode 'exporter)
  215. (polymode-set-exporter t))))
  216. (fname (file-name-nondirectory buffer-file-name))
  217. (gprompt nil)
  218. (case-fold-search t)
  219. (from-opts (mapcar #'from-name.id (pm--selectors exporter :from)))
  220. (from-id
  221. (cond
  222. ;; A: guess from spec
  223. ((null from)
  224. (or
  225. ;; 1. repeated export; don't ask
  226. pm--export:from-last
  227. ;; 2. select :from entries which match to current context
  228. (let ((matched (pm--matched-selectors exporter :from)))
  229. (when matched
  230. (if (> (length matched) 1)
  231. (cdr (pm--completing-read "Multiple `from' specs matched. Choose one: "
  232. (mapcar #'from-name.id matched)))
  233. (caar matched))))
  234. ;; 3. guess from weaver and return a cons (weaver-id . exporter-id)
  235. (let ((weaver (symbol-value (or (eieio-oref pm/polymode 'weaver)
  236. (progn
  237. (setq gprompt "Choose `from' spec: ")
  238. (polymode-set-weaver))))))
  239. (when weaver
  240. ;; fixme: weaver was not yet ported to selectors
  241. ;; fixme: currently only first match is returned
  242. (let ((pair (cl-loop for w in (eieio-oref weaver 'from-to)
  243. ;; weaver input extension matches the filename
  244. if (string-match-p (nth 1 w) fname)
  245. return (cl-loop for el in (pm--selectors exporter :from)
  246. ;; input exporter extensnion matches weaver output extension
  247. when (pm--selector-match el (concat "dummy." (nth 2 w)))
  248. return (cons (car w) (car el))))))
  249. (when pair
  250. (message "Matching weaver found. Weaving to '%s' first." (car pair))
  251. pair))))
  252. ;; 4. nothing matched; ask
  253. (let* ((prompt (or gprompt "No `from' specs matched. Choose one: "))
  254. (sel (pm--completing-read prompt from-opts nil t nil 'pm--export:from-hist)))
  255. (cdr sel))))
  256. ;; B: C-u, force a :from spec
  257. ((equal from '(4))
  258. (cdr (if (> (length from-opts) 1)
  259. (pm--completing-read "Input type: " from-opts nil t nil 'pm--export:from-hist)
  260. (car from-opts))))
  261. ;; C. string
  262. ((stringp from)
  263. (if (assoc from (eieio-oref exporter 'from))
  264. from
  265. (error "Cannot find `from' spec '%s' in %s exporter"
  266. from (eieio-object-name exporter))))
  267. ;; D. error
  268. (t (error "'from' argument must be nil, universal argument or a string"))))
  269. (to-opts (mapcar #'to-name.id (pm--selectors exporter :to)))
  270. (to-id
  271. (cond
  272. ;; A. guess from spec
  273. ((null to)
  274. (or
  275. ;; 1. repeated export; don't ask and use first entry in history
  276. (unless (equal from '(4))
  277. pm--export:to-last)
  278. ;; 2. First export or C-u
  279. (if (= (length to-opts) 1)
  280. (cdar to-opts)
  281. (cdr (pm--completing-read "Export to: " to-opts nil t nil 'pm--export:to-hist)))))
  282. ;; B. string
  283. ((stringp to)
  284. (if (assoc to (eieio-oref exporter 'to))
  285. to
  286. (error "Cannot find output spec '%s' in %s exporter"
  287. to (eieio-object-name exporter))))
  288. ;; C . Error
  289. (t (error "'to' argument must be nil or a string")))))
  290. (setq-local pm--export:from-last from-id)
  291. (setq-local pm--export:to-last to-id)
  292. (if (consp from-id)
  293. ;; run through weaver
  294. (let ((pm--export-spec (cons (cdr from-id) to-id))
  295. (pm--output-not-real t))
  296. (pm-weave (symbol-value (eieio-oref pm/polymode 'weaver)) (car from-id)))
  297. (pm-export exporter from-id to-id)))))
  298. (defun polymode-set-exporter (&optional no-ask-if-1)
  299. "Interactively set exporter for the current file.
  300. If NO-ASK-IF-1 is non-nil, don't ask if there is only one exporter."
  301. (interactive)
  302. (unless pm/polymode
  303. (error "No pm/polymode object found. Not in polymode buffer?"))
  304. (let* ((weavers (delete-dups (pm--oref-with-parents pm/polymode :weavers)))
  305. (exporters (pm--abrev-names
  306. "pm-exporter/\\|-exporter"
  307. (cl-delete-if-not
  308. (lambda (el)
  309. (or (pm--matched-selectors el :from)
  310. ;; FIXME: rewrite this abomination
  311. ;; Match weaver to the exporter.
  312. (cl-loop for weaver in weavers
  313. if (cl-loop for w in (eieio-oref (symbol-value weaver) 'from-to)
  314. ;; weaver input extension matches the filename
  315. if (string-match-p (nth 1 w) buffer-file-name)
  316. return (cl-loop for el in (pm--selectors (symbol-value el) :from)
  317. ;; input exporter extensnion matches weaver output extension
  318. when (pm--selector-match el (concat "dummy." (nth 2 w)))
  319. return t))
  320. return t)))
  321. (delete-dups (pm--oref-with-parents pm/polymode :exporters)))))
  322. (sel (if exporters
  323. (if (and no-ask-if-1 (= (length exporters) 1))
  324. (car exporters)
  325. (pm--completing-read "Choose exporter: " exporters nil t nil 'pm--exporter-hist))
  326. (user-error "No valid exporters in current context")))
  327. (out (intern (cdr sel))))
  328. (setq pm--exporter-hist (delete-dups pm--exporter-hist))
  329. (setq-local pm--export:from-last nil)
  330. (setq-local pm--export:to-last nil)
  331. (oset pm/polymode :exporter out)
  332. out))
  333. (defmacro polymode-register-exporter (exporter default &rest configs)
  334. "Add EXPORTER to :exporters slot of all config objects in CONFIGS.
  335. When DEFAULT is non-nil, also make EXPORTER the default exporter
  336. for each polymode in CONFIGS."
  337. `(dolist (pm ',configs)
  338. (object-add-to-list (symbol-value pm) :exporters ',exporter)
  339. (when ,default (oset (symbol-value pm) :exporter ',exporter))))
  340. ;;; GLOBAL EXPORTERS
  341. (define-obsolete-variable-alias 'pm-exporter/pandoc 'poly-pandoc-exporter "v0.2")
  342. (defcustom poly-pandoc-exporter
  343. (pm-shell-exporter
  344. :name "pandoc"
  345. :from
  346. '(;; ("json" "\\.json\\'" "JSON native AST" "pandoc %i -f json -t %t -o %o")
  347. ("markdown" "\\.md\\'" "pandoc's markdown" "pandoc %i -f markdown -t %t -o %o")
  348. ("markdown_strict" "\\.md\\'" "original markdown" "pandoc %i -f markdown_strict -t %t -o %o")
  349. ("markdown_phpextra" "\\.md\\'" "PHP markdown" "pandoc %i -f markdown_phpextra -t %t -o %o")
  350. ("markdown_phpextra" "\\.md\\'" "github markdown" "pandoc %i -f markdown_phpextra -t %t -o %o")
  351. ("textile" "\\.textile\\'" "Textile" "pandoc %i -f textile -t %t -o %o")
  352. ("rst" "\\.rst\\'" "reStructuredText" "pandoc %i -f rst -t %t -o %o")
  353. ("html" "\\.x?html?\\'" "HTML" "pandoc %i -f html -t %t -o %o")
  354. ("docbook" "\\.xml\\'" "DocBook" "pandoc %i -f docbook -t %t -o %o")
  355. ("mediawiki" "\\.wiki\\'" "MediaWiki" "pandoc %i -f mediawiki -t %t -o %o")
  356. ("latex" "\\.tex\\'" "LaTeX" "pandoc %i -f latex -t %t -o %o"))
  357. :to
  358. '(;; ("json" "json" "JSON version of native AST" "json")
  359. ("plain" "txt" "plain text" "plain")
  360. ("markdown" "md" "pandoc's extended markdown" "markdown")
  361. ("markdown_strict" "md" "original markdown" "markdown_strict")
  362. ("markdown_phpextra" "md" "PHP extended markdown" "markdown_phpextra")
  363. ("markdown_github" "md" "github extended markdown" "markdown_github")
  364. ("rst" "rst" "reStructuredText" "rst")
  365. ("html" "html" "XHTML 1" "html")
  366. ("html5" "html" "HTML 5" "html5")
  367. ("latex" "tex" "LaTeX" "latex")
  368. ("beamer" "tex" "LaTeX beamer" "beamer")
  369. ("context" "tex" "ConTeXt" "context")
  370. ("man" "man" "groff man" "man")
  371. ("mediawiki" "wiki" "MediaWiki markup" "mediawiki")
  372. ("textile" "textile" "Textile" "textile")
  373. ("org" "org" "Emacs Org-Mode" "org")
  374. ("texinfo" "info" "GNU Texinfo" "texinfo")
  375. ("docbook" "xml" "DocBook XML" "docbook")
  376. ("opendocument" "xml" "OpenDocument XML" "opendocument")
  377. ("odt" "odt" "OpenOffice text document" "odt")
  378. ("pdf" "pdf" "Portable Document Format" "latex")
  379. ("docx" "docx" "Word docx" "docx")
  380. ("epub" "epub" "EPUB book" "epub")
  381. ("epub3" "epub" "EPUB v3" "epub3")
  382. ("fb2" "fb" "FictionBook2 e-book" "fb2")
  383. ("asciidoc" "txt" "AsciiDoc" "asciidoc")
  384. ("slidy" "html" "Slidy HTML slide show" "slidy")
  385. ("slideous" "html" "Slideous HTML slide show" "slideous")
  386. ("dzslides" "html" "HTML5 slide show" "dzslides")
  387. ("s5" "html" "S5 HTML slide show" "s5")
  388. ("rtf" "rtf" "rich text format" "rtf"))
  389. :function 'pm-default-shell-export-function
  390. :sentinel 'pm-default-shell-export-sentinel)
  391. "Pandoc exporter."
  392. :group 'polymode-export
  393. :type 'object)
  394. (provide 'polymode-export)
  395. ;;; polymode-export.el ends here