user-error for an error, rather than message.
[pelican-mode.git] / pelican-mode.el
index dce4bb8..3691c2f 100644 (file)
@@ -3,7 +3,7 @@
 ;; Copyright 2013-2017 Joe Wreschnig
 ;;
 ;; Author: Joe Wreschnig <joe.wreschnig@gmail.com>
-;; Package-Version: 20170618
+;; Package-Version: 20170730
 ;; Package-Requires: ((emacs "25"))
 ;; Keywords: convenience, editing
 ;;
 
 ;;; Code:
 
+(require 'seq)
 (require 'subr-x)
 
+(defgroup pelican-mode nil
+  "Support for Pelican posts and pages."
+  :group 'convenience)
+
+(defcustom pelican-mode-default-page-fields
+  '(:slug slug)
+  "Fields to include when creating a new page.
+
+See the documentation for `pelican-field' for more information
+about metadata fields and special values."
+  :group 'pelican-mode
+  :type '(plist))
+
+(defcustom pelican-mode-default-post-fields
+  '(:date now :status "draft" :slug slug)
+  "Fields to include when creating a new post.
+
+See the documentation for `pelican-field' for more information
+about metadata fields and special values."
+  :group 'pelican-mode
+  :type '(plist))
+
 (defun pelican-timestamp (&optional time)
   "Generate a Pelican-compatible timestamp for TIME."
   (format-time-string "%Y-%m-%d %H:%M" time))
 
-(defun pelican-is-markdown ()
-  "Check if the buffer is likely using Markdown."
-  (derived-mode-p 'markdown-mode))
-
 (defun pelican-field (name value)
-  "Format a line for a field NAME with a VALUE."
+  "Format a line for a field NAME with a VALUE.
+
+NAME may be a string or a symbol; if it is a symbol, the
+symbol name is used (removing a leading ':' if present).
+
+VALUE may be any value; except for the following special values,
+the unquoted printed representation of it is used:
+
+- `now' means the current time; see `pelican-timestamp'.
+
+- `slug' means the file's path relative to the document root sans
+  extension; see `pelican-default-slug'.
+
+- nil or an empty strings means return an empty string, without
+  any name or value."
+  (setq value (pcase value
+                ('now (pelican-timestamp))
+                ('slug (pelican-default-slug))
+                ('"" nil)
+                (_ value)))
+  (when (symbolp name)
+    (setq name (string-remove-prefix ":" (symbol-name name))))
   (if value
       (cond ((derived-mode-p 'markdown-mode)
              (format "%s: %s\n" (capitalize name) value))
@@ -50,7 +90,7 @@
     ""))
 
 (defun pelican-rst-title (title)
-  "Create a ReSt version of TITLE."
+  "Format a reStructureText version of TITLE."
   (concat title "\n" (make-string (string-width title) ?#) "\n\n"))
 
 (defun pelican-title (title)
          (pelican-rst-title title))
         (t (error "Unsupported major mode %S" major-mode))))
 
-(defun pelican-header (title date status category tags slug)
-  "Create a Pelican header."
-  ;; TODO: Use a property list (-> alist via seq-partition) instead.
-  (when (eq date t)
-    (setq date (pelican-timestamp)))
-  
+(defun pelican-header (title &rest fields)
+  "Generate a Pelican header for a post with a TITLE and metadata FIELDS."
   (concat (pelican-title title)
-          (pelican-field "date" date)
-          (pelican-field "status" status)
-          (pelican-field "tags" tags)
-          (pelican-field "category" category)
-          (pelican-field "slug" slug)
+          (mapconcat (apply-partially #'apply #'pelican-field)
+                     (seq-partition fields 2) "")
           "\n"))
 
+(defun pelican-insert-header (title &rest fields)
+  "Insert a Pelican header for a post with a TITLE and metadata FIELDS."
+  (save-excursion
+    (goto-char 0)
+    (insert (apply #'pelican-header (cons title fields)))))
+
 (defun pelican-insert-draft-post-header (title tags)
-  "Insert a Pelican header for a draft post."
+  "Insert a Pelican header for a draft with a TITLE and TAGS."
   (interactive "sPost title: \nsTags: ")
-  (let ((slug (pelican-default-slug)))
-    (save-excursion
-      (goto-char 0)
-      (insert (pelican-header title 't "draft" nil tags slug)))))
+  (apply #'pelican-insert-header
+         `(,title ,@pelican-mode-default-post-fields :tags ,tags)))
 
-(defun pelican-insert-page-header (title hidden)
-  "Insert a Pelican header for a page."
+(defun pelican-insert-page-header (title &optional hidden)
+  "Insert a Pelican header for a page with a TITLE, potentially HIDDEN."
   (interactive
    (list (read-string "Page title: ")
          (y-or-n-p "Hidden? ")))
-  (let ((slug (pelican-default-slug))
-        (hidden (if hidden "hidden" nil)))
-    (save-excursion
-      (goto-char 0)
-      (insert (pelican-header title nil hidden nil nil slug)))))
+  (apply #'pelican-insert-header
+         `(,title ,@pelican-mode-default-page-fields
+                  :hidden ,(when hidden "hidden"))))
 
-(defun pelican-insert-header ()
+(defun pelican-insert-auto-header ()
   "Insert a Pelican header for a page or post."
   (interactive)
-  (call-interactively (if (pelican-is-page)
-                          'pelican-insert-page-header
-                        'pelican-insert-draft-post-header)))
+  (call-interactively
+   (if (pelican-page-p)
+       #'pelican-insert-page-header
+     #'pelican-insert-draft-post-header)))
 
 (defun pelican-set-field (field value)
   "Set FIELD to VALUE."
   (interactive "sField: \nsValue: ")
   (save-excursion
     (goto-char 0)
+    (when (and (derived-mode-p 'rst-mode)
+               (re-search-forward "^#" nil t))
+      (forward-line 2))
     (if (re-search-forward (concat "^" (pelican-field field ".+*")) nil t)
         (replace-match (pelican-field field value))
-      (re-search-forward "#")
-      (forward-line 2)
-      (re-search-forward "^$")
-      (replace-match (pelican-field field value)))))
+      (when value
+        (re-search-forward "^$")
+        (replace-match (pelican-field field value))))))
+
+(defun pelican-remove-field (field)
+  "Remove FIELD."
+  (pelican-set-field field nil))
 
 (defun pelican-set-title (title)
   "Set the title to TITLE."
   (interactive "sTitle: ")
-  (if (pelican-is-markdown)
+  (if (derived-mode-p 'markdown-mode)
       (pelican-set-field "title" title)
     (save-excursion
       (goto-char 0)
 (defun pelican-update-date ()
   "Update a Pelican date header."
   (interactive)
-  (pelican-set-field "date" (pelican-timestamp)))
+  (pelican-set-field :date 'now))
 
 (defun pelican-publish-draft ()
   "Remove draft status from a Pelican post."
   (interactive)
-  (pelican-set-field "status" nil)
+  (pelican-remove-field :status)
   (pelican-update-date))
 
-(defun pelican-is-page ()
+(defun pelican-page-p ()
   "Guess the current buffer is a Pelican page (vs. a post or neither)."
   (when-let (pelican-base (pelican-find-root))
     (let* ((relative (file-relative-name buffer-file-name pelican-base))
   (when-let (conf (pelican-find-in-parents "pelicanconf.py"))
     (file-name-directory conf)))
 
-(defun pelican-is-in-site ()
+(defun pelican-site-p ()
   "Check if this buffer is under a Pelican site."
   (not (null (pelican-find-root))))
 
 (defun pelican-make (target)
   "Execute TARGET in a Makefile at the root of the site."
   (interactive "sMake Pelican target: ")
-  (if-let ((default-directory (pelican-find-root)))
-      (let ((output (get-buffer-create "*Pelican Output*")))
-        (display-buffer output)
-        (pop-to-buffer output)
-        (compilation-mode)
-        (start-process "Pelican Makefile" output "make" target))
-    (message "This doesn't look like a Pelican site.")))
+  (if-let (default-directory (pelican-find-root))
+      (compilation-start (format "make %s" target)
+                         nil (lambda (_) "*pelican*"))
+    (user-error "This doesn't look like a Pelican site")))
 
 (defun pelican-make-html ()
   "Generate HTML via a Makefile at the root of the site."
   (interactive)
   (pelican-make "rsync_upload"))
 
-(defconst pelican-keymap (make-sparse-keymap)
-  "The default keymap used in Pelican mode.")
-(define-key pelican-keymap (kbd "C-c P n")
-  'pelican-insert-header)
-(define-key pelican-keymap (kbd "C-c P p")
-  'pelican-publish-draft)
-(define-key pelican-keymap (kbd "C-c P t")
-  'pelican-update-date)
-(define-key pelican-keymap (kbd "C-c P h")
-  'pelican-make-html)
-(define-key pelican-keymap (kbd "C-c P u")
-  'pelican-make-rsync-upload)
-
-
 ;;;###autoload
 (define-minor-mode pelican-mode
   "Toggle Pelican mode.
 
 Interactively with no argument, this command toggles the mode.
 for editing Pelican site files."
-  :init-value nil
   :lighter " Pelican"
-  :keymap pelican-keymap
-  :group 'pelican)
+  :group 'pelican
+  :keymap `((,(kbd "C-c P n") . pelican-insert-auto-header)
+            (,(kbd "C-c P p") . pelican-publish-draft)
+            (,(kbd "C-c P t") . pelican-update-date)
+            (,(kbd "C-c P h") . pelican-make-html)
+            (,(kbd "C-c P u") . pelican-make-rsync-upload)))
 
 ;;;###autoload
 (defun pelican-enable-if-site ()
   "Enable `pelican-mode' if this buffer is under a Pelican site."
-  (when (pelican-is-in-site)
+  (when (pelican-site-p)
     (pelican-mode 1)))
 
 ;;;###autoload