diff --git a/coleslaw.asd b/coleslaw.asd
index 35063030..e62c786c 100644
--- a/coleslaw.asd
+++ b/coleslaw.asd
@@ -14,7 +14,8 @@
:cl-fad
:cl-ppcre
:closer-mop
- :cl-unicode)
+ :cl-unicode
+ :djula)
:serial t
:components ((:file "packages")
(:file "util")
diff --git a/docs/config.md b/docs/config.md
index 5814c579..38beaf51 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -22,14 +22,15 @@ It is usually recommend to start from the [example config][ex_config] and pare d
## Extras
There are also many *optional* config parameters such as:
-* `:charset` => to set HTML attributes for international characters, default: "UTF-8"
-* `:feeds` => to generate RSS and Atom feeds for certain tagged content
-* `:lang` => to set HTML attributes indicating the site language, default: "en"
-* `:license` => to override the displayed content license, the default is CC-BY-SA
-* `:page-ext` => to set the suffix of generated files, default: "html"
-* `:plugins` => to configure and enable coleslaw's [various plugins][plugin-use]
-* `:separator` => to set the separator for content metadata, default: ";;;;;"
-* `:sitenav` => to provide relevant links and ease navigation
-* `:staging-dir` => for Coleslaw to do intermediate work, default: "/tmp/coleslaw"
+* `:charset` => to set HTML attributes for international characters, default: "UTF-8"
+* `:feeds` => to generate RSS and Atom feeds for certain tagged content
+* `:lang` => to set HTML attributes indicating the site language, default: "en"
+* `:license` => to override the displayed content license, the default is CC-BY-SA
+* `:page-ext` => to set the suffix of generated files, default: "html"
+* `:plugins` => to configure and enable coleslaw's [various plugins][plugin-use]
+* `:separator` => to set the separator for content metadata, default: ";;;;;"
+* `:sitenav` => to provide relevant links and ease navigation
+* `:staging-dir` => for Coleslaw to do intermediate work, default: "/tmp/coleslaw"
+* `:template-engine` => to set the template engine coleslaw should use, default: cl-closure
[plugin-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
diff --git a/docs/themes.md b/docs/themes.md
index 178dc934..2a3192c9 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -10,10 +10,13 @@ template engine and how you can influence the resulting HTML.
## High-Level Overview
-Themes are written using [Closure Templates][clt]. Those templates are
-then compiled into functions that Lisp calls with the blog data to get
-HTML. Since the Lisp code to use theme functions is already written,
-your theme must follow a few rules.
+Themes are written using [Closure Templates][clt] or using
+[Djula][djula].
+
+## Closure templates
+Closure templates are then compiled into functions that Lisp calls with
+the blog data to get HTML. Since the Lisp code to use theme functions is
+already written, your theme must follow a few rules.
Every theme **must** be in a folder under "themes/" named after the
theme. The theme's templates must start with a namespace declaration
@@ -67,7 +70,23 @@ template hacking. There is plenty of advice on CSS styling on the web.
I'm no expert but feel free to send pull requests modifying a theme's
CSS or improving this section, perhaps by recommending a CSS resource.
-## Creating a Theme from Scratch (with code)
+## Djula templates
+
+Djula templates are somewhat more traditional than closure templates.
+They are inspired by Django templates. For more info see the Djula
+[documentation][djula_doc]. Instead of having a base template that
+gets called with data from the child template, Djula uses an extend
+mechanism. A template extends from an other template and defines blocks
+that the extended templates uses.
+
+Because you extend templates in djula you only have to define a post.html and
+index.html template in the theme folder. The variables passed to this template
+are the same as with closure. So post.html gets the post and base variables and
+index.html gets the index and base variables.
+
+For more information see the hyde-djula theme on how djula works.
+
+## Creating a Theme from Scratch using Closure (with code)
### Step 1. Create the directory.
@@ -214,5 +233,7 @@ between the pages so navigation is cumbersome but adding links is simple.
Just do: `{$object.name}`.
[clt]: https://developers.google.com/closure/templates/
+[djula]: https://github.com/mmontone/djula
+[djula_doc]: https://mmontone.github.io/djula/doc/build/html/index.html
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md
[hck]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
diff --git a/plugins/cl-closure.lisp b/plugins/cl-closure.lisp
new file mode 100644
index 00000000..2c943702
--- /dev/null
+++ b/plugins/cl-closure.lisp
@@ -0,0 +1,45 @@
+(eval-when (:compile-toplevel :load-toplevel)
+ (ql:quickload :cl-closure))
+
+(defpackage :coleslaw-cl-closure
+ (:use :cl)
+ (:import-from :closure-template #:compile-template)
+ (:import-from :local-time #:format-rfc1123-timestring)
+ (:import-from :coleslaw #:find-theme
+ #:app-path
+ #:render
+ #:find-injections
+ #:*config*
+ #:theme
+ #:do-files
+ #:render
+ #:theme-package)
+ (:export #:enable))
+
+(in-package :coleslaw-cl-closure)
+
+(defclass cl-closure () ())
+
+(defmethod coleslaw:compile-theme ((cl-closure cl-closure) theme)
+ "Compile the templates used by cl-closure"
+ (do-files (file (app-path "themes/auxiliary")
+ "cl-closure")
+ (compile-template :common-lisp-backend file))
+ (do-files (file (find-theme theme) "tmpl")
+ (compile-template :common-lisp-backend file)))
+
+(defmethod coleslaw:render-page ((template cl-closure) content &optional theme-fn &rest render-args)
+ (apply (or theme-fn (get-theme-fn 'theme 'base))
+ :config *config*
+ :content content
+ :pubdate (format-rfc1123-timestring nil (local-time:now))
+ :injections (find-injections content)
+ :raw (apply #'render content render-args)
+ render-args))
+
+(defmethod coleslaw:get-theme-fn ((template cl-closure) name &optional (package (theme *config*)))
+ (let ((closure-func (find-symbol (princ-to-string name) (theme-package package))))
+ (lambda (&rest template-args)
+ (funcall closure-func template-args))))
+
+(defun enable ())
diff --git a/plugins/djula.lisp b/plugins/djula.lisp
new file mode 100644
index 00000000..b75d1233
--- /dev/null
+++ b/plugins/djula.lisp
@@ -0,0 +1,57 @@
+(eval-when (:compile-toplevel :load-toplevel)
+ (ql:quickload :djula))
+
+(defpackage :coleslaw-djula
+ (:use :cl :cl-ppcre)
+ (:import-from :djula #:compile-template*
+ #:render-template*)
+ (:import-from :local-time #:format-rfc1123-timestring)
+ (:import-from :coleslaw #:get-theme-fn
+ #:render-page
+ #:find-theme
+ #:app-path
+ #:render
+ #:find-injections
+ #:*config*
+ #:theme
+ #:do-files)
+ (:import-from :cl-ppcre #:create-scanner
+ #:regex-replace)
+ (:export #:enable))
+
+(in-package :coleslaw-djula)
+
+(defclass djula ()
+ ((templates :initform (make-hash-table :test #'equalp) :accessor templates)))
+
+(defparameter +remove-extension+ (create-scanner "(.*)\\..*"))
+
+(defun compile-djula (djula file &optional (filename (regex-replace +remove-extension+ (file-namestring file) "\\1")))
+ (setf (gethash filename
+ (templates djula)) (compile-template* file)))
+
+(defmethod coleslaw:compile-theme ((djula djula) theme)
+ (do-files (file (app-path "themes/auxiliary")
+ "djula")
+ (compile-djula djula file))
+ (do-files (file (find-theme theme) "html")
+ (compile-djula djula file)))
+
+(defmethod coleslaw:render-page ((djula djula) content &optional theme-fn &rest render-args)
+ (apply (or theme-fn #'render)
+ content
+ :config *config*
+ :content content
+ :pubdate (format-rfc1123-timestring nil (local-time:now))
+ :injections (find-injections content)
+ render-args))
+
+(defmethod coleslaw:get-theme-fn ((djula djula) name &optional package)
+ (declare (ignore package))
+ (break)
+ (let ((theme (gethash (string name) (templates djula))))
+ (lambda (&rest template-args)
+ (with-output-to-string (stream)
+ (apply #'render-template* theme stream template-args)))))
+
+(defun enable ())
diff --git a/plugins/sitemap.lisp b/plugins/sitemap.lisp
index ea4a0ea8..3605a204 100644
--- a/plugins/sitemap.lisp
+++ b/plugins/sitemap.lisp
@@ -5,8 +5,9 @@
#:page-url
#:find-all
#:publish
- #:theme-fn
+ #:get-theme-fn
#:add-document
+ #:template-engine
#:write-document)
(:import-from :alexandria #:hash-table-values)
(:export #:enable))
@@ -25,6 +26,6 @@
(let* ((base-urls '("" "sitemap.xml"))
(urls (mapcar #'page-url (hash-table-values coleslaw::*site*)))
(sitemap (make-instance 'sitemap :urls (append base-urls urls))))
- (write-document sitemap (theme-fn 'sitemap "sitemap"))))
+ (write-document sitemap (get-theme-fn (template-engine *config*) 'sitemap "sitemap"))))
(defun enable ())
diff --git a/plugins/static-pages.lisp b/plugins/static-pages.lisp
index f83849e8..0a78947b 100644
--- a/plugins/static-pages.lisp
+++ b/plugins/static-pages.lisp
@@ -6,9 +6,11 @@
#:find-all
#:render
#:publish
- #:theme-fn
+ #:get-theme-fn
#:render-text
- #:write-document))
+ #:write-document
+ #:template-engine)
+ (:import-from :djula #:render-template*))
(in-package :coleslaw-static-pages)
@@ -24,11 +26,12 @@
format (alexandria:make-keyword (string-upcase format))
coleslaw::text (render-text coleslaw::text format))))
-(defmethod render ((object page) &key next prev)
+(defmethod render ((object page) &rest rest &key next prev)
;; For the time being, we'll re-use the normal post theme.
(declare (ignore next prev))
- (funcall (theme-fn 'post) (list :config *config*
- :post object)))
+ (apply (get-theme-fn (template-engine *config*) 'post) :config *config*
+ :post object
+ rest))
(defmethod publish ((doc-type (eql (find-class 'page))))
(dolist (page (find-all 'page))
diff --git a/src/coleslaw.lisp b/src/coleslaw.lisp
index c5b9bad2..7cc5de46 100644
--- a/src/coleslaw.lisp
+++ b/src/coleslaw.lisp
@@ -3,16 +3,27 @@
(defvar *last-revision* nil
"The git revision prior to the last push. For use with GET-UPDATED-FILES.")
+(defvar *templating-engine* nil
+ "The templating engine to use. This will be set during the main routine.
+Possible options at this time are djula and cl-closure.")
+
+(defvar *djula-post-template* nil
+ "The template to use for rendering a post using a djula template.")
+
+(defvar *djula-index-template* nil
+ "The template to use for rendering the index using a djula template.")
+
(defun main (repo-dir &optional oldrev)
"Load the user's config file, then compile and deploy the blog stored
in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
(load-config repo-dir)
- (setf *last-revision* oldrev)
- (load-content)
- (compile-theme (theme *config*))
- (let ((dir (staging-dir *config*)))
- (compile-blog dir)
- (deploy dir)))
+ (let ((*templating-engine* (template-engine *config*)))
+ (setf *last-revision* oldrev)
+ (load-content)
+ (compile-theme (template-engine *config*) (theme *config*))
+ (let ((dir (staging-dir *config*)))
+ (compile-blog dir)
+ (deploy dir))))
(defun load-content ()
"Load all content stored in the blog's repo."
@@ -55,17 +66,15 @@ in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
(let ((current-working-directory (cl-fad:pathname-directory-pathname path)))
(unless *config*
(load-config (namestring current-working-directory))
- (compile-theme (theme *config*)))
+ (compile-theme (template-engine *config*) (theme *config*)))
(let* ((file (rel-path (repo-dir *config*) path))
(content (construct content-type (read-content file))))
- (write-file "tmp.html" (render-page content)))))
+ (write-file "tmp.html" (render-page (template-engine *config*) content)))))
-(defun render-page (content &optional theme-fn &rest render-args)
- "Render the given CONTENT to HTML using THEME-FN if supplied.
-Additional args to render CONTENT can be passed via RENDER-ARGS."
- (funcall (or theme-fn (theme-fn 'base))
- (list :config *config*
- :content content
- :raw (apply 'render content render-args)
- :pubdate (format-rfc1123-timestring nil (local-time:now))
- :injections (find-injections content))))
+(defgeneric render-page (template-engine content &optional theme-fn &rest render-args)
+ (:documentation "Render a page using the given theme. TEMPLATE-ENGINE should be
+an instance of a template-engine object specified in one of the plugins. This
+object is stored in the config object. CONTENT should be the object to render as
+main part of the page, THEME-FN should be the function to render the page with
+and RENDER-ARGS should be additional arguments that should be passed to the
+render function(s)."))
diff --git a/src/config.lisp b/src/config.lisp
index 15b5b1f1..b0c35080 100644
--- a/src/config.lisp
+++ b/src/config.lisp
@@ -1,7 +1,8 @@
(in-package :coleslaw)
(defclass blog ()
- ((author :initarg :author :reader author)
+ ((master :initarg :master :reader master)
+ (author :initarg :author :reader author)
(charset :initarg :charset :reader charset)
(deploy-dir :initarg :deploy-dir :reader deploy-dir)
(domain :initarg :domain :reader domain)
@@ -16,8 +17,10 @@
(sitenav :initarg :sitenav :reader sitenav)
(staging-dir :initarg :staging-dir :reader staging-dir)
(theme :initarg :theme :reader theme)
+ (template-engine :initarg :template-engine :accessor template-engine)
(title :initarg :title :reader title))
(:default-initargs
+ :master nil
:feeds nil
:license nil
:plugins nil
@@ -26,7 +29,23 @@
:lang "en"
:page-ext "html"
:separator ";;;;;"
- :staging-dir "/tmp/coleslaw"))
+ :staging-dir "/tmp/coleslaw"
+ :template-engine 'cl-closure))
+
+(defmethod initialize-instance :after ((config blog) &rest rest)
+ "Initialize config object by creating the correct template-engine class and
+setting it to template-engine"
+ (declare (ignore rest))
+ (when (master config) (setf *config* config))
+ (load-plugins (cons `(,(format nil
+ "~:@(~A~)"
+ (string (template-engine config))))
+ (plugins config)))
+ (let ((theme-class (intern (string-upcase (template-engine config))
+ (format nil
+ "~:@(coleslaw-~A~)"
+ (string (template-engine config))))))
+ (setf (template-engine config) (make-instance theme-class))))
(defun dir-slot-reader (config name)
"Take CONFIG and NAME, and return a directory pathname for the matching SLOT."
@@ -84,6 +103,4 @@ doesn't exist, use the .coleslawrc in the home directory."
preferred over the home directory if provided."
(with-open-file (in (discover-config-path repo-dir) :external-format :utf-8)
(let ((config-form (read in)))
- (setf *config* (construct 'blog config-form)
- (repo-dir *config*) repo-dir)))
- (load-plugins (plugins *config*)))
+ (construct 'blog (append `(:master t :repo ,repo-dir) config-form)))))
diff --git a/src/documents.lisp b/src/documents.lisp
index b90d299f..7950fa80 100644
--- a/src/documents.lisp
+++ b/src/documents.lisp
@@ -63,8 +63,8 @@ is provided, it overrides the route used."
"Write the given DOCUMENT to disk as HTML. If THEME-FN is present,
use it as the template passing any RENDER-ARGS."
(let ((html (if (or theme-fn render-args)
- (apply #'render-page document theme-fn render-args)
- (render-page document nil)))
+ (apply #'render-page (template-engine *config*) document theme-fn render-args)
+ (render-page (template-engine *config*) document nil)))
(url (namestring (page-url document))))
(write-file (rel-path (staging-dir *config*) url) html)))
diff --git a/src/feeds.lisp b/src/feeds.lisp
index 4bebcb0e..56e9a5ff 100644
--- a/src/feeds.lisp
+++ b/src/feeds.lisp
@@ -16,7 +16,10 @@
(defmethod publish ((doc-type (eql (find-class 'feed))))
(dolist (feed (find-all 'feed))
- (write-document feed (theme-fn (feed-format feed) "feeds"))))
+ (write-document feed
+ (get-theme-fn (template-engine *config*)
+ (feed-format feed)
+ "feeds"))))
;;; Tag Feeds
@@ -34,4 +37,4 @@
(defmethod publish ((doc-type (eql (find-class 'tag-feed))))
(dolist (feed (find-all 'tag-feed))
- (write-document feed (theme-fn (feed-format feed) "feeds"))))
+ (write-document feed (get-theme-fn (template-engine *config*) (feed-format feed) "feeds"))))
diff --git a/src/indexes.lisp b/src/indexes.lisp
index 6cbf0562..e6db8f9c 100644
--- a/src/indexes.lisp
+++ b/src/indexes.lisp
@@ -15,13 +15,14 @@
(with-slots (url) object
(setf url (compute-url object slug))))
-(defmethod render ((object index) &key prev next)
- (funcall (theme-fn 'index) (list :tags (find-all 'tag-index)
- :months (find-all 'month-index)
- :config *config*
- :index object
- :prev prev
- :next next)))
+(defmethod render ((object index) &rest rest)
+ (apply (get-theme-fn (template-engine *config*) 'index)
+ :tags (find-all 'tag-index)
+ :months (find-all 'month-index)
+ :config *config*
+ :index object
+ (append (find-all 'month-index)
+ rest)))
;;; Index by Tag
diff --git a/src/packages.lisp b/src/packages.lisp
index d49035f0..ce9b6d68 100644
--- a/src/packages.lisp
+++ b/src/packages.lisp
@@ -10,6 +10,8 @@
(:import-from :local-time #:format-rfc1123-timestring)
(:import-from :uiop #:getcwd
#:ensure-directory-pathname)
+ (:import-from :djula #:render-template*
+ #:compile-template*)
(:export #:main
#:preview
#:*config*
@@ -21,6 +23,7 @@
#:repo-dir
#:staging-dir
#:title
+ #:template-engine
;; Core Classes
#:content
#:post
@@ -30,7 +33,9 @@
#:author-of
#:find-content-by-path
;; Theming + Plugin API
- #:theme-fn
+ #:get-theme-fn
+ #:compile-theme
+ #:render-page
#:plugin-conf-error
#:render-text
#:add-injection
diff --git a/src/posts.lisp b/src/posts.lisp
index d8fe29d5..44ffb24a 100644
--- a/src/posts.lisp
+++ b/src/posts.lisp
@@ -13,11 +13,11 @@
text (render-text text format)
author (or author (author *config*)))))
-(defmethod render ((object post) &key prev next)
- (funcall (theme-fn 'post) (list :config *config*
- :post object
- :prev prev
- :next next)))
+(defmethod render ((object post) &rest rest)
+ (apply (get-theme-fn (template-engine *config*) 'post)
+ :config *config*
+ :post object
+ rest))
(defmethod publish ((doc-type (eql (find-class 'post))))
(loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
diff --git a/src/themes.lisp b/src/themes.lisp
index 267178eb..2195f5b7 100644
--- a/src/themes.lisp
+++ b/src/themes.lisp
@@ -3,6 +3,8 @@
(defparameter *injections* '()
"A list that stores pairs of (string . predicate) to inject in the page.")
+(defclass template-engine () ())
+
(defun add-injection (injection location)
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a
function that takes a DOCUMENT and returns NIL or a STRING for template insertion."
@@ -26,9 +28,14 @@ function that takes a DOCUMENT and returns NIL or a STRING for template insertio
(or (find-package (format nil "~:@(coleslaw.theme.~A~)" name))
(error 'theme-does-not-exist :theme name)))
-(defun theme-fn (name &optional (package (theme *config*)))
- "Find the symbol NAME inside PACKAGE which defaults to the theme package."
- (find-symbol (princ-to-string name) (theme-package package)))
+(defun get-djula-theme (name)
+ (symbol-value (intern (string-upcase (format nil "*djula-~A-template*" (string name))))))
+
+(defgeneric get-theme-fn (template name &optional package)
+ (:documentation "Get the function used to render the template for NAME.
+The engine used to do this must be specified in TEMPLATE and should be a class
+instance of an engine. PACKAGE can be used to specify where to search for
+methods to render a template."))
(defun find-theme (theme)
"Find the theme prefering themes in the local repo."
@@ -37,9 +44,8 @@ function that takes a DOCUMENT and returns NIL or a STRING for template insertio
local-theme
(app-path "themes/~a/" theme))))
-(defun compile-theme (theme)
- "Locate and compile the templates for the given THEME."
- (do-files (file (find-theme theme) "tmpl")
- (compile-template :common-lisp-backend file))
- (do-files (file (app-path "themes/") "tmpl")
- (compile-template :common-lisp-backend file)))
+(defgeneric compile-theme (template-engine theme)
+ (:documentation "Locate and compile the templates for the given THEME and
+ compile them. The compiling is done appropriately for each template engine
+ that is specified in TEMPLATE-ENGINE in the form of a template engine class
+ instance."))
diff --git a/themes/atom.tmpl b/themes/auxiliary/atom.cl-closure
similarity index 100%
rename from themes/atom.tmpl
rename to themes/auxiliary/atom.cl-closure
diff --git a/themes/auxiliary/atom.djula b/themes/auxiliary/atom.djula
new file mode 100644
index 00000000..ed5fe377
--- /dev/null
+++ b/themes/auxiliary/atom.djula
@@ -0,0 +1,26 @@
+
+
View content from + {% for month in months %} + {{ month.name }} + {% if not forloop.last %}, {% endif %} + {% endfor %} +