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/>.
27 ;; pelican-mode is an Emacs minor mode for editing pages and posts in
28 ;; Pelican sites. Pelican is a static site generator which can
29 ;; process a variety of text file formats. For more information, see
30 ;; URL https://blog.getpelican.com/.
32 ;; It's intended to be used alongside `markdown-mode' or `rst-mode'.
33 ;; It also assumes you've set up Pelican with ``pelican-quickstart''
34 ;; or something like it. In particular it assumes:
36 ;; * The existence of ``pelicanconf.py'' and ``Makefile'' in some
37 ;; ancestor directory.
38 ;; * The first component of the path (e.g. ``content'') after that
39 ;; ancestor is irrelevant.
40 ;; * If the next component is ``pages'', that indicates a page
41 ;; rather than an article.
43 ;; To enable by default on all text files in a Pelican site:
45 ;; (require 'pelican-mode)
46 ;; (pelican-global-mode)
48 ;; Or, register `pelican-mode' or `pelican-mode-enable-if-site'
49 ;; as hook functions for more direct control.
61 (define-minor-mode pelican-mode
63 With a prefix argument ARG, enable Pelican mode if ARG is
64 positive, and disable it otherwise. If called from Lisp, enable
65 the mode if ARG is omitted or nil.
67 Pelican is a static site generator which can process a variety of
68 text file formats. For more information, see URL
69 https://blog.getpelican.com/.
71 Rather than manually enabling this mode, you may wish to use
72 `pelican-global-mode' or `pelican-mode-enable-if-site'.
74 When Pelican mode is enabled, additional commands are available
75 for editing articles or pages:
79 :keymap `((,(kbd "C-c P d") . pelican-mode-update-date)
80 (,(kbd "C-c P f") . pelican-set-field)
81 (,(kbd "C-c P h") . pelican-make-html)
82 (,(kbd "C-c P n") . pelican-mode-insert-header)
83 (,(kbd "C-c P p") . pelican-mode-publish-draft)
84 (,(kbd "C-c P u") . pelican-make-rsync-upload)))
87 (define-minor-mode pelican-global-mode
88 "Toggle Pelican global mode.
89 With a prefix argument ARG, enable Pelican global mode if ARG is
90 positive, and disable it otherwise. If called from Lisp, enable
91 the mode if ARG is omitted or nil.
93 Pelican is a static site generator which can process a variety of
94 text file formats. For more information, see URL
95 https://blog.getpelican.com/.
97 When Pelican global mode is enabled, text files which seem to
98 be part of a Pelican site will have `pelican-mode' automatically
101 If you disable this, you may still enable `pelican-mode' manually
102 or add `pelican-mode-enable-if-site' to more specific mode
106 (if pelican-global-mode
107 (add-hook 'text-mode-hook #'pelican-mode-enable-if-site)
108 (remove-hook 'text-mode-hook #'pelican-mode-enable-if-site)))
111 (defun pelican-mode-enable-if-site ()
112 "Enable `pelican-mode' if this buffer is part of a Pelican site.
114 Pelican sites are detected by looking for a file named `pelicanconf.py'
115 in an ancestor directory."
116 (when (pelican-mode-find-root)
123 (defgroup pelican-mode nil
124 "Support for Pelican articles and pages.
126 For more information about Pelican see URL https://blog.getpelican.com/."
129 (defcustom pelican-mode-default-page-fields
131 "Fields to include when creating a new page.
133 See the documentation for `pelican-mode-set-field' for more information
134 about metadata fields and special values."
138 (defcustom pelican-mode-default-article-fields
139 '(:date now :status "draft" :slug slug)
140 "Fields to include when creating a new article.
142 See the documentation for `pelican-mode-set-field' for more information
143 about metadata fields and special values."
147 (defcustom pelican-mode-formats
148 '((adoc-mode . pelican-mode-set-field-adoc-mode)
149 (markdown-mode . pelican-mode-set-field-markdown-mode)
150 (org-mode . pelican-mode-set-field-org-mode)
151 (rst-mode . pelican-mode-set-field-rst-mode))
152 "Functions to handle setting metadata, based on major mode.
154 This association list maps modes to functions that take two
155 arguments, field and value strings."
157 :type '(alist :key-type function :value-type function))
163 (defun pelican-mode-set-field (field value)
166 FIELD may be a string or a symbol; if it is a symbol, the
167 symbol name is used (removing a leading ':' if present).
169 When called from Lisp, VALUE may be any value; except for the
170 following special values, the unquoted printed representation of
173 - `now' means the current time; see `pelican-mode-timestamp'.
175 - `slug' means the file's path relative to the document root sans
176 extension; see `pelican-mode-default-slug'.
178 - nil or an empty string removes the field.
180 The buffer must be in a format listed in `pelican-mode-formats'
181 for this function to work correctly."
182 (interactive "sField: \nsValue: ")
183 (setq value (pcase value
184 ('now (pelican-mode-timestamp))
185 ('slug (pelican-mode-default-slug))
188 (when (symbolp field)
189 (setq field (string-remove-prefix ":" (symbol-name field))))
191 (assoc-default nil pelican-mode-formats #'derived-mode-p)))
193 (error "Unsupported major mode %S" major-mode))
196 (funcall set-field field value))))
198 (defun pelican-mode-remove-field (field)
200 (interactive "sField: ")
201 (pelican-mode-set-field field nil))
203 (defun pelican-mode-set-title (title)
204 "Set the title to TITLE."
205 (interactive "sTitle: ")
206 (pelican-mode-set-field :title title))
208 (defun pelican-mode-update-date (&optional original)
209 "Update the document's modification date.
211 If ORIGINAL is non-nil, the publication date is updated rather
212 than the modification date."
214 (pelican-mode-set-field (if original :date :modified) 'now))
216 (defun pelican-mode-publish-draft ()
217 "Remove draft status from a Pelican article."
219 (pelican-mode-remove-field :status)
220 (pelican-mode-update-date :date))
222 (defun pelican-mode-insert-draft-article-header (title tags)
223 "Insert a Pelican header for a draft with a TITLE and TAGS."
224 (interactive "sArticle title: \nsTags: ")
225 (apply #'pelican-mode-set-fields
227 ,@pelican-mode-default-article-fields
230 (defun pelican-mode-insert-page-header (title &optional hidden)
231 "Insert a Pelican header for a page with a TITLE.
233 If HIDDEN is non-nil, the page is marked hidden; otherwise it
235 (interactive "sPage title: \nP")
236 (apply #'pelican-mode-set-fields
238 (list :title title :status (when hidden "hidden"))
239 pelican-mode-default-page-fields)))
241 (defun pelican-mode-insert-header ()
242 "Insert a Pelican header for a page or article."
245 (if (pelican-mode-page-p)
246 #'pelican-mode-insert-page-header
247 #'pelican-mode-insert-draft-article-header)))
249 (defun pelican-make (target)
250 "Execute TARGET in a Makefile at the root of the site."
251 (interactive "sMake Pelican target: ")
252 (if-let (default-directory (pelican-mode-find-root))
253 (compilation-start (format "make %s" target)
254 nil (lambda (_) "*pelican*"))
255 (user-error "No Pelican site root could be found")))
257 (defun pelican-make-html ()
258 "Generate HTML via a Makefile at the root of the site."
260 (pelican-make "html"))
262 (defun pelican-make-rsync-upload ()
263 "Upload with rsync via a Makefile at the root of the site."
265 (pelican-make "rsync_upload"))
269 (defun pelican-mode-timestamp (&optional time)
270 "Generate a pelican-mode-compatible timestamp for TIME."
271 (format-time-string "%Y-%m-%d %H:%M" time))
273 (defun pelican-mode-set-fields (&rest fields)
274 "Insert a Pelican header for an article with metadata FIELDS."
275 (mapc (apply-partially #'apply #'pelican-mode-set-field)
276 (seq-partition fields 2)))
278 (defun pelican-mode-set-field-rst-mode (field value)
279 "Set reStructuredText metadata FIELD to VALUE."
280 (setq field (downcase field))
281 (if (equal field "title")
282 (let ((header (format "%s\n%s\n\n"
283 value (make-string (string-width value) ?#))))
284 (if (looking-at ".*\n#+\n+")
285 (replace-match header)
287 (let ((text (when value (format ":%s: %s\n" field value))))
288 (when (looking-at "^.*\n#")
290 (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t)
291 (replace-match (or text ""))
293 (if (re-search-forward "^$" nil t)
297 (defun pelican-mode-set-field-markdown-mode (field value)
298 "Set Markdown metadata FIELD to VALUE."
299 (setq field (capitalize field))
300 (let ((text (when value (format "%s: %s\n" field value))))
301 (if (re-search-forward (format "^%s:.*\n" (regexp-quote field)) nil t)
304 (if (re-search-forward "^$" nil t)
308 (defun pelican-mode-set-field-adoc-mode (field value)
309 "Set AsciiDoc metadata FIELD to VALUE."
310 (setq field (downcase field))
311 (if (equal field "title")
312 (let ((header (format "= %s\n\n" value)))
313 (if (looking-at "= .*\n\n+")
314 (replace-match header)
316 (let ((text (when value (format ":%s: %s\n" field value))))
317 (when (looking-at "^=")
319 (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t)
320 (replace-match (or text ""))
322 (if (re-search-forward "^$" nil t)
326 (defun pelican-mode-set-field-org-mode (field value)
327 "Set Org global metadata FIELD to VALUE."
328 ;; None of org-mode's functions I can find for setting properties
329 ;; operate on the global list, only a single property drawer.
330 (setq field (upcase field))
332 (format (if (member field '("TITLE" "DATE" "CATEGORY" "AUTHOR"))
336 (let ((text (when value (format "%s %s\n" field value))))
337 (if (re-search-forward (format "^%s .*\n" (regexp-quote field)) nil t)
338 (replace-match (or text ""))
340 (if (re-search-forward "^$" nil t)
344 (defun pelican-mode-page-p ()
345 "Return non-nil the current buffer is a Pelican page."
346 (when-let (pelican-mode-base (pelican-mode-find-root))
347 (let* ((relative (file-relative-name buffer-file-name pelican-mode-base))
348 (components (split-string relative "/")))
349 (equal "pages" (cadr components)))))
351 (defun pelican-mode-default-slug ()
352 "Generate a Pelican article/page slug for the current buffer."
353 (if-let ((pelican-mode-base (pelican-mode-find-root))
354 (file-name (file-name-sans-extension buffer-file-name)))
355 (let* ((relative (file-relative-name file-name pelican-mode-base))
356 (components (cdr (split-string relative "/")))
357 (components (if (string= "pages" (car components))
358 (cdr components) components)))
359 (mapconcat 'identity components "/"))
360 (when-let (file-name (file-name-sans-extension buffer-file-name))
361 (file-name-base file-name))))
363 (defun pelican-mode-find-root ()
364 "Return the root of the buffer's Pelican site, or nil."
365 (locate-dominating-file default-directory "pelicanconf.py"))
367 (provide 'pelican-mode)
368 ;;; pelican-mode.el ends here
373 ;; sentence-end-double-space: t