Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it possible to use djula as template engine #107

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion coleslaw.asd
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
:cl-fad
:cl-ppcre
:closer-mop
:cl-unicode)
:cl-unicode
:djula)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If at all possible, I would prefer we load djula in an eval-when in the plugin as we do with other plugins that have additional library dependencies. If it is necessary to also initialize/compile templates at that time, the data necessary to do so can be passed into the enable method in the plugin and they can be compiled then.

:serial t
:components ((:file "packages")
(:file "util")
Expand Down
19 changes: 10 additions & 9 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 26 additions & 5 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -214,5 +233,7 @@ between the pages so navigation is cumbersome but adding links is simple.
Just do: `<a href="{$config.domain}/{$object.url}">{$object.name}</a>`.

[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
10 changes: 6 additions & 4 deletions plugins/static-pages.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
#:publish
#:theme-fn
#:render-text
#:write-document))
#:write-document)
(:import-from :djula #:render-template*))

(in-package :coleslaw-static-pages)

Expand All @@ -24,11 +25,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 (theme-fn 'post) :config *config*
:post object
rest))

(defmethod publish ((doc-type (eql (find-class 'page))))
(dolist (page (find-all 'page))
Expand Down
42 changes: 30 additions & 12 deletions src/coleslaw.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer the templating engine (and any related vars like *djula-post-template* and *djula-index-template*) be kept in the config object if at all possible. Sure, it's one big nasty God object, but it's in keeping with the conventions elsewhere in the project.

"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 (theme *config*))
(let ((dir (staging-dir *config*)))
(compile-blog dir)
(deploy dir))))

(defun load-content ()
"Load all content stored in the blog's repo."
Expand Down Expand Up @@ -63,9 +74,16 @@ in REPO-DIR. Optionally, OLDREV is the revision prior to the last push."
(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))))
(let ((index-args (list :config *config*
:content content
:pubdate (format-rfc1123-timestring nil
(local-time:now))
:injections (find-injections content))))
(case *templating-engine*
(cl-closure
(apply (or theme-fn
(theme-fn 'base))
(append (list :raw (apply 'render content render-args))
index-args)))
(djula (apply 'render content (append index-args render-args)))
(otherwise (error "Unkown templating engine found")))))
9 changes: 8 additions & 1 deletion src/config.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
(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
:feeds nil
Expand All @@ -26,7 +27,13 @@
: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)
"Make sure that TEMPLATE-ENGINE is interned in the coleslaw package"
(declare (ignore rest))
(setf (template-engine config) (intern (string (template-engine config)) :coleslaw)))

(defun dir-slot-reader (config name)
"Take CONFIG and NAME, and return a directory pathname for the matching SLOT."
Expand Down
15 changes: 8 additions & 7 deletions src/indexes.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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 (theme-fn 'index)
:tags (find-all 'tag-index)
:months (find-all 'month-index)
:config *config*
:index object
(append (find-all 'month-index)
rest)))

;;; Index by Tag

Expand Down
2 changes: 2 additions & 0 deletions src/packages.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
10 changes: 5 additions & 5 deletions src/posts.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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 (theme-fn '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)))
Expand Down
54 changes: 47 additions & 7 deletions src/themes.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,27 @@ 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))))))

(defun theme-fn (name &optional
(package (theme *config*))
(templating-engine *templating-engine*))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't template-engine be more in line with the rest of the wording instead?

"This function returns a function that will accept an arbitrary number of
arguments and will return a html string. If the given templating-engine is not
support this will signal a SIMPLE-ERROR"
(case templating-engine
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this extensible, we should dispatch on hash-table instead, something like

(if-let (theme-fn (gethash templating-engine *available-engines*))
  theme-fn
  (error "Theme-fn not found. Unknown template-engine ~A" templating-engine))

(cl-closure (let ((func (find-symbol (princ-to-string name)
(theme-package package))))
(lambda (&rest rest)
(funcall func rest))))
(djula (lambda (&rest rest)
(with-output-to-string (stream)
(apply #'render-template*
(get-djula-theme name)
stream
rest))))
(otherwise (error "Unkown templating engine found"))))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ease of debuggability, we should include the value that we failed to match, templating-engine in this case.


(defun find-theme (theme)
"Find the theme prefering themes in the local repo."
Expand All @@ -38,8 +56,30 @@ function that takes a DOCUMENT and returns NIL or a STRING for template insertio
(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")
"Locate and compile the templates for the given THEME and compile them. In the
case of cl-closure this will define extra functions and in the case of Djula this
will set *DJULA-POST-TEMPLATE* and *DJULA-INDEX-TEMPATE*. This will always define
the cl-closure functions for atom, sitemap and rss."
;; Define functions for atom, sitemap and rss.
(do-files (file (app-path "themes/")
"tmpl")
(compile-template :common-lisp-backend file))
(do-files (file (app-path "themes/") "tmpl")
(compile-template :common-lisp-backend file)))
;; Now find the correct templates
(let ((theme-base (find-theme theme)))
(format t
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This format expression seems to be left over from debugging!

"~A, ~A, ~A ~A~%"
*templating-engine*
(equal *templating-engine* (intern "DJULA"))
(eql *templating-engine*
(intern "DJULA"))
(equal *templating-engine* 'DJULA))
(case *templating-engine*
(cl-closure (do-files (file theme-base "tmpl")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above comment, using hash-table would allow extension

(compile-template :common-lisp-backend file)))
(djula (dolist (page '("post" "index"))
(set (intern (string-upcase (format nil "*djula-~A-template*" page)))
(compile-template* (merge-pathnames theme-base
(make-pathname :directory "/"
:name page
:type "html"))))))
(otherwise (error "Unkown templating engine found at compiling")))))
53 changes: 53 additions & 0 deletions themes/hyde-djula/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!doctype html>
<html lang="{{ config.lang }}">
<head>
<title>{{ config.title }}</title>
<meta http-equiv="content-type" content="text/html; charset={{ config.charset }}" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//fonts.googleapis.com/css?family=Vollkorn:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css" />
<link href="//fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet" type="text/css" />
<link href= "{{ config.domain }}/css/style.css" rel="stylesheet" type="text/css" />
<link rel="alternate" href="{{ config.domain }}/rss.xml" type="application/rss+xml" />
{% if injections.head %}
{% for injection in injections.head %}
{{ injection | safe }}
{% endfor %}
{% endif %}
</head>
<body>
<div class="navigation">
<a href="{{ config.domain }}">{{ config.title }}</a> |
{% for link in config.sitenav %}
{% if link.relative %}
<a href="{{ config.domain }}/{{ link.url }}">{{ link.name }}</a>
{% else %}
<a href="{{ link.url }}">{{ link.name }}</a>
{% endif %}
{% if not forloop.last %} | {% endif %}
{% endfor %}
</div>
<div id="content">
{% block content %}{% endblock %}
</div>
{% if injections.body %}
{% for injection in injections.body %}
{{ injection | safe }}
{% endfor %}
{% endif %}
<div class="fineprint">
<hr>
Unless otherwise credited all material
{% if config.license %}
{{ config.license }}
{% else %}
<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/deed.en_US">
<img alt="Creative Commons License" style="border-width:0" src="{{ config.domain }}/css/cc-by-sa.png" />
</a>
{% endif %}
by {{ config.author }}
<a id="coleslaw-logo" href="https://github.com/redline6561/coleslaw">
<img src="{{ config.domain }}/css/logo_small.jpg" alt="Coleslaw logo" />
</a>
</div>
</body>
</html>
Binary file added themes/hyde-djula/css/cc-by-sa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added themes/hyde-djula/css/logo_large.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added themes/hyde-djula/css/logo_medium.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added themes/hyde-djula/css/logo_small.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading