Initial import.
authorJoe Wreschnig <joe.wreschnig@gmail.com>
Tue, 21 May 2013 18:55:37 +0000 (20:55 +0200)
committerJoe Wreschnig <joe.wreschnig@gmail.com>
Tue, 21 May 2013 18:55:37 +0000 (20:55 +0200)
.gitignore [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
README.md [new file with mode: 0644]
pelican-mode.el [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..6302bc3
--- /dev/null
@@ -0,0 +1,2 @@
+*~
+*.elc
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..0e259d4
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..ddc21ea
--- /dev/null
@@ -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