X-Git-Url: https://git.korewanetadesu.com/?p=pelican-mode.git;a=blobdiff_plain;f=pelican-mode.el;h=6dceba57dd2034aeada638e90dd81223cf78ff1a;hp=46712544b0bdb0f4d7eccc89be9dbce60c04c847;hb=6ab55815a4bbae7367a5fc07bc27fe88194ce249;hpb=cbf0b460910f42175be6e739fd28174fa1347e7f diff --git a/pelican-mode.el b/pelican-mode.el index 4671254..6dceba5 100644 --- a/pelican-mode.el +++ b/pelican-mode.el @@ -3,7 +3,7 @@ ;; Copyright 2013-2017 Joe Wreschnig ;; ;; Author: Joe Wreschnig -;; Package-Version: 20170618 +;; Package-Version: 20170730 ;; Package-Requires: ((emacs "25")) ;; Keywords: convenience, editing ;; @@ -20,145 +20,194 @@ ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . + ;;; Commentary: ;; -;; Probably, this doesn't handle a lot of error cases. I also never -;; tested it on networked drives and the lookup for pelicanconf.py -;; might slow it down considerably. +;; pelican-mode is an Emacs minor mode for editing pages and posts in +;; Pelican (URL http://getpelican.com/) sites. +;; +;; It's intended to be used alongside `markdown-mode' or `rst-mode'. +;; It also assumes you've set up Pelican with ``pelican-quickstart'' +;; or something like it. In particular it assumes: +;; +;; * The existence of ``pelicanconf.py'' and ``Makefile'' in some +;; ancestor directory. +;; * The first component of the path (e.g. ``content'') after that +;; ancestor is irrelevant. +;; * If the next component is ``pages'', that indicates a page +;; rather than an article. ;;; Code: +(require 'seq) (require 'subr-x) -(defun pelican-timestamp (&optional time) - "Generate a Pelican-compatible timestamp for TIME." +(defgroup pelican-mode nil + "Support for Pelican articles 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-mode-set-field' for more information +about metadata fields and special values." + :group 'pelican + :type '(plist)) + +(defcustom pelican-mode-default-article-fields + '(:date now :status "draft" :slug slug) + "Fields to include when creating a new article. + +See the documentation for `pelican-mode-set-field' for more information +about metadata fields and special values." + :group 'pelican + :type '(plist)) + +(defcustom pelican-mode-set-field-alist + '((markdown-mode . pelican-mode-set-field-markdown-mode) + (rst-mode . pelican-mode-set-field-rst-mode)) + "Functions to handle setting metadata, based on major mode. + +This association list maps modes to functions that take two +arguments, field and value strings." + :group 'pelican + :type '(alist :key-type function :value-type function)) + +(defun pelican-mode-timestamp (&optional time) + "Generate a pelican-mode-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." - (if value - (cond ((derived-mode-p 'markdown-mode) - (format "%s: %s\n" (capitalize name) value)) - ((derived-mode-p 'rst-mode) - (format ":%s: %s\n" (downcase name) value)) - (t (error "Unsupported major mode %S" major-mode))) - "")) - -(defun pelican-rst-title (title) - "Create a ReSt version of TITLE." - (concat title "\n" (make-string (string-width title) ?#) "\n\n")) - -(defun pelican-title (title) - "Format a TITLE for the current document, according to major mode." - (cond ((derived-mode-p 'markdown-mode) - (pelican-field "title" title)) - ((derived-mode-p 'rst-mode) - (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))) - - (concat (pelican-title title) - (pelican-field "date" date) - (pelican-field "status" status) - (pelican-field "tags" tags) - (pelican-field "category" category) - (pelican-field "slug" slug) - "\n")) - -(defun pelican-insert-draft-post-header (title tags) - "Insert a Pelican header for a draft post." - (interactive "sPost title: \nsTags: ") - (let ((slug (pelican-default-slug))) - (save-excursion - (goto-char 0) - (insert (pelican-header title 't "draft" nil tags slug))))) +(defun pelican-mode-insert-header (&rest fields) + "Insert a Pelican header for an article with metadata FIELDS." + (mapc (apply-partially #'apply #'pelican-mode-set-field) + (seq-partition fields 2))) + +(defun pelican-mode-insert-draft-article-header (title tags) + "Insert a Pelican header for a draft with a TITLE and TAGS." + (interactive "sArticle title: \nsTags: ") + (apply #'pelican-mode-insert-header + `(:title ,title ,@pelican-mode-default-article-fields :tags ,tags))) -(defun pelican-insert-page-header (title hidden) - "Insert a Pelican header for a page." +(defun pelican-mode-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))) + (apply #'pelican-mode-insert-header + `(:title ,title ,@pelican-mode-default-page-fields + :hidden ,(when hidden "hidden")))) + +(defun pelican-mode-insert-auto-header () + "Insert a Pelican header for a page or article." + (interactive) + (call-interactively + (if (pelican-mode-page-p) + #'pelican-mode-insert-page-header + #'pelican-mode-insert-draft-article-header))) + +(defun pelican-mode-set-field-rst-mode (field value) + "Set reStructuredText metadata FIELD to VALUE." + (setq field (downcase field)) + (if (equal field "title") + (let ((header (format "%s\n%s\n\n" + value (make-string (string-width value) ?#)))) + (if (looking-at ".*\n#+\n+") + (replace-match header) + (insert header))) + (let ((text (when value (format ":%s: %s\n" field value)))) + (when (re-search-forward "^#" nil t) + (forward-line 2)) + (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t) + (replace-match (or text "")) + (when text + (if (re-search-forward "^$" nil t) + (replace-match text) + (insert text))))))) + +(defun pelican-mode-set-field-markdown-mode (field value) + "Set Markdown metadata FIELD to VALUE." + (setq field (capitalize field)) + (let ((text (when value (format "%s: %s\n" field value)))) + (if (re-search-forward (format "^%s:.*\n" (regexp-quote field)) nil t) + (replace-match text) + (when value + (if (re-search-forward "^$" nil t) + (replace-match text) + (insert text)))))) + +(defun pelican-mode-set-field (field value) + "Set FIELD to VALUE. + +FIELD 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-mode-timestamp'. + +- `slug' means the file's path relative to the document root sans + extension; see `pelican-mode-default-slug'. + +- nil or an empty string removes the field." + (interactive "sField: \nsValue: ") + (setq value (pcase value + ('now (pelican-mode-timestamp)) + ('slug (pelican-mode-default-slug)) + ('"" nil) + (_ value))) + (when (symbolp field) + (setq field (string-remove-prefix ":" (symbol-name field)))) + (let ((set-field + (assoc-default nil pelican-mode-set-field-alist #'derived-mode-p))) + (unless set-field + (error "Unsupported major mode %S" major-mode)) (save-excursion (goto-char 0) - (insert (pelican-header title nil hidden nil nil slug))))) + (funcall set-field field value)))) -(defun pelican-insert-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))) +(defun pelican-mode-remove-field (field) + "Remove FIELD." + (pelican-mode-set-field field nil)) -(defun pelican-set-field (field value) - "Set FIELD to VALUE." - (interactive "sField: \nsValue: ") - (save-excursion - (goto-char 0) - (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))))) - -(defun pelican-set-title (title) +(defun pelican-mode-set-title (title) "Set the title to TITLE." (interactive "sTitle: ") - (if (pelican-is-markdown) - (pelican-set-field "title" title) - (save-excursion - (goto-char 0) - (let ((header (pelican-rst-title title))) - (if (looking-at ".*\n#+\n+") - (replace-match header) - (insert header)))))) + (pelican-mode-set-field :title title)) -(defun pelican-update-date () +(defun pelican-mode-update-date () "Update a Pelican date header." (interactive) - (pelican-set-field "date" (pelican-timestamp))) + (pelican-mode-set-field :date 'now)) -(defun pelican-publish-draft () - "Remove draft status from a Pelican post." +(defun pelican-mode-publish-draft () + "Remove draft status from a Pelican article." (interactive) - (pelican-set-field "status" nil) - (pelican-update-date)) + (pelican-mode-remove-field :status) + (pelican-mode-update-date)) -(defun pelican-is-page () - "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)) +(defun pelican-mode-page-p () + "Return non-nil the current buffer is a Pelican page." + (when-let (pelican-mode-base (pelican-mode-find-root)) + (let* ((relative (file-relative-name buffer-file-name pelican-mode-base)) (components (split-string relative "/"))) (equal "pages" (cadr components))))) -(defun pelican-default-slug () - "Generate a Pelican post/page slug for the current buffer." - (if-let ((pelican-base (pelican-find-root)) +(defun pelican-mode-default-slug () + "Generate a Pelican article/page slug for the current buffer." + (if-let ((pelican-mode-base (pelican-mode-find-root)) (file-name (file-name-sans-extension buffer-file-name))) - (let* ((relative (file-relative-name file-name pelican-base)) + (let* ((relative (file-relative-name file-name pelican-mode-base)) (components (cdr (split-string relative "/"))) (components (if (string= "pages" (car components)) (cdr components) components))) (mapconcat 'identity components "/")) - (format "%s/%s" - (file-name-nondirectory - (directory-file-name - (file-name-directory file-name))) - (file-name-base file-name)))) + (when-let (file-name (file-name-sans-extension buffer-file-name)) + (file-name-base file-name)))) -(defun pelican-find-in-parents (file-name) +(defun pelican-mode-find-in-parents (file-name) "Find FILE-NAME in the default directory or one of its parents, or nil." (let* ((parent (expand-file-name default-directory))) (while (and (not (file-readable-p (concat parent file-name))) @@ -167,25 +216,18 @@ (let ((found (concat parent file-name))) (if (file-readable-p found) found nil)))) -(defun pelican-find-root () +(defun pelican-mode-find-root () "Return the root of the buffer's Pelican site, or nil." - (when-let (conf (pelican-find-in-parents "pelicanconf.py")) + (when-let (conf (pelican-mode-find-in-parents "pelicanconf.py")) (file-name-directory conf))) -(defun pelican-is-in-site () - "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-mode-find-root)) + (compilation-start (format "make %s" target) + nil (lambda (_) "*pelican*")) + (user-error "No Pelican site root could be found"))) (defun pelican-make-html () "Generate HTML via a Makefile at the root of the site." @@ -197,42 +239,53 @@ (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. +With a prefix argument ARG, enable Pelican mode if ARG is +positive, and disable it otherwise. If called from Lisp, enable +the mode if ARG is omitted or nil. -Interactively with no argument, this command toggles the mode. -to show buffer size and position in mode-line." - :init-value nil - :lighter " Pelican" - :keymap pelican-keymap - :group 'pelican) +When Pelican mode is enabled, additional commands are available +for editing articles or pages: -;;;###autoload -(defun pelican-enable-if-site () - "Enable `pelican-mode' if this buffer is under a Pelican site." - (when (pelican-is-in-site) - (pelican-mode 1))) +\\{pelican-mode-map}" + :lighter " Pelican" + :keymap `((,(kbd "C-c P n") . pelican-mode-insert-auto-header) + (,(kbd "C-c P p") . pelican-mode-publish-draft) + (,(kbd "C-c P t") . pelican-mode-update-date) + (,(kbd "C-c P h") . pelican-make-html) + (,(kbd "C-c P u") . pelican-make-rsync-upload))) ;;;###autoload -(add-hook 'markdown-mode-hook 'pelican-enable-if-site) +(define-minor-mode pelican-global-mode + "Toggle Pelican global mode. +With a prefix argument ARG, enable Pelican global mode if ARG is +positive, and disable it otherwise. If called from Lisp, enable +the mode if ARG is omitted or nil. + +When Pelican global mode is enabled, text files which seem to +be part of a Pelican site will have `pelican-mode' automatically +enabled. + +If you disable this, you may still enable `pelican-mode' manually +or add `pelican-mode-enable-if-site' to more specific mode +hooks." + :global t + :group 'pelican + (if pelican-global-mode + (add-hook 'text-mode-hook #'pelican-mode-enable-if-site) + (remove-hook 'text-mode-hook #'pelican-mode-enable-if-site))) ;;;###autoload -(add-hook 'rst-mode-hook 'pelican-enable-if-site) +(defun pelican-mode-enable-if-site () + "Enable `pelican-mode' if this buffer is part of a Pelican site." + (when (pelican-mode-find-root) + (pelican-mode 1))) (provide 'pelican-mode) ;;; pelican-mode.el ends here + +;; Local Variables: +;; sentence-end-double-space: t +;; End: