├── .gitignore ├── Cask ├── hublo ├── src ├── hublo-page.el ├── hublo-item.el ├── hublo-types.el ├── hublo-config.el ├── hublo-utils.el ├── hublo-mustache.el ├── hublo-dsl.el ├── hublo-org.el ├── hublo.el ├── hublo-cli.el └── hublo-transform.el ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /site 3 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source melpa) 2 | (depends-on "dash") 3 | (depends-on "mustache") 4 | 5 | (source org) 6 | (depends-on "org") 7 | -------------------------------------------------------------------------------- /hublo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/emacs --script 2 | 3 | (require 'cask "~/.cask/cask.el") 4 | (cask-initialize) 5 | 6 | (add-to-list 'load-path (expand-file-name "./src")) 7 | (load "hublo.el") 8 | (hb/run-script) 9 | -------------------------------------------------------------------------------- /src/hublo-page.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defvar *hb/pages* nil) 4 | 5 | (defun hb/register-page (pattern transforms) 6 | (push (make-hb/page :pattern pattern :transforms transforms) *hb/pages*)) 7 | -------------------------------------------------------------------------------- /src/hublo-item.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/item-from (path) 4 | (make-hb/item 5 | :path path 6 | :payload "" 7 | :route "" 8 | :transforms (hb/find-handler-transforms path) 9 | :meta (ht-create))) 10 | -------------------------------------------------------------------------------- /src/hublo-types.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defstruct hb/config source-dir output-dir context) 4 | (defstruct hb/transform name phases) 5 | (defstruct hb/page pattern transforms) 6 | (defstruct hb/handler pattern transforms) 7 | (defstruct hb/item path payload route transforms meta) 8 | -------------------------------------------------------------------------------- /src/hublo-config.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/default-config () 4 | (make-hb/config 5 | :source-dir (or (getenv "HUBLO_SOURCE_DIR") "sources") 6 | :output-dir (or (getenv "HUBLO_OUTPUT_DIR") "output") 7 | :context (ht (:groups (ht-create))))) 8 | 9 | (defvar *hb/config* 10 | (hb/default-config)) 11 | 12 | (defun hb/load-config (config-file) 13 | "Load a configuration file in teh context of the configuration DSL." 14 | (hb/build-config 15 | (load-file config-file))) 16 | -------------------------------------------------------------------------------- /src/hublo-utils.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/get-in (h keys) 4 | (let* ((ks (if (vectorp keys) (mapcar 'identity keys) keys)) 5 | (k (car ks))) 6 | (if k 7 | (let ((new-h (ht-get h k))) 8 | (when new-h 9 | (hb/get-in new-h (cdr ks)))) 10 | h))) 11 | 12 | (defun hb/walk-dir (dir) 13 | (-mapcat (lambda (e) 14 | (let ((path (concat dir "/" e))) 15 | (when (not (string-match "^\\." e)) 16 | (if (file-directory-p path) 17 | (hb/walk-dir path) 18 | (list path))))) 19 | (directory-files dir))) 20 | 21 | (defun hb/file->string (path) 22 | (with-temp-buffer 23 | (insert-file-contents path) 24 | (buffer-string))) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Pierre-Yves Ritschard 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/hublo-mustache.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/find-mustache-layout (layout) 4 | (let ((path (format "%s/layouts/%s.mustache" 5 | (hb/config-source-dir *hb/config*) 6 | layout))) 7 | (hb/file->string path))) 8 | 9 | (defun hb/mustache-layout-transform (layout) 10 | (let ((tpl (hb/find-mustache-layout layout))) 11 | (list 12 | :content 13 | (lambda (item) 14 | (ht-set (hb/item-meta item) :payload (hb/item-payload item)) 15 | (setf (hb/item-payload item) 16 | (let ((mustache-key-type 'keyword)) 17 | (mustache-render tpl (hb/item-meta item)))) 18 | (ht-remove (hb/item-meta item) :payload))))) 19 | 20 | (defun hb/mustache-route-transform (route) 21 | (list 22 | :route 23 | (lambda (item) 24 | (let* ((mustache-key-type 'keyword) 25 | (route (mustache-render route (hb/item-meta item)))) 26 | (setf (hb/item-route item) route) 27 | (ht-set! (hb/item-meta item) :route route))))) 28 | 29 | (defun hb/mustache-transform (item) 30 | (let ((payload (substring-no-properties (hb/item-payload item)))) 31 | (setf (hb/item-payload item) 32 | (let ((mustache-key-type 'keyword)) 33 | (mustache-render payload 34 | (hb/item-meta item)))))) 35 | 36 | (hb/register-transform :mustache 37 | :augment 'hb/mustache-transform) 38 | -------------------------------------------------------------------------------- /src/hublo-dsl.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/dsl-source-dir (x) 4 | (setf (hb/config-source-dir *hb/config*) x)) 5 | 6 | (defun hb/dsl-output-dir (x) 7 | (setf (hb/config-output-dir *hb/config*) x)) 8 | 9 | (defun hb/dsl-context (k v) 10 | (ht-set! (hb/config-context *hb/config*) k v)) 11 | 12 | (defun hb/dsl-handle! (pattern &rest transforms) 13 | (hb/register-handler pattern transforms)) 14 | 15 | (defun hb/dsl-page (pattern &rest transforms) 16 | (hb/register-page pattern transforms)) 17 | 18 | (defun hb/dsl-title (title) 19 | (hb/metadata-transform :title title)) 20 | 21 | (defmacro hb/build-config (&rest body) 22 | "Execute body in the context of the configuration DSL." 23 | `(cl-letf (((symbol-function 'source-dir) #'hb/dsl-source-dir) 24 | ((symbol-function 'output-dir) #'hb/dsl-output-dir) 25 | ((symbol-function 'context) #'hb/dsl-context) 26 | ((symbol-function 'handle!) #'hb/dsl-handle!) 27 | ((symbol-function 'page) #'hb/dsl-page) 28 | ((symbol-function 'layout) #'hb/mustache-layout-transform) 29 | ((symbol-function 'route) #'hb/mustache-route-transform) 30 | ((symbol-function 'title) #'hb/dsl-title) 31 | ((symbol-function 'slugify) #'hb/slug-transform) 32 | ((symbol-function 'metadata) #'hb/metadata-transform) 33 | ((symbol-function 'group-with) #'hb/group-transform)) 34 | (progn ,@body))) 35 | -------------------------------------------------------------------------------- /src/hublo-org.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defun hb/load-org-file (meta) 4 | "Parse org-mode metadata and update metadata" 5 | (-each (-partition 2 (org-export--get-inbuffer-options)) 6 | (lambda (x) 7 | (ht-set! meta (car x) (car (car (cdr x)))) 8 | (when (equal (car x) :date) 9 | (let ((date (parse-time-string (car (car (cdr x)))))) 10 | (ht-set! meta :year (format "%04d" (nth 5 date))) 11 | (ht-set! meta :month (format "%02d" (nth 4 date))) 12 | (ht-set! meta :day (format "%02d" (nth 3 date)))))))) 13 | 14 | (defun hb/org-publish (content) 15 | "" 16 | (with-temp-buffer 17 | (insert-string content) 18 | (goto-char (point-min)) 19 | (let ((old-val org-export-show-temporary-export-buffer) 20 | (bf (current-buffer))) 21 | (org-html-export-as-html nil nil nil t nil) 22 | (setq org-export-show-temporary-export-buffer old-val) 23 | (switch-to-buffer "*Org HTML Export*") 24 | (let ((out (buffer-string))) 25 | (kill-buffer (current-buffer)) 26 | (switch-to-buffer bf) 27 | out)))) 28 | 29 | (defun hb/org-metadata (item) 30 | (save-excursion 31 | (with-temp-buffer 32 | (insert-string (hb/item-payload item)) 33 | (goto-char (point-min)) 34 | (hb/load-org-file (hb/item-meta item))))) 35 | 36 | (defun hb/org-content (item) 37 | (let* ((content (hb/file->string (hb/item-path item))) 38 | (payload (hb/org-publish content))) 39 | (setf (hb/item-payload item) payload) 40 | (ht-set (hb/item-meta item) :content payload))) 41 | 42 | (hb/register-transform :org 43 | :metadata 'hb/org-metadata 44 | :content 'hb/org-content) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hublo: the world needed another site generator 2 | ============================================== 3 | 4 | ### Configuration 5 | 6 | ```elisp 7 | (source-dir "site/sources") 8 | (output-dir "site/out") 9 | 10 | (handle! "\\.org$" :org) 11 | (handle! "\\.html$" :noop) 12 | (handle! "\\.css$" :noop) 13 | (handle! "\\.mustache$" :mustache) 14 | 15 | (page "/entries/.*" 16 | (slugify) 17 | (route "/entries/{{year}}/{{month}}/{{day}}/{{slug}}.html") 18 | (group-with :entries) 19 | (layout "blog") 20 | (layout "default")) 21 | 22 | (page "/index.mustache" 23 | (route "/index.html") 24 | (title "home") 25 | (layout "default")) 26 | 27 | (page "/index.xml.mustache" 28 | (title "spootnik") 29 | (metadata :description "my blog") 30 | (route "/index.xml")) 31 | 32 | (page "/static/.*") 33 | ``` 34 | 35 | ### Running 36 | 37 | ``` 38 | hublo clean 39 | hublo publish 40 | hublo phases 41 | ``` 42 | 43 | ### Philosophy 44 | 45 | Hublo provides a way to build a site by publishing pages in different *phases*. 46 | A phase is a barrier that must be reached by all pages before advancing to the next. 47 | Each hublo command consists of a list of phases to run through, some shortcuts 48 | are defined in `hublo-cli.el`. 49 | 50 | For instance, the most common hublo command is `publish` and translates to 51 | the following phases: `:bootstrap`, `:metadata`, `:route`, `:content`, `:augment`, `:publish`, and `:clean`. 52 | 53 | This allows some steps to gather information or metadata from other pages which provides 54 | an elegant way to build lists and incrementally produce content. 55 | 56 | In the above example, blog entries are grouped together with the label `:entries` which is 57 | then accessible in templates. This allows the index page and an rss feed page to go through 58 | the list. Similar techniques can be used for publishing content dynamically. 59 | 60 | The configuration is processed as elisp code and can thus accomodate for plenty of scenarios. 61 | 62 | There is an example configuration at https://github.com/pyr/blog with the corresponding output 63 | at: http://spootnik.org. 64 | -------------------------------------------------------------------------------- /src/hublo.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | ;; Our full dependency list 4 | ;; ======================== 5 | (require 'cl-lib) 6 | (require 'ht) 7 | (require 'dash) 8 | (require 'mustache) 9 | (require 'org) 10 | (require 'ox-html) 11 | 12 | ;; Library functions 13 | ;; ================= 14 | (load "hublo-utils.el") 15 | (load "hublo-types.el") 16 | (load "hublo-config.el") 17 | (load "hublo-transform.el") 18 | (load "hublo-page.el") 19 | (load "hublo-item.el") 20 | (load "hublo-dsl.el") 21 | 22 | ;; Transform plugins 23 | ;; ================= 24 | (load "hublo-mustache.el") 25 | (load "hublo-org.el") 26 | 27 | (defun hb/item-match-p (page item) 28 | (let ((subpath (replace-regexp-in-string 29 | (format "^%s" (hb/config-source-dir *hb/config*)) 30 | "" 31 | (hb/item-path item)))) 32 | (string-match-p (hb/page-pattern page) subpath))) 33 | 34 | (defun hb/run-phases (&rest phases) 35 | 36 | (let ((candidates (->> (hb/walk-dir (hb/config-source-dir *hb/config*)) 37 | (-map 'hb/item-from) 38 | (-remove (lambda (x) (equal x nil))))) 39 | (items nil)) 40 | 41 | (dolist (page *hb/pages*) 42 | (dolist (item (-filter (-partial 'hb/item-match-p page) candidates)) 43 | (push (cons item (-concat hb/base-transforms 44 | (hb/item-transforms item) 45 | (hb/page-transforms page))) 46 | items))) 47 | 48 | (dolist (phase phases) 49 | (message "running through phase: %s" phase) 50 | (dolist (tuple items) 51 | (let ((item (car tuple)) 52 | (transforms (->> (cdr tuple) 53 | (-filter (lambda (x) (equal (car x) phase))) 54 | (-map 'cadr)))) 55 | ;; always merge global and local context 56 | ;; this makes it easier for templates 57 | (ht-update! (hb/item-meta item) (hb/config-context *hb/config*)) 58 | (dolist (transform transforms) 59 | (funcall transform item)))))) 60 | (message "ran through all phases correctly")) 61 | 62 | ;; Now pull-in our CLI helpers 63 | ;; =========================== 64 | (load "hublo-cli.el") 65 | 66 | (provide 'hublo) 67 | -------------------------------------------------------------------------------- /src/hublo-cli.el: -------------------------------------------------------------------------------- 1 | (setq hb/default-config-file "hublo.config") 2 | 3 | (defun hb/noop () 4 | "Shortcut for a no-op run" 5 | (message "running hublo dry run") 6 | (hb/run-phases :bootstrap :metadata :route :content :augment :clean)) 7 | 8 | (defun hb/publish! () 9 | "Shortcut for publishing" 10 | (message "running hublo publish") 11 | (hb/run-phases :bootstrap :metadata :route :content :augment :publish :clean)) 12 | 13 | (defun hb/clean! () 14 | (message "running hublo clean") 15 | (when (file-directory-p (hb/config-output-dir *hb/config*)) 16 | (delete-directory (hb/config-output-dir *hb/config*) t nil))) 17 | 18 | (defun hb/consume-args! (args) 19 | (let (seen flags command) 20 | (setq seen nil) 21 | (dolist (arg args) 22 | (cond 23 | (seen (progn (push (list seen arg) flags) 24 | (setq seen nil))) 25 | ((string-match-p "^-" arg) (setq seen arg)) 26 | (t (push arg command)))) 27 | (cons (reverse command) (reverse flags)))) 28 | 29 | (defun hb/usage () 30 | (message "Usage: hublo [-f config-file] {noop|publish|clean|phases} ")) 31 | 32 | (defun hb/run-script-from (args) 33 | (let* (config-file 34 | (command-and-flags (hb/consume-args! args)) 35 | (command (caar command-and-flags)) 36 | (command-args (cdar command-and-flags)) 37 | (flags (cdr command-and-flags))) 38 | (-when-let (env-file (getenv "HUBLO_CONFIG")) 39 | (setq config-file env-file)) 40 | (dolist (flag flags) 41 | (let ((flag-name (car flag)) 42 | (value (cadr flag))) 43 | (cond 44 | ((equal flag-name "-f") (setq config-file value)) 45 | (t (error "unknown flag: %s" flag-name)))) 46 | nil) 47 | 48 | (when (not config-file) 49 | (message "no config file provided, using: hublo.config") 50 | (setq config-file hb/default-config-file)) 51 | 52 | (message "using config: %s" config-file) 53 | 54 | (hb/load-config config-file) 55 | (setq config-file nil) 56 | 57 | (cond 58 | ((equal "clean" command) (hb/clean!)) 59 | ((equal "publish" command) (hb/publish!)) 60 | ((equal "noop" command) (hb/noop)) 61 | ((equal "phases" command) (apply 'hb/run-phases command-args)) 62 | (t (hb/usage)))) 63 | nil) 64 | 65 | (defun hb/run-script () 66 | (let ((args command-line-args-left)) 67 | (setq command-line-args-left '()) 68 | (setq command-line-processed t) 69 | (hb/run-script-from args)) 70 | 71 | (message "hublo script ran successfuly.")) 72 | -------------------------------------------------------------------------------- /src/hublo-transform.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; 2 | 3 | (defvar *hb/handlers* nil) 4 | (defvar *hb/transforms* (ht-create)) 5 | 6 | (defun hb/register-transform (name &rest phases) 7 | (ht-set *hb/transforms* name (-partition 2 phases))) 8 | 9 | (defun hb/find-transform (name) 10 | (ht-get *hb/transforms* name)) 11 | 12 | (defun hb/register-handler (pattern transforms) 13 | (push (make-hb/handler 14 | :pattern pattern 15 | :transforms (-mapcat 'hb/find-transform transforms)) 16 | *hb/handlers*)) 17 | 18 | (defun hb/find-handler-transforms (path) 19 | 20 | (-when-let (h (-first (lambda (h) 21 | (string-match (hb/handler-pattern h) 22 | (file-name-nondirectory path))) 23 | *hb/handlers*)) 24 | (hb/handler-transforms h))) 25 | 26 | ;; 27 | ;; ============================= 28 | 29 | (defun hb/write-content (route content) 30 | (let ((path (format "%s/%s" (hb/config-output-dir *hb/config*) route))) 31 | (make-directory (file-name-directory path) t) 32 | (save-excursion 33 | (let ((cb (current-buffer))) 34 | (with-temp-buffer 35 | (insert-string content) 36 | (write-file path nil)))))) 37 | 38 | (defun hb/base-route (item) 39 | (let ((route (replace-regexp-in-string 40 | (format "^%s" (hb/config-source-dir *hb/config*)) 41 | "" 42 | (hb/item-path item)))) 43 | (setf (hb/item-route item) route) 44 | (ht-set! (hb/item-meta item) :route route))) 45 | 46 | (defun hb/base-clean (item) 47 | (setq *hb/handlers* nil) 48 | (setq *hb/transforms* (ht-create)) 49 | (setq *hb/pages* nil) 50 | (setq *hb/config* (hb/default-config))) 51 | 52 | (defun hb/base-bootstrap (item) 53 | (setf (hb/item-payload item) (hb/file->string 54 | (hb/item-path item)))) 55 | 56 | (defun hb/base-publish (item) 57 | (hb/write-content (hb/item-route item) (hb/item-payload item))) 58 | 59 | (defun hb/metadata-transform (k v) 60 | (list :metadata (lambda (item) (ht-set (hb/item-meta item) k v)))) 61 | 62 | (defun hb/truncate-title-elems (title) 63 | (let* ((title-seq (split-string title " ")) 64 | (title-len (min (length title-seq) 6))) 65 | (subseq title-seq 0 title-len))) 66 | 67 | (defun hb/slug-transform () 68 | (list 69 | :metadata 70 | (lambda (item) 71 | (let ((title (ht-get (hb/item-meta item) :title))) 72 | (when title 73 | (ht-set (hb/item-meta item) :slug 74 | (replace-regexp-in-string 75 | "[^a-z0-9-]" "" 76 | (mapconcat 77 | 'identity 78 | (remove-if-not 79 | 'identity 80 | (hb/truncate-title-elems (downcase title))) 81 | "-")))))))) 82 | 83 | (defun hb/group-transform (groupk) 84 | (list 85 | :metadata 86 | (lambda (item) 87 | (let* ((groups (ht-get (hb/config-context *hb/config*) :groups)) 88 | (group (ht-get groups groupk)) 89 | (meta (hb/item-meta item))) 90 | (ht-set groups groupk (if group (add-to-list 'group meta t) (list meta))))))) 91 | 92 | (hb/register-transform :noop) 93 | 94 | (setq hb/base-transforms '((:bootstrap hb/base-bootstrap) 95 | (:route hb/base-route) 96 | (:publish hb/base-publish) 97 | (:clean hb/base-clean))) 98 | --------------------------------------------------------------------------------