diff --git a/pandoc-mode.el b/pandoc-mode.el new file mode 100644 index 0000000..c16a835 --- /dev/null +++ b/pandoc-mode.el @@ -0,0 +1,898 @@ +;; pandoc-mode.el v0.1.6 +;; +;; Copyright (c) 2009 Joost Kremers +;; All rights reserved. +;; +;; Redistribution and use in source and binary forms, with or without +;; modification, are permitted provided that the following conditions +;; are met: +;; +;; 1. Redistributions of source code must retain the above copyright +;; notice, this list of conditions and the following disclaimer. +;; 2. Redistributions in binary form must reproduce the above copyright +;; notice, this list of conditions and the following disclaimer in the +;; documentation and/or other materials provided with the distribution. +;; 3. The name of the author may not be used to endorse or promote products +;; derived from this software without specific prior written permission. +;; +;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +;; IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES ; LOSS OF USE, +;; DATA, OR PROFITS ; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +;; + +(require 'easymenu) + +(defmacro nor (&rest args) + "Returns T if none of its arguments are true." + `(not (or ,@args))) + +(defgroup pandoc nil "Minor mode for interacting with pandoc." :group 'Wp) + +(defcustom pandoc-binary "/usr/bin/pandoc" + "*The full path of the pandoc binary." + :group 'pandoc + :type 'file) + +(defcustom pandoc-markdown2pdf-script "/usr/bin/markdown2pdf" + "*The full path of the markdown2pdf script." + :group 'pandoc + :type 'file) + +(defcustom pandoc-directives '(("include" . pandoc-process-include-directive) + ("lisp" . pandoc-process-lisp-directive)) + "*List of directives to be processed before pandoc is called. +The directive must be given without `@@'; the function should +return a string that will replace the directive and its +argument (if any). + +The directives are processed in the order in which they appear in +this list. If a directive produces output that contains another +directive, the new directive will only be processed if it is of +the same type (i.e., an @@include directive loading a text that +also contains @@include directives) or if it is lower on the +list, not if it appears higher on the list." + :group 'pandoc + :type '(alist :key-type (string :tag "Directive") :value-type function)) + +(defcustom pandoc-directives-hook nil + "*List of functions to call before the directives are processed." + :group 'pandoc + :type '(repeat function)) + +(defvar pandoc-major-modes + '((haskell-mode . "native") + (text-mode . "markdown") + (markdown-mode . "markdown") + (rst-mode . "rst") + (html-mode . "html") + (latex-mode . "latex"))) + +(defvar pandoc-input-formats + '("native" + "markdown" + "rst" + "html" + "latex") + "List of pandoc input formats.") + +(defvar pandoc-output-formats + '(("native" . ".hs") + ("markdown" . ".text") + ("rst" . ".rst") + ("html" . ".html") + ("latex" . ".tex") + ("context" . ".tex") + ("man" . "") + ("mediawiki" . ".mw") + ("texinfo" . ".texi") + ("docbook" . ".xml") + ("opendocument" . ".odf") + ("odt" . ".odt") + ("s5" . ".html") + ("rtf" . ".rtf")) + "List of pandoc output formats plus file extensions.") + +(defvar pandoc-switches + '(standalone tab-stop + preserve-tabs reference-links + strict smart + parse-raw jsmath + latexmathml mimetex + gladtex number-sections + incremental sanitize-html + no-wrap table-of-contents css + email-obfuscation include-before-body + include-in-header custom-header title-prefix + include-after-body) + "List of switches accepted by the pandoc binary. Switches that + need special treatment (--read, --write and --output) are not + in this list.") + +(defvar pandoc-binary-switches + '(("gladTeX" . gladtex) + ("Incremental" . incremental) + ("No Wrap" . no-wrap) + ("Number Sections" . number-sections) + ("Parse Raw" . parse-raw) + ("Preserve Tabs" . preserve-tabs) + ("Reference Links" . reference-links) + ("Sanitize HTML" . sanitize-html) + ("Smart" . smart) + ("Standalone" . standalone) + ("Strict" . strict) + ("Table of Contents" . table-of-contents))) + +(defvar pandoc-options + '((read) ; see pandoc-input-formats + (read-lhs) ; input is literal Haskell + (write . "native") ; see pandoc-output-formats + (write-lhs) ; output is literal Haskell + + (output) ; a string + ; NIL means stdout (redirected to a temp buffer) + ; T means create output filename on the basis of + ; the input file name and the output format. + + (css) ; a file or NIL + (include-in-header) ; a file or NIL + (include-before-body) ; a file or NIL + (include-after-body) ; a file or NIL + (custom-header) ; a file or NIL + + (tab-stop) ; an integer or NIL + (title-prefix) ; a string or NIL + (latexmathml) ; a string or NIL + (jsmath) ; a string or NIL + (mimetex) ; a string, NIL or T + + (email-obfuscation) ; nil (="none"), "javascript" or "references" + + (gladtex) ; NIL, T + (incremental) ; NIL, T + (no-wrap) ; NIL, T + (number-sections) ; NIL, T + (parse-raw) ; NIL, T + (preserve-tabs) ; NIL, T + (reference-links) ; NIL, T + (sanitize-html) ; NIL, T + (smart) ; NIL, T + (standalone) ; NIL, T + (strict) ; NIL, T + (table-of-contents) ; NIL, T + + ;; this is not actually a pandoc option: + (output-dir)) ; a string; NIL means use input directory. + "Pandoc option alist.") + +(defvar pandoc-local-options nil "A buffer-local variable holding a file's pandoc options.") +(make-variable-buffer-local 'pandoc-local-options) + +(defvar pandoc-project-options nil "A buffer-local variable holding a file's project options.") +(make-variable-buffer-local 'pandoc-project-options) + +(defvar pandoc-settings-modified-flag nil "T if the current settings were modified and not saved.") +(make-variable-buffer-local 'pandoc-settings-modified-flag) + +(defvar pandoc-output-buffer (get-buffer-create " *Pandoc output*")) + +(defvar pandoc-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-c/r" 'pandoc-run-pandoc) + (define-key map "\C-c/p" 'pandoc-run-markdown2pdf) + (define-key map "\C-c/s" 'pandoc-save-settings-file) + (define-key map "\C-c/Ps" 'pandoc-save-project-file) + (define-key map "\C-c/Pu" 'pandoc-undo-file-settings) + (define-key map "\C-c/w" 'pandoc-set-write) + (define-key map "\C-c/v" 'pandoc-view-output) + (define-key map "\C-c/V" 'pandoc-view-settings) + (define-key map "\C-c/oo" 'pandoc-set-output) + (define-key map "\C-c/oc" 'pandoc-set-css) + (define-key map "\C-c/oH" 'pandoc-set-include-in-header) + (define-key map "\C-c/oB" 'pandoc-set-include-before-body) + (define-key map "\C-c/oA" 'pandoc-set-include-after-body) + (define-key map "\C-c/oC" 'pandoc-set-custom-header) + (define-key map "\C-c/oT" 'pandoc-set-title-prefix) + (define-key map "\C-c/ot" 'pandoc-set-tab-stop) + (define-key map "\C-c/om" 'pandoc-set-latexmathml) + (define-key map "\C-c/oj" 'pandoc-set-jsmath) + (define-key map "\C-c/oM" 'pandoc-set-mimetex) + (define-key map "\C-c/oe" 'pandoc-set-email-obfuscation) + (define-key map "\C-c/oD" 'pandoc-set-output-dir) + (define-key map "\C-c/t" 'pandoc-toggle-interactive) + map) + "Keymap for pandoc-mode.") + +(define-minor-mode pandoc-mode + "Minor mode for interacting with Pandoc." + :init-value nil :lighter (:eval (concat " Pandoc/" (pandoc-get 'write))) :global nil + (setq pandoc-local-options (copy-alist pandoc-options)) + (pandoc-set 'read (cdr (assq major-mode pandoc-major-modes))) + (setq pandoc-settings-modified-flag nil)) + +(defun turn-on-pandoc () + "Unconditionally turn on pandoc-mode." + (interactive) + (pandoc-mode 1)) + +(defun turn-off-pandoc () + "Unconditionally turn off pandoc-mode" + (interactive) + (pandoc-mode -1)) + +(defun conditionally-turn-on-pandoc () + "Turn on pandoc-mode if a pandoc settings file exists. +This is for use in major mode hooks." + (when (file-exists-p (pandoc-create-settings-filename 'settings (buffer-file-name) "default")) + (turn-on-pandoc))) + +(defun pandoc-set (option value) + "Sets the local value of OPTION to VALUE." + (when (assq option pandoc-local-options) + (setcdr (assq option pandoc-local-options) value) + (setq pandoc-settings-modified-flag t))) + +(defun pandoc-set* (option value) + "Sets the project value of OPTION to VALUE." + (when (assq option pandoc-project-options) + (setcdr (assq option pandoc-project-options) value) + (setq pandoc-settings-modified-flag t))) + +(defun pandoc-get (option &optional buffer) + "Returns the local value of OPTION." + (cdr (assq option (if buffer + (buffer-local-value 'pandoc-local-options buffer) + pandoc-local-options)))) + +(defun pandoc-get* (option) + "Returns the project value of OPTION." + (cdr (assq option pandoc-project-options))) + +(defun pandoc-toggle (option) + "Toggles the local value of an on/off option." + (pandoc-set option (not (pandoc-get option)))) + +(defun pandoc-toggle* (option) + "Toggles the project value of an on/off option." + (pandoc-set* option (not (pandoc-get* option)))) + +(defun pandoc-create-settings-filename (type filename output-format) + "Create a settings filename. +TYPE is the type of settings file, either 'settings or 'project. +FILENAME should be an absolute filename, the return value is an +absolute filename as well." + (cond + ((eq type 'settings) + (concat (file-name-directory filename) "." (file-name-nondirectory filename) "." output-format ".pandoc")) + ((eq type 'project) + (concat (file-name-directory filename) "Project." output-format ".pandoc")))) + +(defun pandoc-create-command-option-list (input-file &optional pdf) + "Create a list of strings with pandoc switches for the current buffer. +INPUT-FILE is the name of the input file. If PDF is non-nil, an +output file is always set, derived either from the input file or +from the output file set for the \"latex\" output profile, and +gets the suffix `.pdf'. If the output format is \"odt\" but no +output file is specified, one will be created, since pandoc does +not support output to stdout for odt." + (let ((read (format "--read=%s%s" (pandoc-get 'read) (if (pandoc-get 'read-lhs) "+lhs" ""))) + (write (if pdf + nil + (format "--write=%s%s" (pandoc-get 'write) (if (pandoc-get 'write-lhs) "+lhs" "")))) + (output (cond + ((or (eq (pandoc-get 'output) t) ; if the user set the output file to T + (and (null (pandoc-get 'output)) ; or if the user set no output file but either + (or pdf ; (i) we're running markdown2pdf, or + (string= (pandoc-get 'write) "odt")))) ; (ii) the output format is odt + (format "--output=%s/%s%s" ; we create an output file name. + (or (pandoc-get 'output-dir) + (file-name-directory input-file)) + (file-name-sans-extension (file-name-nondirectory input-file)) + (if pdf + ".pdf" + (cdr (assoc (pandoc-get 'write) pandoc-output-formats))))) + ((stringp (pandoc-get 'output)) ; if the user set an output file, + (format "--output=%s/%s" ; we combine it with the output directory + (or (pandoc-get 'output-dir) + (file-name-directory input-file)) + (if pdf ; and check if we're running markdown2pdf + (concat (file-name-sans-extension (pandoc-get 'output)) ".pdf") + (pandoc-get 'output)))) + (t nil))) + (other-options (mapcar #'(lambda (switch) + (let ((value (pandoc-get switch))) + (cond + ((eq value t) (format "--%s" switch)) + ((stringp value) (format "--%s=%s" switch value)) + (t nil)))) + pandoc-switches))) + (delq nil (append (list read write output) other-options)))) + +(defun pandoc-process-directives () + "Processes pandoc-mode @@-directives in the current buffer." + (interactive) + (mapc #'funcall pandoc-directives-hook) + (let ((case-fold-search nil)) + (mapc #'(lambda (directive) + (goto-char (point-min)) + (while (re-search-forward (concat "\\([\\]?\\)@@" (car directive)) nil t) + (if (string= (match-string 1) "\\") + (delete-region (match-beginning 1) (match-end 1)) + (let ((@@-beg (match-beginning 0)) + (@@-end (match-end 0))) + (cond + ((eq (char-after) ?{) ; if there is an argument + ;; note: point is on the left brace, while scan-lists + ;; returns the position *after* the right brace, so we + ;; need to adjust to get the actual argument. + (let* ((arg-beg (1+ (point))) + (arg-end (1- (scan-lists (point) 1 0))) + (text (buffer-substring-no-properties arg-beg arg-end))) + (goto-char @@-beg) + (delete-region @@-beg (1+ arg-end)) + (insert (funcall (cdr directive) text))) + (goto-char @@-beg)) + ((looking-at "[a-zA-Z0-9]") t) ; this means we're actually + ; dealing with a different directive + (t (goto-char @@-beg) + (delete-region @@-beg @@-end) ; else there is no argument + (insert (funcall (cdr directive))) + (goto-char @@-beg))))))) + pandoc-directives))) + +(defun pandoc-process-lisp-directive (lisp) + "Process @@lisp directives." + (format "%s" (eval (car (read-from-string lisp))))) + +(defun pandoc-process-include-directive (include-file) + "Process @@include directives." + (with-temp-buffer + (insert-file-contents include-file) + (buffer-string))) + +(defun pandoc-call-external (buffer output-format &optional pdf) + "Call pandoc on the current document. +This function creates a temporary buffer and sets up the required +local options. BUFFER is the buffer whose contents must be sent +to pandoc. Its contents is copied into the temporary buffer, the +@@-directives are processed, after which pandoc called. + +OUTPUT-FORMAT is the format to use. If nil, BUFFER's output +format is used. + +If PDF is non-nil, markdown2pdf is called instead of pandoc." + (let ((filename (buffer-file-name buffer)) + (command (if pdf pandoc-markdown2pdf-script pandoc-binary))) + (with-temp-buffer ; we do this in a temp buffer so we can process @@-directives without having to undo them. + (if (and output-format ; if an output format was provided (and the buffer is visiting a file) + filename) ; we want to use settings for that format or no settings at all. + (unless (pandoc-load-settings-for-file (expand-file-name filename) output-format t) + (setq pandoc-local-options (copy-alist pandoc-options) + pandoc-project-options (copy-alist pandoc-options)) + (pandoc-set 'write output-format) + (pandoc-set 'read (pandoc-get 'read buffer))) + (setq pandoc-local-options (buffer-local-value 'pandoc-local-options buffer)) + (setq pandoc-project-options (buffer-local-value 'pandoc-project-options buffer))) + (let ((option-list (pandoc-create-command-option-list filename pdf))) + (insert-buffer-substring-no-properties buffer) + (message "Running %s..." (file-name-nondirectory command)) + (pandoc-process-directives) + (with-current-buffer pandoc-output-buffer + (erase-buffer) + (insert (format "Running `%s %s'\n\n" (file-name-nondirectory command) (mapconcat #'identity option-list " ")))) + (if (= 0 (apply 'call-process-region (point-min) (point-max) command nil pandoc-output-buffer t option-list)) + (message "Running %s... Finished." (file-name-nondirectory command)) + (message "Error in %s process." (file-name-nondirectory command)) + (display-buffer pandoc-output-buffer)))))) + +(defun pandoc-run-pandoc (prefix) + "Run pandoc on the current document. +If called with a prefix argument, the user is asked for an output +format. Otherwise, the output format currently set in the buffer +is used." + (interactive "P") + (pandoc-call-external (current-buffer) + (if prefix + (completing-read "Output format to use: " pandoc-output-formats nil t) + nil))) + +(defun pandoc-run-markdown2pdf (prefix) + "Run markdown2pdf on the current document. +If there is a settings and/or project file for LaTeX output, the +options in them are used. If called with a prefix argument, +however, no check for the existence of LaTeX settings is made and +the buffer's current settings are used." + (interactive "P") + (pandoc-call-external (current-buffer) + (if prefix nil "latex") + t)) + +(defun pandoc-set-default-format () + "Sets the current output format as default. +This is done by creating a symbolic link to the relevant settings +files. (Therefore, this function is not available on Windows.)" + (interactive) + (if (eq system-type 'windows-nt) + (message "This option is not available on MS Windows") + (let ((current-settings-file (file-name-nondirectory (pandoc-create-settings-filename 'settings (buffer-file-name) (pandoc-get 'write)))) + (current-project-file (file-name-nondirectory (pandoc-create-settings-filename 'project (buffer-file-name) (pandoc-get 'write))))) + (when (not (file-exists-p current-settings-file)) + (pandoc-save-settings 'settings (pandoc-get 'write))) + (make-symbolic-link current-settings-file (pandoc-create-settings-filename 'settings (buffer-file-name) "default") t) + (when (file-exists-p current-project-file) + (make-symbolic-link current-project-file (pandoc-create-settings-filename 'project (buffer-file-name) "default") t)) + (message "`%s' set as default output format." (pandoc-get 'write))))) + +(defun pandoc-save-settings-file () + "Save the settings of the current buffer. +This function just calls pandoc-save-settings with the +appropriate output format." + (interactive) + (pandoc-save-settings 'settings (pandoc-get 'write))) + +(defun pandoc-save-project-file () + "Save the current settings as a project file. +In order to achieve this, the current local settings are copied +to the project settings." + (interactive) + (setq pandoc-project-options (copy-alist pandoc-local-options)) + (pandoc-save-settings 'project (pandoc-get 'write))) + +(defun pandoc-save-settings (type format &optional no-confirm) + "Save the settings of the current buffer for FORMAT. +TYPE must be a quoted symbol and specifies the type of settings +file. If its value is 'settings, a normal settings file is +created for the current file. If TYPE's value is 'project, a +project settings file is written. If optional argument NO-CONFIRM +is non-nil, any existing settings file is overwritten without +asking." + (let ((settings-file (pandoc-create-settings-filename type (buffer-file-name) format)) + (filename (buffer-file-name)) + ;; If TYPE is 'settings, we only need the options in + ;; pandoc-local-options that differ from pandoc-project-options. Note + ;; that we convert all values to strings, so that options that are nil + ;; in pandoc-local-options but non-nil in pandoc-project-options are + ;; also saved below. + (options (cond ((eq type 'settings) (delq nil (mapcar #'(lambda (option) + (when (not (equal (pandoc-get option) + (pandoc-get* option))) + (cons option (format "%s" (pandoc-get option))))) + (mapcar #'car pandoc-options)))) + ((eq type 'project) pandoc-project-options)))) + (if (and (not no-confirm) + (file-exists-p settings-file) + (not (y-or-n-p (format "%s file `%s' already exists. Overwrite? " + (capitalize (symbol-name type)) + (file-name-nondirectory settings-file))))) + (message "%s file not written." (capitalize (symbol-name type)))) + (with-temp-buffer + (insert (format "# pandoc-mode %s file for %s #\n" + type + (file-name-nondirectory filename)) + (format "# saved on %s #\n\n" (format-time-string "%Y.%m.%d %H:%M"))) + (pandoc-insert-options options) + (let ((make-backup-files nil)) + (write-region (point-min) (point-max) settings-file)) + (message "%s file written to `%s'." (capitalize (symbol-name type)) (file-name-nondirectory settings-file))) + (setq pandoc-settings-modified-flag nil))) + +(defun pandoc-insert-options (options) + "Insert OPTIONS in the current buffer. +Options are written out in the format