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: 20170808
7 ;; Package-Requires: ((emacs "25"))
8 ;; URL: https://git.korewanetadesu.com/pelican-mode.git
9 ;; Keywords: convenience, editing
11 ;; This program is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
28 ;; pelican-mode is an Emacs minor mode for editing articles and pages
29 ;; in Pelican sites. Pelican is a static site generator which can
30 ;; process a variety of text file formats. For more information, see
31 ;; URL https://blog.getpelican.com/.
33 ;; It’s intended to be used alongside a major mode for the Pelican
34 ;; document. Currently supported formats are Markdown,
35 ;; reStructuredText, AsciiDoc, and Org. It also assumes you’ve set up
36 ;; Pelican with “pelican-quickstart” or something like it. In
37 ;; particular it expects:
39 ;; * The existence of “pelicanconf.py” and “Makefile” in some
40 ;; ancestor directory.
41 ;; * The first component of the path (e.g. “content”) after that
42 ;; ancestor is irrelevant.
43 ;; * If the next component is “pages”, that indicates a page
44 ;; rather than an article.
46 ;; To enable by default on all text files in a Pelican site:
48 ;; (require 'pelican-mode)
49 ;; (pelican-global-mode)
51 ;; Or with ‘use-package’ and deferred loading:
53 ;; (use-package pelican-mode
54 ;; :after (:any org rst markdown-mode adoc-mode)
56 ;; (pelican-global-mode))
58 ;; Or, register ‘pelican-mode’ or ‘pelican-mode-enable-if-site’
59 ;; as hook functions for more direct control.
71 "Support for Pelican articles and pages.
73 For more information about Pelican see URL https://blog.getpelican.com/."
76 (defcustom pelican-mode-keymap-prefix (kbd "C-c P")
77 "Pelican mode keymap prefix."
81 (defcustom pelican-mode-default-page-fields
83 "Fields to include when creating a new page.
85 See the documentation for ‘pelican-mode-set-field’ for more information
86 about metadata fields and special values."
90 (defcustom pelican-mode-default-article-fields
91 '(:date now :status "draft" :slug slug)
92 "Fields to include when creating a new article.
94 See the documentation for ‘pelican-mode-set-field’ for more information
95 about metadata fields and special values."
99 (defcustom pelican-mode-formats
100 '((adoc-mode . pelican-mode-set-field-adoc-mode)
101 (markdown-mode . pelican-mode-set-field-markdown-mode)
102 (org-mode . pelican-mode-set-field-org-mode)
103 (rst-mode . pelican-mode-set-field-rst-mode))
104 "Functions to handle setting metadata, based on major mode.
106 This association list maps modes to functions that take two
107 arguments, field and value strings."
109 :type '(alist :key-type function :value-type function))
115 (defvar pelican-mode-command-map
116 (let ((map (make-sparse-keymap)))
117 (define-key map (kbd "d") #'pelican-mode-update-date)
118 (define-key map (kbd "f") #'pelican-mode-set-field)
119 (define-key map (kbd "h") #'pelican-make-html)
120 (define-key map (kbd "n") #'pelican-mode-insert-header)
121 (define-key map (kbd "p") #'pelican-mode-publish)
122 (define-key map (kbd "u") #'pelican-make-rsync-upload)
124 "Keymap for Pelican commands after ‘pelican-mode-keymap-prefix’.")
125 (fset 'pelican-mode-command-map pelican-mode-command-map)
127 (defvar pelican-mode-map
128 (let ((map (make-sparse-keymap)))
129 (define-key map pelican-mode-keymap-prefix
130 'pelican-mode-command-map)
132 "Keymap for Pelican mode.")
135 (define-minor-mode pelican-mode
136 "Toggle Pelican mode.
137 With a prefix argument ARG, enable Pelican mode if ARG is
138 positive, and disable it otherwise. If called from Lisp, enable
139 the mode if ARG is omitted or nil.
141 Pelican is a static site generator which can process a variety of
142 text file formats. For more information, see URL
143 https://blog.getpelican.com/.
145 Rather than manually enabling this mode, you may wish to use
146 ‘pelican-global-mode’ or ‘pelican-mode-enable-if-site’.
148 When Pelican mode is enabled, additional commands are available
149 for editing articles or pages:
151 \\{pelican-mode-map}"
153 :keymap pelican-mode-map
157 (define-globalized-minor-mode pelican-global-mode pelican-mode
159 (when (derived-mode-p #'text-mode)
160 (pelican-mode-enable-if-site)))
162 :require 'pelican-mode)
165 (defun pelican-mode-enable-if-site ()
166 "Enable ‘pelican-mode’ if this buffer is part of a Pelican site.
168 Pelican sites are detected by looking for a file named
169 “pelicanconf.py” in an ancestor directory."
170 (when (pelican-mode-find-root)
177 (defun pelican-mode-set-field (field value)
180 FIELD may be a string or a symbol; if it is a symbol, the
181 symbol name is used (removing a leading “:” if present).
183 When called from Lisp, VALUE may be any value; except for the
184 following special values, the unquoted printed representation of
187 - ‘now’ means the current time.
189 - ‘slug’ means the file’s path relative to the document root sans
190 extension; see ‘pelican-mode-default-slug’.
192 - nil or an empty string removes the field.
194 The buffer must be in a format listed in ‘pelican-mode-formats’
195 for this function to work correctly."
196 (interactive "sField: \nsValue: ")
197 (setq value (pcase value
198 ('now (format-time-string "%Y-%m-%d %H:%M"))
199 ('slug (pelican-mode-default-slug))
202 (when (symbolp field)
203 (setq field (string-remove-prefix ":" (symbol-name field))))
205 (assoc-default nil pelican-mode-formats #'derived-mode-p)))
207 (error "Unsupported major mode %S" major-mode))
210 (funcall set-field field value))))
212 (defun pelican-mode-remove-field (field)
214 (interactive "sField: ")
215 (pelican-mode-set-field field nil))
217 (defun pelican-mode-set-title (title)
218 "Set the title to TITLE."
219 (interactive "sTitle: ")
220 (pelican-mode-set-field :title title))
222 (defun pelican-mode-update-date (&optional original)
223 "Update the document’s modification date.
225 If ORIGINAL is non-nil, the publication date is updated rather
226 than the modification date."
228 (pelican-mode-set-field (if original :date :modified) 'now))
230 (defun pelican-mode-publish ()
231 "Remove draft or hidden status from a Pelican article."
233 (pelican-mode-remove-field :status)
234 (pelican-mode-update-date :date))
236 (defun pelican-mode-insert-article-header (title tags)
237 "Insert a Pelican header for an article with a TITLE and TAGS."
238 (interactive "sArticle title: \nsTags: ")
242 (apply #'pelican-mode-set-fields
244 ,@pelican-mode-default-article-fields
247 (defun pelican-mode-insert-page-header (title &optional hidden)
248 "Insert a Pelican header for a page with a TITLE.
250 If HIDDEN is non-nil, the page is marked hidden; otherwise it
252 (interactive "sPage title: \nP")
256 (apply #'pelican-mode-set-fields
258 (list :title title :status (when hidden "hidden"))
259 pelican-mode-default-page-fields))))
261 (defun pelican-mode-insert-header ()
262 "Insert a Pelican header for a page or article."
265 (if (pelican-mode-page-p)
266 #'pelican-mode-insert-page-header
267 #'pelican-mode-insert-article-header)))
269 (defun pelican-make (target)
270 "Execute TARGET in a Makefile at the root of the site."
271 (interactive "sMake Pelican target: ")
272 (if-let (default-directory (pelican-mode-find-root))
273 (compilation-start (format "make %s" target)
274 nil (lambda (_) "*pelican*"))
275 (user-error "No Pelican site root could be found")))
277 (defun pelican-make-html ()
278 "Generate HTML via a Makefile at the root of the site."
280 (pelican-make "html"))
282 (defun pelican-make-rsync-upload ()
283 "Upload with rsync via a Makefile at the root of the site."
285 (pelican-make "rsync_upload"))
289 (defun pelican-mode-set-fields (&rest fields)
290 "Insert a Pelican header for an article with metadata FIELDS."
291 (mapc (apply-partially #'apply #'pelican-mode-set-field)
292 (seq-partition fields 2)))
294 (defun pelican-mode-set-field-rst-mode (field value)
295 "Set reStructuredText metadata FIELD to VALUE."
296 (setq field (downcase field))
297 (if (equal field "title")
298 (let ((header (format "%s\n%s\n\n"
299 value (make-string (string-width value) ?#))))
300 (if (looking-at ".*\n#+\n+")
301 (replace-match header)
303 (let ((text (when value (format ":%s: %s\n" field value))))
304 (when (looking-at "^.*\n#")
306 (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t)
307 (replace-match (or text ""))
309 (if (re-search-forward "^$" nil t)
313 (defun pelican-mode-set-field-markdown-mode (field value)
314 "Set Markdown metadata FIELD to VALUE."
315 (setq field (capitalize field))
316 (let ((text (when value (format "%s: %s\n" field value))))
317 (if (re-search-forward (format "^%s:.*\n" (regexp-quote field)) nil t)
320 (if (re-search-forward "^$" nil t)
324 (defun pelican-mode-set-field-adoc-mode (field value)
325 "Set AsciiDoc metadata FIELD to VALUE."
326 (setq field (downcase field))
327 (if (equal field "title")
328 (let ((header (format "= %s\n\n" value)))
329 (if (looking-at "= .*\n\n+")
330 (replace-match header)
332 (let ((text (when value (format ":%s: %s\n" field value))))
333 (when (looking-at "^=")
335 (if (re-search-forward (format "^:%s:.*\n" (regexp-quote field)) nil t)
336 (replace-match (or text ""))
338 (if (re-search-forward "^$" nil t)
342 (defun pelican-mode-set-field-org-mode (field value)
343 "Set Org global metadata FIELD to VALUE."
344 ;; None of org-mode’s functions I can find for setting properties
345 ;; operate on the global list, only a single property drawer.
346 (setq field (upcase field))
348 (format (if (member field '("TITLE" "DATE" "CATEGORY" "AUTHOR"))
352 (let ((text (when value (format "%s %s\n" field value))))
353 (if (re-search-forward (format "^%s .*\n" (regexp-quote field)) nil t)
354 (replace-match (or text ""))
356 (if (re-search-forward "^$" nil t)
360 (defun pelican-mode-page-p ()
361 "Return non-nil the current buffer is a Pelican page."
365 (abbreviate-file-name (or (buffer-file-name) (buffer-name)))
366 (pelican-mode-find-root))))
368 (defun pelican-mode-default-slug ()
369 "Generate a Pelican slug for the current buffer."
370 (file-name-sans-extension
371 (replace-regexp-in-string
372 "^[^/]+/\\(?:pages/\\)?" ""
374 (abbreviate-file-name (or (buffer-file-name) (buffer-name)))
375 (pelican-mode-find-root)))))
377 (defun pelican-mode-find-root ()
378 "Return the root of the buffer’s Pelican site, or nil."
379 (locate-dominating-file default-directory "pelicanconf.py"))
381 (provide 'pelican-mode)
382 ;;; pelican-mode.el ends here
387 ;; sentence-end-double-space: t