├── .gitignore ├── README ├── org-jekyll.el └── test ├── _posts ├── 2009-12-26-Entry-in-the-second-file.html ├── 2009-12-26-First-blog-entry.html └── 2009-12-26-Second-blog-entry.html ├── org.el ├── test-2.org └── test.org /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.xfasl 3 | *.pyc 4 | .emacs-bu/* 5 | 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Org-jekyll — export jekyll blog posts from org-mode. 2 | 3 | Juan Reyero, http://juanreyero.com 4 | 5 | Extracts subtrees from your org-publish project files that have a 6 | :blog: keyword and an :on: property with a timestamp, and exports 7 | them to a subdirectory _posts of your project's publishing 8 | directory in the year-month-day-title.html format that Jekyll 9 | expects. Properties are passed over as yaml front-matter in the 10 | exported files. The title of the entry is the title of the 11 | subtree. 12 | 13 | The full documentation is at: 14 | 15 | http://juanreyero.com/open/org-jekyll/ 16 | -------------------------------------------------------------------------------- /org-jekyll.el: -------------------------------------------------------------------------------- 1 | ;;; org-jekyll.el --- Export jekyll-ready posts form org-mode entries 2 | ;;; 3 | ;;; Author: Juan Reyero 4 | ;;; Version: 0.4 5 | ;;; Keywords: hypermedia 6 | ;;; Package-Requires: ((org "8.0")) 7 | ;;; Homepage: http://juanreyero.com/open/org-jekyll/ 8 | ;;; Repository: http://github.com/juanre/org-jekyll 9 | ;;; Public clone: git://github.com/juanre/org-jekyll.git 10 | ;;; 11 | ;;; Commentary: 12 | ;;; 13 | ;;; Extract subtrees from your org-publish project files that have 14 | ;;; a :blog: keyword and an :on: property with a timestamp, and 15 | ;;; export them to a subdirectory _posts of your project's publishing 16 | ;;; directory in the year-month-day-title.html format that Jekyll 17 | ;;; expects. Properties are passed over as yaml front-matter in the 18 | ;;; exported files. The title of the subtree is the title of the 19 | ;;; entry. The title of the post is a link to the post's page. 20 | ;;; 21 | ;;; Look at http://orgmode.org/worg/org-tutorials/org-jekyll.html for 22 | ;;; more info on how to integrate org-mode with Jekyll, and for the 23 | ;;; inspiration of the main function down there. 24 | ;;; 25 | ;;; Code: 26 | 27 | ;;(require 'ox-html) 28 | 29 | (defvar org-jekyll-category nil 30 | "Specify a property which, if defined in the entry, is used as 31 | a category: the post is written to category/_posts. Ignored if 32 | nil. Use \"lang\" if you want to send posts in different 33 | languages to different directories.") 34 | 35 | (defvar org-jekyll-lang-subdirs nil 36 | "Make it an assoc list indexed by language if you want to 37 | bypass the category subdir definition and build blog subdirs per 38 | language.") 39 | 40 | (defvar org-jekyll-localize-dir nil 41 | "If non-nil and the lang property is set in the entry, 42 | org-jekyll will look for a lang.yml file in this directory and 43 | include it in the front matter of the exported entry.") 44 | 45 | (defvar org-jekyll-new-buffers nil 46 | "Buffers created to visit org-publish project files looking for blog posts.") 47 | 48 | (defun org-jekyll-publish-dir (project &optional category) 49 | "Where does the project go, by default a :blog-publishing-directory 50 | entry in the org-publish-project-alist." 51 | (princ category) 52 | (if org-jekyll-lang-subdirs 53 | (let ((pdir (plist-get (cdr project) :blog-publishing-directory)) 54 | (langdir (cdr (assoc category org-jekyll-lang-subdirs)))) 55 | (if langdir 56 | (concat pdir (cdr (assoc category org-jekyll-lang-subdirs)) 57 | "_posts/") 58 | (let ((ppdir (plist-get (cdr project) :blog-publishing-directory))) 59 | (unless ppdir 60 | (setq ppdir (plist-get (cdr project) :publishing-directory))) 61 | (concat ppdir 62 | (if category (concat category "/") "") 63 | "_posts/")))) 64 | (let ((pdir (plist-get (cdr project) :blog-publishing-directory))) 65 | (unless pdir 66 | (setq pdir (plist-get (cdr project) :publishing-directory))) 67 | (concat pdir 68 | (if category (concat category "/") "") 69 | "_posts/")))) 70 | 71 | (defun org-jekyll-site-root (project) 72 | "Site root, like http://yoursite.com, from which blog 73 | permalinks follow. Needed to replace entry titles with 74 | permalinks that RSS agregators and google buzz know how to 75 | follow. Looks for a :site-root entry in the org-publish-project-alist." 76 | (or (plist-get (cdr project) :site-root) 77 | "")) 78 | 79 | 80 | (defun org-get-jekyll-file-buffer (file) 81 | "Get a buffer visiting FILE. If the buffer needs to be 82 | created, add it to the list of buffers which might be released 83 | later. Copied from org-get-agenda-file-buffer, and modified 84 | the list that holds buffers to release." 85 | (let ((buf (org-find-base-buffer-visiting file))) 86 | (if buf 87 | buf 88 | (progn (setq buf (find-file-noselect file)) 89 | (if buf (push buf org-jekyll-new-buffers)) 90 | buf)))) 91 | 92 | (defun org-jekyll-slurp-yaml (fname) 93 | (remove "---" (if (file-exists-p fname) 94 | (split-string (with-temp-buffer 95 | (insert-file-contents fname) 96 | (buffer-string)) 97 | "\n" t)))) 98 | 99 | (defun ensure-directories-exist (fname) 100 | (let ((dir (file-name-directory fname))) 101 | (unless (file-accessible-directory-p dir) 102 | (make-directory dir t))) 103 | fname) 104 | 105 | (defun org-jekyll-sanitize-string (str project) 106 | (if (plist-get (cdr project) :jekyll-sanitize-permalinks) 107 | (progn (setq str (downcase str)) 108 | (dolist (c '(("á" . "a") 109 | ("é" . "e") 110 | ("í" . "i") 111 | ("ó" . "o") 112 | ("ú" . "u") 113 | ("à" . "a") 114 | ("è" . "e") 115 | ("ì" . "i") 116 | ("ò" . "o") 117 | ("ù" . "u") 118 | ("ñ" . "n") 119 | ("ç" . "s") 120 | ("\\$" . "S") 121 | ("€" . "E"))) 122 | (setq str (replace-regexp-in-string (car c) (cdr c) str))) 123 | (replace-regexp-in-string "[^abcdefghijklmnopqrstuvwxyz-]" "" 124 | (replace-regexp-in-string " +" "-" str))) 125 | str)) 126 | 127 | (defun org-jekyll-export-entry (project) 128 | (let* ((props (org-entry-properties nil 'standard)) 129 | (time (cdr (or (assoc "on" props) 130 | (assoc "ON" props)))) 131 | (lang (cdr (or (assoc "lang" props) 132 | (assoc "LANG" props)))) 133 | (category (if org-jekyll-category 134 | (cdr (assoc org-jekyll-category props)) 135 | nil)) 136 | (yaml-front-matter (copy-alist props))) 137 | (unless (assoc "layout" yaml-front-matter) 138 | (push '("layout" . "post") yaml-front-matter)) 139 | (when time 140 | (let* ((heading (org-get-heading t)) 141 | (title (replace-regexp-in-string "[:=\(\)\?]" "" 142 | (replace-regexp-in-string 143 | "[ \t]" "-" heading))) 144 | (str-time (and (string-match "\\([[:digit:]\-]+\\) " time) 145 | (match-string 1 time))) 146 | (to-file (format "%s-%s.html" str-time 147 | (org-jekyll-sanitize-string title project))) 148 | (org-buffer (current-buffer)) 149 | (yaml-front-matter (cons (cons "title" heading) 150 | yaml-front-matter)) 151 | html) 152 | (org-narrow-to-subtree) 153 | (let ((level (- (org-reduced-level (org-outline-level)) 1)) 154 | (top-level org-html-toplevel-hlevel) 155 | (contents (buffer-substring (point-min) (point-max))) 156 | (site-root (org-jekyll-site-root project))) 157 | ;; Without the promotion the header with which the headline 158 | ;; is exported depends on the level. With the promotion it 159 | ;; fails when the entry is not visible (ie, within a folded 160 | ;; entry). 161 | (dotimes (n level nil) (org-promote-subtree)) 162 | (setq html 163 | (replace-regexp-in-string 164 | (format "\\(.+\\)" 165 | top-level top-level) 166 | (format 167 | "\\1" 168 | top-level site-root top-level) 169 | (with-current-buffer 170 | (org-html-export-as-html nil t t t 171 | '(:tags nil 172 | :table-of-contents nil)) 173 | (buffer-string)))) 174 | (set-buffer org-buffer) 175 | (delete-region (point-min) (point-max)) 176 | (insert contents) 177 | (save-buffer)) 178 | (widen) 179 | (with-temp-file (ensure-directories-exist 180 | (expand-file-name 181 | to-file (org-jekyll-publish-dir project category))) 182 | (when yaml-front-matter 183 | (insert "---\n") 184 | (mapc (lambda (pair) 185 | (insert (format "%s: %s\n" (car pair) (cdr pair)))) 186 | yaml-front-matter) 187 | (if (and org-jekyll-localize-dir lang) 188 | (mapc (lambda (line) 189 | (insert (format "%s\n" line))) 190 | (org-jekyll-slurp-yaml (concat org-jekyll-localize-dir 191 | lang ".yml")))) 192 | (insert "---\n\n")) 193 | (insert html)))))) 194 | 195 | ; Evtl. needed to keep compiler happy: 196 | (declare-function org-publish-get-project-from-filename "org-publish" 197 | (filename &optional up)) 198 | 199 | ;;;###autoload 200 | (defun org-jekyll-export-current-entry () 201 | (interactive) 202 | (save-excursion 203 | (let ((project (org-publish-get-project-from-filename buffer-file-name))) 204 | (org-back-to-heading t) 205 | (org-jekyll-export-entry project)))) 206 | 207 | ;;;###autoload 208 | (defun org-jekyll-export-blog () 209 | "Export all entries in project files that have a :blog: keyword 210 | and an :on: datestamp. Property drawers are exported as 211 | front-matters, outline entry title is the exported document 212 | title. " 213 | (interactive) 214 | (save-excursion 215 | (setq org-jekyll-new-buffers nil) 216 | (let ((project (org-publish-get-project-from-filename (buffer-file-name)))) 217 | (mapc 218 | (lambda (jfile) 219 | (if (string= (file-name-extension jfile) "org") 220 | (with-current-buffer (org-get-jekyll-file-buffer jfile) 221 | ;; It fails for non-visible entries, CONTENT visibility 222 | ;; mode ensures that all of them are visible. 223 | (message (concat "org-jekyll: publishing " jfile )) 224 | (org-content) 225 | (org-map-entries (lambda () (org-jekyll-export-entry project)) 226 | "blog|BLOG")))) 227 | (org-publish-get-base-files project))) 228 | (org-release-buffers org-jekyll-new-buffers))) 229 | 230 | ;;;###autoload 231 | (defun org-jekyll-export-project (project-name) 232 | "Export all entries in project files that have a :blog: keyword 233 | and an :on: datestamp. Property drawers are exported as 234 | front-matters, outline entry title is the exported document 235 | title. " 236 | (interactive) 237 | (save-excursion 238 | (setq org-jekyll-new-buffers nil) 239 | (let ((project (assoc project-name org-publish-project-alist))) 240 | (mapc 241 | (lambda (jfile) 242 | (if (string= (file-name-extension jfile) (plist-get (cdr project) 243 | :base-extension)) 244 | (with-current-buffer (org-get-jekyll-file-buffer jfile) 245 | ;; It fails for non-visible entries, CONTENT visibility 246 | ;; mode ensures that all of them are visible. 247 | (message (concat "org-jekyll: publishing " jfile )) 248 | (org-content) 249 | (org-map-entries (lambda () (org-jekyll-export-entry project)) 250 | "blog|BLOG")))) 251 | (org-publish-get-base-files project))) 252 | (org-release-buffers org-jekyll-new-buffers))) 253 | 254 | (provide 'org-jekyll) 255 | 256 | ;;; org-jekyll.el ends here 257 | -------------------------------------------------------------------------------- /test/_posts/2009-12-26-Entry-in-the-second-file.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Entry in the second file 3 | layout: post 4 | on: <2009-12-26 Sat 13:58> 5 | CATEGORY: test-2 6 | --- 7 | 8 |
9 |

Entry in the second file    blog

10 |
11 | 12 |

Add something to it. 13 |

14 |
15 | -------------------------------------------------------------------------------- /test/_posts/2009-12-26-First-blog-entry.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: First blog entry 3 | on: <2009-12-26 Sat> 4 | layout: post 5 | extra: first 6 | CATEGORY: test 7 | --- 8 | 9 |
10 |

First blog entry

11 |
12 | 13 |

With some content in the first entry. 14 |

15 |
16 | -------------------------------------------------------------------------------- /test/_posts/2009-12-26-Second-blog-entry.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Second blog entry 3 | layout: post 4 | on: <2009-12-26 Sat 13:58> 5 | categories: test otro 6 | CATEGORY: test 7 | --- 8 | 9 |
10 |

Second blog entry

11 |
12 | 13 |

With content in the second. 14 |

15 |
16 | -------------------------------------------------------------------------------- /test/org.el: -------------------------------------------------------------------------------- 1 | 2 | (add-to-list 'org-publish-project-alist 3 | `("org-jekyll" 4 | :base-directory "~/src/prj/org-jekyll/test/" 5 | :recursive t 6 | :base-extension "org" 7 | :publishing-directory "~/src/prj/org-jekyll/test/" 8 | :publishing-function org-publish-org-to-html 9 | :section-numbers nil 10 | :headline-levels 4 11 | :table-of-contents nil 12 | :auto-index nil 13 | :auto-preamble nil 14 | :body-only t 15 | ;;:style ,(slurp-file-to-string 16 | ;; "~/cjr/jr/style/org/in-header.html") 17 | ;;:preamble ,(slurp-file-to-string 18 | ;; "~/cjr/open/org/preamble.html") 19 | ;;:postamble ,(slurp-file-to-string 20 | ;; "~/cjr/jr/style/org/postamble.html") 21 | :auto-postamble nil)) 22 | 23 | -------------------------------------------------------------------------------- /test/test-2.org: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #+begin_html 3 | --- 4 | layout: article 5 | title: Con mates y física 6 | category: mates, física 7 | --- 8 | #+end_html 9 | 10 | * Second one 11 | Test what happens when the blog post gets demoted in the outline 12 | *** Further down 13 | ***** Entry in the second file :blog: 14 | :PROPERTIES: 15 | :on: <2009-12-26 Sat 13:58> 16 | :END: 17 | Add something to it. 18 | * COMMENT Options 19 | #+FILETAGS: :cjr: 20 | -------------------------------------------------------------------------------- /test/test.org: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #+begin_html 3 | --- 4 | layout: default 5 | title: Org-jekyll test 6 | --- 7 | #+end_html 8 | 9 | * Non-blog entry 10 | See what it exports. 11 | *** With a second level 12 | And some content 13 | *** Blog :blog: 14 | ***** Second blog entry 15 | :PROPERTIES: 16 | :on: <2009-12-26 Sat 13:58> 17 | :categories: test otro 18 | :END: 19 | With content in /the second/. 20 | ***** Further indent 21 | ******* First blog entry 22 | :PROPERTIES: 23 | :on: <2009-12-26 Sat> 24 | :layout: post 25 | :extra: first 26 | :END: 27 | With some content in the first entry. 28 | --------------------------------------------------------------------------------