org-pandoc-import/org-pandoc-import.el

381 lines
17 KiB
EmacsLisp

;;; org-pandoc-import.el --- Stay in org-mode as much as possible -*- lexical-binding: t; -*-
;; Copyright (C) 2020 TEC
;; Author: TEC <https://github/tecosaur>
;; Maintainer: TEC <tec@tecosaur.com>
;; Created: 16 Aug 2020
;; Modified: August 29, 2020
;; Version: 1.0
;; Keywords: org-mode, pandoc, convenience, files
;; Homepage: https://github.com/tecosaur/org-pandoc-import
;; Package-Requires: ((emacs "26.3"))
;;; License:
;; This file is part of org-pandoc-import, which is not part of GNU Emacs.
;;
;; org-pandoc-import 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 of the License, or
;; (at your option) any later version.
;;
;; org-pandoc-import 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 org-pandoc-import. If not, see <https://www.gnu.org/licenses/>.
;;
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; Leverage Pandoc to make convert non-org formats to org trivial.
;; Also supplies a minor mode to do on-the-fly conversion transparently
;; and automatically, exporting to the original format on save.
;;; Code:
(require 'subr-x)
(defgroup org-pandoc-import nil
"Provides methods to convert other markup files to Org."
:tag "Org Pandoc Import"
:group 'org)
(defcustom org-pandoc-import-executable "pandoc"
"Location of the pandoc binary."
:type 'string
:group 'org-pandoc-import)
(defcustom org-pandoc-import-buffer-name "*org-pandoc-import*"
"Name of the buffer to be created for the results of pandoc's conversion.
If a function, it is called to provide a string with the input file name
as the argument."
:type '(choice string function)
:group 'org-pandoc-import)
(defcustom org-pandoc-import-setup-defaults t
"If non-nil, set up a number of default import backends."
:type 'boolean
:group 'org-pandoc-import)
(defcustom org-pandoc-import-setup-folder
(file-name-directory
(eval-when-compile
(or (bound-and-true-p byte-compile-current-file)
load-file-name)))
"The default folder to look for filters and preprocessor files in.
Affects `org-pandoc-import-filters-folder' and `org-pandoc-import-preprocessor-folder'."
:type 'string
:group 'org-pandoc-import)
(defcustom org-pandoc-import-filters-folder
(expand-file-name
"filters" org-pandoc-import-setup-folder)
"Location of Lua filters for use with pandoc.
If FITERS/backend.lua exists, it will automatically be used when backend is registered."
:type 'string
:group 'org-pandoc-import)
(defcustom org-pandoc-import-preprocessor-folder
(expand-file-name
"preprocessors" org-pandoc-import-setup-folder)
"A file to but pre-processors in.
When a new backend is defined, if PREPROCESSORS/backend.el exists it will be
loaded, and if org-pandoc-import-backend-preprocessor exists (where backend a
placeholder for the actual backend name), it will be called with the input file
as the argument, and the result used as the new input file."
:type 'string
:group 'org-pandoc-import)
(defcustom org-pandoc-import-global-args nil
"List of arguments to apply to all backends.
Accepts three types of atoms:
- strings, which are passed as-is to `call-process'
- plist keywords, which have the : replaced with single dash if the word
is one charachter, else two dashes
- functions, which are evaluated and have the result passed as one
of the arguments"
:type 'list
:group 'org-pandoc-import)
(defcustom org-pandoc-import-global-filters
(directory-files org-pandoc-import-filters-folder nil "^_")
"List of filters to apply to all backends.
Either the name of a file contained in `org-pandoc-import-filters-folder', or
an absolute path to a filter.
Filters ending in '.lua' will be called with '--lua-filter', and all other
filters with '--filter'.
By default, all files starting with '_' in `org-pandoc-import-filters-folder'
are used."
:type 'list
:group 'org-pandoc-import)
(defvar org-pandoc-import-backends nil
"List of registerd org-pandoc-import backends.")
;;;###autoload
(defmacro org-pandoc-import-backend (name &optional recognised-extensions pandoc-type filters pandoc-args)
"Create an export backend named NAME.
The backend is applied by default on files which end in a RECOGNISED-EXTENSIONS.
This calls pandoc, specifying the input format to be PANDOC-TYPE.
PANDOC-ARGS is a list of args passed to the pandoc command in the same manner
as `org-pandoc-import-global-args'.
FILTERS can be either absolute paths to pandoc filters, or names of files
within `org-pandoc-import-filters-folder'.
RECOGNISED-EXTENSIONS defaults to '(\"NAME\"), and PANDOC-TYPE to \"NAME\"."
(let* ((s-name (symbol-name name))
(recognised-extensions (or recognised-extensions `'(,s-name)))
(pandoc-type (or pandoc-type s-name))
(format-args (intern (format "org-pandoc-import-%s-args" s-name)))
(format-extensions (intern (format "org-pandoc-import-%s-extensions" s-name)))
(format-filters (intern (format "org-pandoc-import-%s-filters" s-name)))
(preprocessor (let ((preprocessor-file
(expand-file-name (concat s-name ".el") org-pandoc-import-preprocessor-folder))
(preprocerror-func (intern (format "org-pandoc-import-%s-preprocessor" s-name))))
(when (file-exists-p preprocessor-file)
(load preprocessor-file nil t)
(when (fboundp preprocerror-func)
preprocerror-func))))
(common-args (list pandoc-type 'in-file format-extensions format-args format-filters
'syncronous-p (when preprocessor `(function ,preprocessor))))
(filters (or filters
(let ((filter (expand-file-name (concat s-name ".lua")
org-pandoc-import-filters-folder)))
(when (file-exists-p filter) (list filter))))))
`(progn
(defvar ,format-args ,pandoc-args
,(format "Arguments to be passed to pandoc when processing a %s file.
This is treated the same as `org-pandoc-import-global-args'" s-name))
(defvar ,format-extensions ,recognised-extensions
,(format "File extensions to recognise as associated with %s." s-name))
(defvar ,format-filters ',filters
,(format "Either an absolute path to, or the name of a filter within `org-pandoc-import-filters-folder'
to be applied when converting from %s.
This is treated the same as `org-pandoc-import-global-filters'" s-name))
(defun ,(intern (format "org-pandoc-import-%s-as-org" s-name)) (prompty &optional in-file syncronous-p)
,(format "Parse the provided %s IN-FILE to org-mode and open in a new buffer.
Recognises files with extensions which are a member of `org-pandoc-import-%s-extensions'.
Calls pandoc with arguments listed in `org-pandoc-import-%s-args', and filters `org-pandoc-import-%s-filters'." s-name s-name s-name s-name)
(interactive "P")
(org-pandoc-import-convert prompty nil ,@common-args))
(defun ,(intern (format "org-pandoc-import-%s-to-org" s-name)) (prompty &optional in-file out-file syncronous-p)
,(format "Parse the provided %s IN-FILE to org-mode and save to OUT-FILE - defulting to (file-name-base IN-FILE).org.
Recognises files with extensions which are a member of `org-pandoc-import-%s-extensions'.
Calls pandoc with arguments listed in `org-pandoc-import-%s-args', and filters `org-pandoc-import-%s-filters'." s-name s-name s-name s-name)
(interactive "P")
(org-pandoc-import-convert prompty (or out-file t) ,@common-args))
(add-to-list 'org-pandoc-import-backends ',name))))
(defun org-pandoc-import-convert (prompty out-file pandoc-type &optional in-file expected-extensions args filters syncronous-p preprocessor)
"Call pandoc on an IN-FILE.
Determines the relevant paramaters to convert IN-FILE of type PANDOC-TYPE to
either OUT-FILE, or a buffer (when OUT-FILE is nil).
If PROMPTY is non-nill, then the value of IN-FILE and (if applicable) OUT-FILE
will be always prompted for. A prompt for IN-FILE is also triggered when
IN-FILE is nil, or its extension is not a member of EXPECTED-EXTENSIONS.
A prompt for OUT-FILE is triggered when OUT-FILE is t, or the name of a
pre-existing file. Pandoc is then called with arguments from the list ARGS,
as described in `org-pandoc-import-global-args', and filters named in the list
FILTERS --- which can be either absolute paths to pandoc filters, or names of
files within `org-pandoc-import-filters-folder'.
If preprocessor is given, and a function, it is run with the value of IN-FILE.
The value returned is used as the new IN-FILE."
(let* ((in-file (expand-file-name
(or in-file
(if (and (not prompty)
(buffer-file-name)
(if expected-extensions
(member (file-name-extension (buffer-file-name))
expected-extensions))
t)
(buffer-file-name)
(read-file-name "File to convert: " nil nil t
(when expected-extensions
(concat "." (car expected-extensions))))))))
(in-file-processed (if (and preprocessor (functionp preprocessor))
(funcall preprocessor in-file)
in-file))
(in-file-org (concat (file-name-sans-extension in-file) ".org"))
(out-file (if (eq t out-file)
(if prompty
(read-file-name "File to write: "
(file-name-directory in-file)
nil nil
(concat (file-name-base in-file) ".org"))
in-file-org)
out-file))
(filter-args nil))
(if (and out-file (file-exists-p out-file))
(unless (yes-or-no-p (format "Overwrite file %s? "
(file-relative-name
out-file
(file-name-directory in-file))))
(setq out-file
(read-file-name "File to write: "
(file-name-directory in-file)))))
(dolist (filter (append filters org-pandoc-import-global-filters))
(setq filter-args
(append filter-args
(list (pcase (file-name-extension filter)
("lua" "--lua-filter")
(_ "--filter"))
(if (= ?/ (aref filter 0)) filter
(expand-file-name filter org-pandoc-import-filters-folder))))))
(org-pandoc-import-run-convert
(org-pandoc-import-generate-convert-arguments
in-file-processed pandoc-type out-file (append args filter-args))
in-file-processed out-file syncronous-p)))
(defun org-pandoc-import-run-convert (arguments in-file &optional out-file syncronous-p)
"Call pandoc on IN-FILE with ARGUMENTS, creating OUT-FILE if given.
`call-process' is used instead of `start-process' if SYNCRONOUS-P is non-nil."
(let* ((pandoc-buffer (generate-new-buffer
(if (functionp org-pandoc-import-buffer-name)
(funcall org-pandoc-import-buffer-name in-file)
org-pandoc-import-buffer-name)))
(default-directory (file-name-directory in-file))
(process
(if syncronous-p
(apply #'call-process
org-pandoc-import-executable
nil
pandoc-buffer
nil
arguments)
(apply #'start-process
"org-pandoc-import"
pandoc-buffer
org-pandoc-import-executable
arguments))))
(unless syncronous-p
(set-process-sentinel process (org-pandoc-import-process-sentinel pandoc-buffer out-file (time-to-seconds (current-time)))))))
(defun org-pandoc-import-process-sentinel (process-buffer &optional out-file start-time-seconds)
"Creats a lambda sentinel for a pandoc process, outputing to PROCESS-BUFFER.
If OUT-FILE is given, kill the PROCESS-BUFFER and use the file in its place.
When START-TIME-SECONDS is given, a messege is generated indicating the total
time elapsed."
(lambda (process _signal)
(pcase (process-status process)
('exit (if out-file
(progn (find-file out-file)
(kill-buffer process-buffer))
(switch-to-buffer process-buffer)
(goto-char (point-min)))
(when start-time-seconds
(message "Converted docunent in %3fs" (- (time-to-seconds (current-time)) start-time-seconds)))
(org-mode))
((or 'stop 'signal 'failed)
(user-error "The pandoc process to create %s has exited unexpectedly" out-file)
(switch-to-buffer process-buffer)))))
(defun org-pandoc-import-generate-convert-arguments (in-file target-format &optional out-file arguments-list)
"Format the provided arguments to be passed to pandoc.
Have pandoc convert IN-FILE of pandoc type TARGET-FORMAT to the org file
OUT-FILE (if given), with arguments given by ARGUMENTS-LIST."
(let (arguments)
(dolist (element (reverse (append arguments-list org-pandoc-import-global-args)))
(push
(cond
((stringp element) element)
((keywordp element) (let ((keyword (substring (symbol-name element) 1)))
(pp keyword)
(concat (if (= 1 (length keyword)) "-" "--")
keyword)))
((functionp element) (funcall element in-file target-format)))
arguments))
(append (list "-f" target-format
"-t" "org")
(when out-file
(list "-o" out-file))
arguments
(list in-file))))
;;;###autoload
(defun org-pandoc-import-as-org (prompty &optional in-file syncronous-p)
"Parse the provided file to `org-mode', and open in a new buffer.
With PROMPTY (given by the universal argument), always prompt for the IN-FILE
to act on.
This only works so long as these is backend registered in
`org-pandoc-import-backends' associated with the extension of the selected file.
See org-pandoc-import-{backend}-as-org for information on a particular backend.
When SYNCRONOUS-P is set, the pandoc process is run in a blocking manner."
(interactive "P")
(if-let ((backend (org-pandoc-import-find-associated-backend (or in-file (buffer-file-name)))))
(funcall (intern (format "org-pandoc-import-%s-as-org" (symbol-name backend)))
prompty in-file syncronous-p)
(funcall #'org-pandoc-import-as-org prompty (read-file-name "File to convert: " nil nil t) syncronous-p)))
;;;###autoload
(defun org-pandoc-import-to-org (prompty &optional in-file out-file syncronous-p)
"Parse the provided file to an `org-mode' file, and open.
With PROMPTY (given by the universal argument), always prompt for the IN-FILE to
act on, and the where to save the new Org file.
The result is saved to OUT-FILE, which defaults to IN-FILE but with the .org
extension.
This only works so long as these is backend registered in
`org-pandoc-import-backends' associated with the extension of the selected file.
See org-pandoc-import-{backend}-as-org for information on a particular backend.
When SYNCRONOUS-P is set, the pandoc process is run in a blocking manner."
(interactive "P")
(if-let ((backend (org-pandoc-import-find-associated-backend (or in-file (buffer-file-name)))))
(funcall (intern (format "org-pandoc-import-%s-to-org" (symbol-name backend)))
prompty in-file out-file syncronous-p)
(funcall #'org-pandoc-import-to-org prompty (read-file-name "File to convert: " nil nil t) syncronous-p)))
(defun org-pandoc-import-find-associated-backend (file)
"Find the backend symbol from associated with FILE's extension.
The backend is found from searching `org-pandoc-import-backends', and is nil
if no such match could be found."
(when file
(let ((ext (file-name-extension file))
the-backend)
(dolist (backend org-pandoc-import-backends)
(when (member ext (symbol-value
(intern
(format "org-pandoc-import-%s-extensions"
(symbol-name backend)))))
(setq the-backend backend)))
the-backend)))
(dont-compile
(when org-pandoc-import-setup-defaults
(org-pandoc-import-backend markdown '("md" "markdown"))
(org-pandoc-import-backend latex '("tex" "latex"))
(org-pandoc-import-backend rst)
(org-pandoc-import-backend odt)
(org-pandoc-import-backend docx)
(org-pandoc-import-backend rmarkdown '("rmd" "Rmd") "markdown")
(org-pandoc-import-backend ipynb)
(org-pandoc-import-backend csv)
(org-pandoc-import-backend tsv '("tsv") "csv")))
(provide 'org-pandoc-import)
;;; org-pandoc-import.el ends here