├── README.md ├── ox-jekyll-subtree.el └── ox-jekyll.el /README.md: -------------------------------------------------------------------------------- 1 | # ox-jekyll-subtree 2 | 3 | Extension to ox-jexkyll for better export of subtrees. This is only 4 | possible thanks to `ox-jekyll`, from the 5 | [org-octopress](https://github.com/yoshinari-nomura/org-octopress) 6 | repo (a copy is provided in this repo). 7 | 8 | *Please note, this is not a package, this is a script. Feel free to 9 | submit issues if you run into problems, just be aware that this does 10 | not fully conform with usual package standards.* 11 | 12 | ## Usage 13 | 14 | Place this in your `load-path`, add the following lines to your init file, and invoke `M-x ojs-export-to-blog` to export a subtree as a blog post. 15 | 16 | ``` 17 | (autoload 'ojs-export-to-blog "ox-jekyll-subtree") 18 | (setq org-jekyll-use-src-plugin t) 19 | 20 | ;; Obviously, these two need to be changed for your blog. 21 | (setq ojs-blog-base-url "http://endlessparentheses.com/") 22 | (setq ojs-blog-dir (expand-file-name "~/Git-Projects/blog/")) 23 | ``` 24 | -------------------------------------------------------------------------------- /ox-jekyll-subtree.el: -------------------------------------------------------------------------------- 1 | ;;; ox-jekyll-subtree.el --- Extension to ox-jexkyll for better export of subtrees -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015 Artur Malabarba 4 | 5 | ;; Author: Artur Malabarba 6 | ;; Keywords: hypermedia 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | ;; 23 | ;; Extension to ox-jexkyll for better export of subtrees. This is only 24 | ;; possible thanks to `ox-jekyll`, from the 25 | ;; [org-octopress](https://github.com/yoshinari-nomura/org-octopress) 26 | ;; repo (a copy is provided in this repo). 27 | ;; 28 | ;; *Please note, this is not a package, this is a script. Feel free to 29 | ;; submit issues if you run into problems, just be aware that this does 30 | ;; not fully conform with usual package standards.* 31 | ;; 32 | ;;; Usage 33 | ;; 34 | ;; Place this in your `load-path`, add the following lines to your init file, and invoke `M-x ojs-export-to-blog` to export a subtree as a blog post. 35 | ;; 36 | ;; ``` 37 | ;; (autoload 'ojs-export-to-blog "jekyll-once") 38 | ;; (setq org-jekyll-use-src-plugin t) 39 | ;; 40 | ;; ;; Obviously, these two need to be changed for your blog. 41 | ;; (setq ojs-blog-base-url "http://endlessparentheses.com/") 42 | ;; (setq ojs-blog-dir (expand-file-name "~/Git/blog/")) 43 | ;; ``` 44 | 45 | ;;; Code: 46 | 47 | (require 'subr-x) 48 | 49 | (defcustom ojs-blog-dir (expand-file-name "~/Git/blog/") 50 | "Directory to save posts." 51 | :type 'directory 52 | :group 'endless) 53 | 54 | (defcustom ojs-blog-base-url "http://endlessparentheses.com/" 55 | "Base URL of the blog. 56 | Will be stripped from link addresses on the final HTML." 57 | :type 'string 58 | :group 'endless) 59 | 60 | (defvar ojs--post-date-format "%Y-%m-%d") 61 | 62 | (defun ojs-export-to-blog (dont-show) 63 | "Exports current subtree as jekyll html and copies to blog. 64 | Posts need very little to work, most information is guessed. 65 | Scheduled date is respected and heading is marked as DONE. 66 | 67 | Pages are marked by a \":EXPORT_JEKYLL_LAYOUT: page\" property, 68 | and they also need a :filename: property. Schedule is then 69 | ignored, and the file is saved inside `ojs-blog-dir'. 70 | 71 | The filename property is not mandatory for posts. If present, it 72 | will used exactly (no sanitising will be done). If not, filename 73 | will be a sanitised version of the title, see 74 | `ojs-sanitise-file-name'." 75 | (interactive "P") 76 | (require 'org) 77 | (require 'ox-jekyll) 78 | (save-excursion 79 | ;; Actual posts NEED a TODO state. So we go up the tree until we 80 | ;; reach one. 81 | (while (null (org-entry-get (point) "TODO" nil t)) 82 | (outline-up-heading 1 t)) 83 | (org-entry-put (point) "EXPORT_JEKYLL_LAYOUT" 84 | (org-entry-get (point) "EXPORT_JEKYLL_LAYOUT" t)) 85 | ;; Try the closed stamp first to make sure we don't set the front 86 | ;; matter to 00:00:00 which moves the post back a day 87 | (let* ((closed-stamp (org-entry-get (point) "CLOSED" t)) 88 | (date (if closed-stamp 89 | (date-to-time closed-stamp) 90 | (org-get-scheduled-time (point) nil))) 91 | (tags (nreverse (org-get-tags-at))) 92 | (meta-title (org-entry-get (point) "meta_title")) 93 | (is-page (string= (org-entry-get (point) "EXPORT_JEKYLL_LAYOUT") "page")) 94 | (name (org-entry-get (point) "filename")) 95 | (title (org-get-heading t t)) 96 | (series (org-entry-get (point) "series" t)) 97 | (org-jekyll-categories (mapconcat #'ojs-convert-tag tags " ")) 98 | (org-export-show-temporary-export-buffer nil)) 99 | 100 | (unless date 101 | (org-schedule nil ".") 102 | (setq date (current-time))) 103 | ;; For pages, demand filename. 104 | (if is-page 105 | (unless name (error "Pages need a :filename: property")) 106 | ;; For posts, guess some information that wasn't provided as 107 | ;; properties. 108 | ;; Define a name, if there isn't one. 109 | (unless name 110 | (setq name (concat (format-time-string "%Y-%m-%d" date) "-" (ojs-sanitise-file-name title))) 111 | (org-entry-put (point) "filename" name)) 112 | (org-todo 'done)) 113 | 114 | (let ((subtree-content 115 | (save-restriction 116 | (org-narrow-to-subtree) 117 | (ignore-errors (ispell-buffer)) 118 | (buffer-string))) 119 | (header-content 120 | (ojs-get-org-headers)) 121 | (reference-buffer (current-buffer))) 122 | (with-temp-buffer 123 | (ojs-prepare-input-buffer 124 | header-content subtree-content reference-buffer) 125 | 126 | ;; Export and then do some fixing on the output buffer. 127 | (with-current-buffer 128 | (org-jekyll-export-as-html nil t nil nil nil) 129 | (goto-char (point-min)) 130 | ;; Configure the jekyll header. 131 | (search-forward "\n---\n") 132 | (goto-char (1+ (match-beginning 0))) 133 | (when series 134 | (insert "series: \"" series "\"\n")) 135 | (when meta-title 136 | (insert "meta_title: \"" (format meta-title title) "\"\n")) 137 | (search-backward-regexp "\ndate *:\\(.*\\)$") 138 | (if is-page 139 | ;; Pages don't need a date field. 140 | (replace-match "" :fixedcase :literal nil 0) 141 | (replace-match (concat " " (format-time-string ojs--post-date-format date)) 142 | :fixedcase :literal nil 1)) 143 | 144 | ;; Save the final file. 145 | (ojs-clean-output-links) 146 | (let ((out-file 147 | (expand-file-name (concat (if is-page "" "_posts/") name ".html") 148 | ojs-blog-dir))) 149 | (write-file out-file) 150 | (unless dont-show 151 | (find-file-other-window out-file))) 152 | 153 | ;; In case we commit, lets push the message to the kill-ring 154 | (kill-new (concat "UPDATE: " title)) 155 | (kill-new (concat "POST: " title)))))))) 156 | 157 | (defun ojs-get-org-headers () 158 | "Return everything above the first headline of current buffer." 159 | (save-excursion 160 | (goto-char (point-min)) 161 | (search-forward-regexp "^\\*+ ") 162 | (buffer-substring-no-properties (point-min) (match-beginning 0)))) 163 | 164 | (defvar ojs-base-regexp 165 | (macroexpand `(rx (or ,ojs-blog-base-url ,ojs-blog-dir))) 166 | "") 167 | 168 | (defun ojs-clean-output-links () 169 | "Strip `ojs-blog-base-url' and \"file://\" from the start of URLs. " 170 | ;; Fix org's stupid filename handling. 171 | (goto-char (point-min)) 172 | (while (search-forward-regexp "\\(href\\|src\\)=\"\\(file://\\)/" nil t) 173 | (replace-match "" :fixedcase :literal nil 2)) 174 | ;; Strip base-url from links 175 | (goto-char (point-min)) 176 | (while (search-forward-regexp 177 | (concat "href=\"" ojs-base-regexp) 178 | nil t) 179 | (replace-match "href=\"/" :fixedcase :literal)) 180 | (goto-char (point-min))) 181 | 182 | (defun ojs-prepare-input-buffer (header content reference-buffer) 183 | "Insert content and clean it up a bit." 184 | (insert header content) 185 | (goto-char (point-min)) 186 | (org-mode) 187 | (outline-next-heading) 188 | (let ((this-filename (org-entry-get nil "filename" t)) 189 | target-filename) 190 | (while (progn (org-next-link) 191 | (not org-link-search-failed)) 192 | (cond 193 | ((looking-at (format "\\[\\[\\(file:%s\\)" 194 | (regexp-quote (abbreviate-file-name ojs-blog-dir)))) 195 | (replace-match "file:/" nil nil nil 1) 196 | (goto-char (match-beginning 0)) 197 | (when (looking-at (rx "[[" (group "file:/images/" (+ (not space))) "]]")) 198 | (goto-char (match-end 1)) 199 | (forward-char 1) 200 | (insert "[" (match-string 1) "]") 201 | (forward-char 1))) 202 | ((looking-at "\\[\\[\\(\\*[^]]+\\)\\]") 203 | ;; Find the blog post to which this link points. 204 | (setq target-filename 205 | (save-excursion 206 | (save-match-data 207 | (let ((point (with-current-buffer reference-buffer 208 | (point)))) 209 | (org-open-at-point t reference-buffer) 210 | (with-current-buffer reference-buffer 211 | (prog1 (url-hexify-string (org-entry-get nil "filename" t)) 212 | (goto-char point))))))) 213 | ;; We don't want to replace links inside the same post. Org 214 | ;; handles them better than us. 215 | (when (and target-filename 216 | (null (string= target-filename this-filename))) 217 | (replace-match 218 | (format "/%s.html" (ojs-strip-date-from-filename target-filename)) 219 | :fixedcase :literal nil 1)))))) 220 | (goto-char (point-min)) 221 | (outline-next-heading)) 222 | 223 | (defun ojs-strip-date-from-filename (name) 224 | (replace-regexp-in-string "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-" "" name)) 225 | 226 | (defun ojs-convert-tag (tag) 227 | "Overcome org-mode's tag limitations." 228 | (replace-regexp-in-string 229 | "_" "-" 230 | (replace-regexp-in-string "__" "." tag))) 231 | 232 | (defun ojs-sanitise-file-name (name) 233 | "Make NAME safe for filenames. 234 | Removes any occurrence of parentheses (with their content), 235 | Trims the result, 236 | And transforms anything that's not alphanumeric into dashes." 237 | (require 'url-util) 238 | (require 'subr-x) 239 | (url-hexify-string 240 | (downcase 241 | (replace-regexp-in-string 242 | "[^[:alnum:]]+" "-" 243 | (string-trim 244 | (replace-regexp-in-string 245 | "(.*)" "" name)))))) 246 | 247 | (provide 'ox-jekyll-subtree) 248 | ;;; ox-jekyll-subtree.el ends here 249 | -------------------------------------------------------------------------------- /ox-jekyll.el: -------------------------------------------------------------------------------- 1 | ;;; ox-jekyll.el --- Export Jekyll articles using org-mode. 2 | 3 | ;; Copyright (C) 2013 Yoshinari Nomura 4 | 5 | ;; Author: Yoshinari Nomura 6 | ;; Author: Justin Gordon 7 | ;; Keywords: org, jekyll 8 | ;; Version: 0.1 9 | 10 | ;;; Commentary: 11 | 12 | ;; This library implements a Jekyll-style html backend for 13 | ;; Org exporter, based on `html' back-end. 14 | ;; 15 | ;; It provides two commands for export, depending on the desired 16 | ;; output: `org-jkl-export-as-html' (temporary buffer) and 17 | ;; `org-jkl-export-to-html' ("html" file with YAML front matter). 18 | ;; 19 | ;; For publishing, `org-jekyll-publish-to-html' is available. 20 | ;; For composing, `org-jekyll-insert-export-options-template' is available. 21 | 22 | ;;; Code: 23 | 24 | ;;; Dependencies 25 | 26 | (require 'ox-html) 27 | (defvar org-html-display-buffer-mode) 28 | ;;; User Configurable Variables 29 | 30 | (defgroup org-export-jekyll nil 31 | "Options for exporting Org mode files to jekyll HTML." 32 | :tag "Org Jekyll" 33 | :group 'org-export 34 | :version "24.2") 35 | 36 | (defcustom org-jekyll-include-yaml-front-matter t 37 | "If true, then include yaml-front-matter when exporting to html. 38 | 39 | If false, then you should include the yaml front matter like this at the top of the file: 40 | 41 | #+BEGIN_HTML 42 | --- 43 | layout: post 44 | title: \"Upgrading Octopress\" 45 | date: 2013-09-15 22:08 46 | comments: true 47 | categories: [octopress, rubymine] 48 | keywords: Octopress 49 | description: Instructions on Upgrading Octopress 50 | --- 51 | #+END_HTML" 52 | :group 'org-export-jekyll 53 | :type 'boolean) 54 | 55 | (defcustom org-jekyll-layout "post" 56 | "Default layout used in Jekyll article." 57 | :group 'org-export-jekyll 58 | :type 'string) 59 | 60 | (defcustom org-jekyll-categories "" 61 | "Default space-separated categories in Jekyll article." 62 | :group 'org-export-jekyll 63 | :type 'string) 64 | 65 | (defcustom org-jekyll-published "true" 66 | "Default publish status in Jekyll article." 67 | :group 'org-export-jekyll 68 | :type 'string) 69 | 70 | (defcustom org-jekyll-comments "" 71 | "Default comments (disqus) flag in Jekyll article." 72 | :group 'org-export-jekyll 73 | :type 'string) 74 | 75 | (defcustom org-jekyll-use-src-plugin nil 76 | "If t, org-jekyll exporter eagerly uses plugins instead of 77 | org-mode's original HTML stuff. For example: 78 | 79 | #+BEGIN_SRC ruby 80 | puts \"Hello world\" 81 | #+END_SRC 82 | 83 | makes: 84 | 85 | {% highlight ruby %} 86 | puts \"Hello world\" 87 | {% endhighlight %}" 88 | :group 'org-export-jekyll-use-src-plugin 89 | :type 'boolean) 90 | 91 | 92 | ;;; Define Back-End 93 | (org-export-define-derived-backend 'jekyll 'html 94 | ;; :export-block '("HTML" "JEKYLL") 95 | :menu-entry 96 | '(?j "Jekyll: export to HTML with YAML front matter." 97 | ((?H "As HTML buffer" org-jekyll-export-as-html) 98 | (?h "As HTML file" org-jekyll-export-to-html))) 99 | :translate-alist 100 | '((template . org-jekyll-template) ;; add YAML front matter. 101 | (src-block . org-jekyll-src-block) 102 | (inner-template . org-jekyll-inner-template)) ;; force body-only 103 | :options-alist 104 | '((:jekyll-layout "JEKYLL_LAYOUT" nil org-jekyll-layout) 105 | (:jekyll-categories "JEKYLL_CATEGORIES" nil org-jekyll-categories) 106 | (:jekyll-published "JEKYLL_PUBLISHED" nil org-jekyll-published) 107 | (:jekyll-comments "JEKYLL_COMMENTS" nil org-jekyll-comments))) 108 | 109 | 110 | ;;; Internal Filters 111 | (defun org-jekyll-src-block (src-block contents info) 112 | "Transcode SRC-BLOCK element into jekyll code template format 113 | if `org-jekyll-use-src-plugin` is t. Otherwise, perform as 114 | `org-html-src-block`. CONTENTS holds the contents of the item. 115 | INFO is a plist used as a communication channel." 116 | (if org-jekyll-use-src-plugin 117 | (let ((language (org-element-property :language src-block)) 118 | (value (org-remove-indentation 119 | (org-element-property :value src-block)))) 120 | (format "{%% highlight %s %%}\n%s{%% endhighlight %%}" 121 | (replace-regexp-in-string 122 | "emacs-lisp" "cl" 123 | (format "%s" language)) value)) 124 | (org-export-with-backend 'html src-block contents info))) 125 | 126 | ;;; Template 127 | (defun org-jekyll-template (contents info) 128 | "Return complete document string after HTML conversion. 129 | CONTENTS is the transcoded contents string. INFO is a plist 130 | holding export options." 131 | (if org-jekyll-include-yaml-front-matter 132 | (concat 133 | (org-jekyll--yaml-front-matter info) 134 | contents) 135 | contents)) 136 | 137 | (defun org-jekyll-inner-template (contents info) 138 | "Return body of document string after HTML conversion. 139 | CONTENTS is the transcoded contents string. INFO is a plist 140 | holding export options." 141 | (concat 142 | ;; Table of contents. 143 | (let ((depth (plist-get info :with-toc))) 144 | (when depth (org-html-toc depth info))) 145 | ;; ;; PREVIEW mark on the top of article. 146 | ;; (unless (equal "true" (plist-get info :jekyll-published)) 147 | ;; "PREVIEW") 148 | ;; Document contents. 149 | contents 150 | ;; Footnotes section. 151 | (org-html-footnote-section info))) 152 | 153 | ;;; YAML Front Matter 154 | (defun org-jekyll--get-option (info property-name &optional default) 155 | (let ((property (org-export-data (plist-get info property-name) info))) 156 | (format "%s" (or property default "")))) 157 | 158 | (defun org-jekyll--yaml-front-matter (info) 159 | (let ((title 160 | (org-jekyll--get-option info :title)) 161 | (date 162 | (org-jekyll--get-option info :date)) 163 | (layout 164 | (org-jekyll--get-option info :jekyll-layout org-jekyll-layout)) 165 | (categories 166 | (org-jekyll--get-option info :jekyll-categories org-jekyll-categories))) 167 | (concat 168 | "---" 169 | "\ntitle: \"" title 170 | "\"\ndate: " date 171 | "\nlayout: " layout 172 | "\ntags: " categories 173 | "\n---\n"))) 174 | 175 | ;;; Filename and Date Helper 176 | (defun org-jekyll-date-from-filename (&optional filename) 177 | (let ((fn (file-name-nondirectory (or filename (buffer-file-name))))) 178 | (if (string-match "^[0-9]+-[0-9]+-[0-9]+" fn) 179 | (match-string 0 fn) 180 | nil))) 181 | 182 | (defun org-jekyll-property-list (&optional filename) 183 | (let ((backend 'jekyll) plist) 184 | (if filename 185 | (with-temp-buffer 186 | (insert-file-contents filename) 187 | (org-mode) 188 | (setq plist (org-export-get-environment backend)) 189 | (setq plist (plist-put plist :input-file filename))) 190 | (setq plist (org-export-backend-options backend)) 191 | plist))) 192 | 193 | (defun org-jekyll-property (keys &optional filename) 194 | (let ((plist (org-jekyll-property-list filename))) 195 | (mapcar (lambda (key) (org-export-data-with-backend (plist-get plist key) 'jekyll plist)) 196 | keys))) 197 | 198 | (defun org-jekyll-date-from-property (&optional filename) 199 | (let ((plist (org-jekyll-property filename))) 200 | (org-read-date 201 | nil nil 202 | (org-export-data-with-backend (plist-get plist :date) 'jekyll plist)))) 203 | 204 | (defun org-jekyll-create-filename () 205 | (let ((date (org-jekyll-date-from-property)) 206 | (file (file-name-nondirectory (buffer-file-name))) 207 | (dir (file-name-directory (buffer-file-name)))) 208 | (expand-file-name 209 | (replace-regexp-in-string "^[0-9]+-[0-9]+-[0-9]+" date file) 210 | dir))) 211 | 212 | ;;; End-User functions 213 | ;;;###autoload 214 | (defun org-jekyll-export-subtree-as-html 215 | (&optional async visible-only body-only ext-plist) 216 | "Export current subtree to a HTML buffer adding some YAML front matter." 217 | (interactive) 218 | ) 219 | 220 | ;;;###autoload 221 | (defun org-jekyll-export-as-html 222 | (&optional async subtreep visible-only body-only ext-plist) 223 | "Export current buffer to a HTML buffer adding some YAML front matter." 224 | (interactive) 225 | (if async 226 | (org-export-async-start 227 | (lambda (output) 228 | (with-current-buffer (get-buffer-create "*Org Jekyll HTML Export*") 229 | (erase-buffer) 230 | (insert output) 231 | (goto-char (point-min)) 232 | (funcall org-html-display-buffer-mode) 233 | (org-export-add-to-stack (current-buffer) 'jekyll))) 234 | `(org-export-as 'jekyll ,subtreep ,visible-only ,body-only ',ext-plist)) 235 | (let ((outbuf (org-export-to-buffer 236 | 'jekyll "*Org Jekyll HTML Export*" 237 | nil subtreep visible-only body-only ext-plist))) 238 | ;; Set major mode. 239 | (with-current-buffer outbuf (set-auto-mode t)) 240 | (if org-export-show-temporary-export-buffer 241 | (switch-to-buffer-other-window outbuf) 242 | outbuf)))) 243 | 244 | ;;;###autoload 245 | (defun org-jekyll-export-to-html 246 | (&optional async subtreep visible-only body-only ext-plist) 247 | "Export current buffer to a HTML file adding some YAML front matter." 248 | (interactive) 249 | (let* ((extension (concat "." org-html-extension)) 250 | (file (org-export-output-file-name extension subtreep)) 251 | (org-export-coding-system org-html-coding-system)) 252 | (if async 253 | (org-export-async-start 254 | (lambda (f) (org-export-add-to-stack f 'jekyll)) 255 | (let ((org-export-coding-system org-html-coding-system)) 256 | `(expand-file-name 257 | (org-export-to-file 258 | 'jekyll ,file ,subtreep ,visible-only ,body-only ',ext-plist)))) 259 | (let ((org-export-coding-system org-html-coding-system)) 260 | (org-export-to-file 261 | 'jekyll file nil subtreep visible-only body-only ext-plist))))) 262 | 263 | ;;;###autoload 264 | (defun org-jekyll-publish-to-html (plist filename pub-dir) 265 | "Publish an org file to HTML with YAML front matter. 266 | 267 | FILENAME is the filename of the Org file to be published. PLIST 268 | is the property list for the given project. PUB-DIR is the 269 | publishing directory. 270 | 271 | Return output file name." 272 | (org-publish-org-to 'jekyll filename ".html" plist pub-dir)) 273 | 274 | ;;;###autoload 275 | (defun org-jekyll-insert-export-options-template 276 | (&optional title date setupfile categories published layout) 277 | "Insert a settings template for Jekyll exporter." 278 | (interactive) 279 | (let ((layout (or layout org-jekyll-layout)) 280 | (published (or published org-jekyll-published)) 281 | (categories (or categories org-jekyll-categories))) 282 | (save-excursion 283 | (insert (format (concat 284 | "#+TITLE: " title 285 | "\n#+DATE: " date 286 | "\n#+SETUPFILE: " setupfile 287 | "\n#+JEKYLL_LAYOUT: " layout 288 | "\n#+JEKYLL_CATEGORIES: " categories 289 | "\n#+JEKYLL_PUBLISHED: " published 290 | "\n\n* \n\n{{{more}}}")))))) 291 | 292 | ;;; provide 293 | 294 | (provide 'ox-jekyll) 295 | 296 | ;;; ox-jekyll.el ends here 297 | --------------------------------------------------------------------------------