1 ;;; pelican-mode.el --- Minor mode for editing Pelican sites -*- lexical-binding: t -*-
3 ;; Copyright 2013-2017 Joe Wreschnig
5 ;; Author: Joe Wreschnig <joe.wreschnig@gmail.com>
6 ;; Package-Version: 20170730
7 ;; Package-Requires: ((emacs "25"))
8 ;; Keywords: convenience, editing
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;; Probably, this doesn't handle a lot of error cases. I also never
26 ;; tested it on networked drives and the lookup for pelicanconf.py
27 ;; might slow it down considerably.
35 (defgroup pelican-mode nil
36 "Support for Pelican articles and pages."
39 (defcustom pelican-mode-default-page-fields
41 "Fields to include when creating a new page.
43 See the documentation for `pelican-mode-set-field' for more information
44 about metadata fields and special values."
48 (defcustom pelican-mode-default-article-fields
49 '(:date now :status "draft" :slug slug)
50 "Fields to include when creating a new article.
52 See the documentation for `pelican-mode-set-field' for more information
53 about metadata fields and special values."
57 (defun pelican-mode-timestamp (&optional time)
58 "Generate a pelican-mode-compatible timestamp for TIME."
59 (format-time-string "%Y-%m-%d %H:%M" time))
61 (defun pelican-mode-insert-header (&rest fields)
62 "Insert a Pelican header for an article with metadata FIELDS."
63 (mapc (apply-partially #'apply #'pelican-mode-set-field)
64 (seq-partition fields 2)))
66 (defun pelican-mode-insert-draft-article-header (title tags)
67 "Insert a Pelican header for a draft with a TITLE and TAGS."
68 (interactive "sArticle title: \nsTags: ")
69 (apply #'pelican-mode-insert-header
70 `(:title ,title ,@pelican-mode-default-article-fields :tags ,tags)))
72 (defun pelican-mode-insert-page-header (title &optional hidden)
73 "Insert a Pelican header for a page with a TITLE, potentially HIDDEN."
75 (list (read-string "Page title: ")
76 (y-or-n-p "Hidden? ")))
77 (apply #'pelican-mode-insert-header
78 `(:title ,title ,@pelican-mode-default-page-fields
79 :hidden ,(when hidden "hidden"))))
81 (defun pelican-mode-insert-auto-header ()
82 "Insert a Pelican header for a page or article."
85 (if (pelican-mode-page-p)
86 #'pelican-mode-insert-page-header
87 #'pelican-mode-insert-draft-article-header)))
89 (defun pelican-mode-set-field/rst-mode (field value)
90 "Set reStructuredText metadata FIELD to VALUE."
91 (setq field (downcase field))
92 (if (equal field "title")
93 (let ((header (format "%s\n%s\n\n"
94 value (make-string (string-width value) ?#))))
95 (if (looking-at ".*\n#+\n+")
96 (replace-match header)
98 (let ((text (when value (format ":%s: %s\n" field value))))
99 (when (re-search-forward "^#" nil t)
101 (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t)
102 (replace-match (or text ""))
104 (re-search-forward "^$")
105 (replace-match text))))))
107 (defun pelican-mode-set-field/markdown-mode (field value)
108 "Set Markdown metadata FIELD to VALUE."
109 (setq field (capitalize field))
110 (let ((text (when value (format "%s: %s\n" field value))))
111 (if (re-search-forward (format "^%s:.*\n" (regexp-quote field)) nil t)
114 (re-search-forward "^$")
115 (replace-match text)))))
117 (defun pelican-mode-set-field (field value)
120 FIELD may be a string or a symbol; if it is a symbol, the
121 symbol name is used (removing a leading ':' if present).
123 VALUE may be any value; except for the following special values,
124 the unquoted printed representation of it is used:
126 - `now' means the current time; see `pelican-mode-timestamp'.
128 - `slug' means the file's path relative to the document root sans
129 extension; see `pelican-mode-default-slug'.
131 - nil or an empty string removes the field."
132 (interactive "sField: \nsValue: ")
133 (setq value (pcase value
134 ('now (pelican-mode-timestamp))
135 ('slug (pelican-mode-default-slug))
138 (when (symbolp field)
139 (setq field (string-remove-prefix ":" (symbol-name field))))
142 (cond ((derived-mode-p 'markdown-mode)
143 (pelican-mode-set-field/markdown-mode field value))
144 ((derived-mode-p 'rst-mode)
145 (pelican-mode-set-field/rst-mode field value))
146 (t (error "Unsupported major mode %S" major-mode)))))
148 (defun pelican-mode-remove-field (field)
150 (pelican-mode-set-field field nil))
152 (defun pelican-mode-set-title (title)
153 "Set the title to TITLE."
154 (interactive "sTitle: ")
155 (pelican-mode-set-field :title title))
157 (defun pelican-mode-update-date ()
158 "Update a Pelican date header."
160 (pelican-mode-set-field :date 'now))
162 (defun pelican-mode-publish-draft ()
163 "Remove draft status from a Pelican article."
165 (pelican-mode-remove-field :status)
166 (pelican-mode-update-date))
168 (defun pelican-mode-page-p ()
169 "Return non-nil the current buffer is a Pelican page."
170 (when-let (pelican-mode-base (pelican-mode-find-root))
171 (let* ((relative (file-relative-name buffer-file-name pelican-mode-base))
172 (components (split-string relative "/")))
173 (equal "pages" (cadr components)))))
175 (defun pelican-mode-default-slug ()
176 "Generate a Pelican article/page slug for the current buffer."
177 (if-let ((pelican-mode-base (pelican-mode-find-root))
178 (file-name (file-name-sans-extension buffer-file-name)))
179 (let* ((relative (file-relative-name file-name pelican-mode-base))
180 (components (cdr (split-string relative "/")))
181 (components (if (string= "pages" (car components))
182 (cdr components) components)))
183 (mapconcat 'identity components "/"))
185 (file-name-nondirectory
187 (file-name-directory file-name)))
188 (file-name-base file-name))))
190 (defun pelican-mode-find-in-parents (file-name)
191 "Find FILE-NAME in the default directory or one of its parents, or nil."
192 (let* ((parent (expand-file-name default-directory)))
193 (while (and (not (file-readable-p (concat parent file-name)))
194 (not (string= parent (directory-file-name parent))))
195 (setq parent (file-name-directory (directory-file-name parent))))
196 (let ((found (concat parent file-name)))
197 (if (file-readable-p found) found nil))))
199 (defun pelican-mode-find-root ()
200 "Return the root of the buffer's Pelican site, or nil."
201 (when-let (conf (pelican-mode-find-in-parents "pelicanconf.py"))
202 (file-name-directory conf)))
204 (defun pelican-make (target)
205 "Execute TARGET in a Makefile at the root of the site."
206 (interactive "sMake Pelican target: ")
207 (if-let (default-directory (pelican-mode-find-root))
208 (compilation-start (format "make %s" target)
209 nil (lambda (_) "*pelican*"))
210 (user-error "This doesn't look like a Pelican site")))
212 (defun pelican-make-html ()
213 "Generate HTML via a Makefile at the root of the site."
215 (pelican-make "html"))
217 (defun pelican-make-rsync-upload ()
218 "Upload with rsync via a Makefile at the root of the site."
220 (pelican-make "rsync_upload"))
223 (define-minor-mode pelican-mode
224 "Toggle Pelican mode.
225 With a prefix argument ARG, enable Pelican mode if ARG is
226 positive, and disable it otherwise. If called from Lisp, enable
227 the mode if ARG is omitted or nil.
229 When Pelican mode is enabled, additional commands are available
230 for editing articles or pages:
232 \\{pelican-mode-map}"
234 :keymap `((,(kbd "C-c P n") . pelican-mode-insert-auto-header)
235 (,(kbd "C-c P p") . pelican-mode-publish-draft)
236 (,(kbd "C-c P t") . pelican-mode-update-date)
237 (,(kbd "C-c P h") . pelican-make-html)
238 (,(kbd "C-c P u") . pelican-make-rsync-upload)))
241 (define-minor-mode pelican-global-mode
242 "Toggle Pelican global mode.
243 With a prefix argument ARG, enable Pelican global mode if ARG is
244 positive, and disable it otherwise. If called from Lisp, enable
245 the mode if ARG is omitted or nil.
247 When Pelican global mode is enabled, text files which seem to
248 be part of a Pelican site will have `pelican-mode' automatically
251 If you disable this, you may still enable `pelican-mode' manually
252 or add `pelican-mode-enable-if-site' to more specific mode
256 (if pelican-global-mode
257 (add-hook 'text-mode-hook #'pelican-mode-enable-if-site)
258 (remove-hook 'text-mode-hook #'pelican-mode-enable-if-site)))
261 (defun pelican-mode-enable-if-site ()
262 "Enable `pelican-mode' if this buffer is part of a Pelican site."
263 (when (pelican-mode-find-root)
266 (provide 'pelican-mode)
267 ;;; pelican-mode.el ends here
270 ;; sentence-end-double-space: t