From: Joe Wreschnig Date: Tue, 21 May 2013 18:55:37 +0000 (+0200) Subject: Initial import. X-Git-Tag: v20170807~41 X-Git-Url: https://git.korewanetadesu.com/?p=pelican-mode.git;a=commitdiff_plain;h=c1a3b1044b0fc327ecfc2c5104a283201d7a0b7a Initial import. --- c1a3b1044b0fc327ecfc2c5104a283201d7a0b7a diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6302bc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +*.elc diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ceec3d7 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# pelican-mode + +pelican-mode is a minor mode for editing pages and posts in [Pelican] +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 static page + rather than a dated post. + +It also enforces some parts of my preferred Pelican configuration: + + * Categories are never provided (you can have one if you want, but + the default interactive commands don't provide one). + * Tags are always provided. + * Slugs are explicit, and include nested subdirectories. + +## Quick Guide + +* `C-x p n` - Insert a post or page header +* `C-x p p` - Remove draft status from a post (i.e. publish it) +* `C-x p t` - Update the date field in a post/page header +* `C-x p h` - Generate HTML output for a site (equivalent to `make html`) +* `C-x p u` - Upload a site using rsync (equivalent to `make rsync_upload`) + +## Troubleshooting + +If the commands which invoke `make` can find the Makefile but can't +find `pelican`, your `exec-path` may not be set right. Try out +[exec-path-from-shell]. + +## License + +This code is released into the public domain via the +[CC0 Public Domain Dedication][0]. + + [Pelican]: http://getpelican.com/ + [markdown-mode]: http://jblevins.org/projects/markdown-mode/ + [rst-mode]: http://docutils.sourceforge.net/docs/user/emacs.html + [exec-path-from-shell]: https://github.com/purcell/exec-path-from-shell + [0]: http://creativecommons.org/publicdomain/zero/1.0/legalcode diff --git a/pelican-mode.el b/pelican-mode.el new file mode 100644 index 0000000..ddc21ea --- /dev/null +++ b/pelican-mode.el @@ -0,0 +1,213 @@ +;;; pelican-mode.el --- Minor mode for editing pages and posts in Pelican sites +;; +;; Author: Joe Wreschnig +;; This code is released into the public domain. + +;;; 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. + +;;; Code: + +(defun pelican-timestamp-now () + "Generate a Pelican-compatible timestamp." + (format-time-string "%Y-%m-%d %H:%M")) + +(defun pelican-is-markdown () + "Check if the buffer is likely using Markdown." + (eq major-mode 'markdown-mode)) + +(defun pelican-field (name value) + "Helper to format a field NAME and VALUE." + (if value (format "%s: %s\n" name value) "")) + +(defun pelican-markdown-header (title date status category tags slug) + "Generate a Pelican Markdown header. + +All parameters but TITLE may be nil to omit them. DATE may be a +string or 't to use the current date and time." + (let ((title (format "Title: %s\n" title)) + (status (pelican-field "Status" status)) + (category (pelican-field "Category" category)) + (tags (pelican-field "Tags" tags)) + (slug (pelican-field "Slug" slug)) + (date (if date (format "Date: %s\n" + (if (stringp date) date + (pelican-timestamp-now))) + ""))) + (concat title date status tags category slug "\n"))) + +(defun pelican-rst-header (title date status category tags slug) + "Generate a Pelican reStructuredText header. + +All parameters but TITLE may be nil to omit them. DATE may be a +string or 't to use the current date and time." + (let ((title (format "%s\n%s\n\n" title + (make-string (string-width title) ?#))) + (status (pelican-field ":status" status)) + (category (pelican-field ":category" category)) + (tags (pelican-field ":tags" tags)) + (slug (pelican-field ":slug" slug)) + (date (if date (format ":date: %s\n" + (if (stringp date) date + (pelican-timestamp-now))) + ""))) + (concat title date status tags category 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)) + (header (if (pelican-is-markdown) + 'pelican-markdown-header 'pelican-rst-header))) + (save-excursion + (goto-char 0) + (insert (funcall header title 't "draft" nil tags slug))))) + +(defun pelican-insert-page-header (title hidden) + "Insert a Pelican header for a page." + (interactive + (list (read-string "Page title: ") + (y-or-n-p "Hidden? "))) + (let ((slug (pelican-default-slug)) + (hidden (if hidden "hidden" nil)) + (header (if (pelican-is-markdown) + 'pelican-markdown-header 'pelican-rst-header))) + (save-excursion + (goto-char 0) + (insert (funcall header title nil hidden nil nil slug))))) + +(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-update-date () + "Update a Pelican date header." + (interactive) + (save-excursion + (goto-char 0) + (let* ((field (if (pelican-is-markdown) "Date" ":date")) + (re (format "^%s: [-0-9 :]+\n" field)) + (date (pelican-timestamp-now))) + (if (re-search-forward re nil t) + (replace-match (format "%s: %s\n" field date)) + (message "This doesn't look like a Pelican page."))))) + +(defun pelican-publish-draft () + "Remove draft status from a Pelican post." + (interactive) + (save-excursion + (goto-char 0) + (let* ((field (if (pelican-is-markdown) "Status" ":status")) + (re (format "^%s: draft\n" field))) + (if (re-search-forward re nil t) + (progn + (replace-match (format "")) + (pelican-update-date)) + (message "This doesn't look like a Pelican draft."))))) + +(defun pelican-is-page () + "Guess the current buffer is a Pelican page (vs. a post or neither)." + (let ((pelican-base (pelican-find-root))) + (if pelican-base + (let* ((relative (file-relative-name buffer-file-name pelican-base)) + (components (split-string relative "/"))) + (string= "pages" (car (cdr components))))))) + +(defun pelican-default-slug () + "Generate a Pelican post/page slug for the current buffer." + (let ((pelican-base (pelican-find-root)) + (file-name (file-name-sans-extension buffer-file-name))) + (if pelican-base + (let* ((relative (file-relative-name file-name pelican-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))))) + +(defun pelican-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))) + (not (string= parent (directory-file-name parent)))) + (setq parent (file-name-directory (directory-file-name parent)))) + (let ((found (concat parent file-name))) + (if (file-readable-p found) found nil)))) + +(defun pelican-find-root () + "Return the root of the buffer's Pelican site, or nil." + (let ((conf (pelican-find-in-parents "pelicanconf.py"))) + (if conf (file-name-directory conf)))) + +(defun pelican-is-in-site () + "Check if this buffer is under a Pelican site." + (not (not (pelican-find-root)))) + +(defun pelican-enable-if-site () + "Enable `pelican-mode' if this buffer is under a Pelican site." + (if (pelican-is-in-site) + (pelican-mode 1))) + +(defun pelican-make (target) + "Execute TARGET in a Makefile at the root of the site." + (interactive "sMake Pelican target: ") + (let ((default-directory (pelican-find-root))) + (if default-directory + (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.")))) + +(defun pelican-make-html () + "Generate HTML via a Makefile at the root of the site." + (interactive) + (pelican-make "html")) + +(defun pelican-make-rsync-upload () + "Upload with rsync via a Makefile at the root of the site." + (interactive) + (pelican-make "rsync_upload")) + +(defconst pelican-keymap (make-sparse-keymap) + "The default keymap used in Pelican mode.") +(define-key pelican-keymap [?\C-x ?p ?n] + 'pelican-insert-header) +(define-key pelican-keymap [?\C-x ?p ?p] + 'pelican-publish-draft) +(define-key pelican-keymap [?\C-x ?p ?t] + 'pelican-update-date) +(define-key pelican-keymap [?\C-x ?p ?h] + 'pelican-make-html) +(define-key pelican-keymap [?\C-x ?p ?u] + 'pelican-make-rsync-upload) + +(define-minor-mode pelican-mode + "Toggle Pelican mode. + +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 + ) + +(add-hook 'markdown-mode-hook 'pelican-enable-if-site) +(add-hook 'rst-mode-hook 'pelican-enable-if-site) + +(provide 'pelican-mode) + +;;; pelican-mode.el ends here