;;; 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))
""))
(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))
+ (if-let (default-directory (pelican-find-root))
+ (compilation-start (format "make %s" target)
+ nil (lambda (_) "*pelican*"))
(message "This doesn't look like a Pelican site.")))
(defun pelican-make-html ()
(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