Lisp web templating engine
roxbase is my minimal, rather simple yet quite versatile Templating Engine for Common Lisp:
It isn't yet feature-complete and first and foremost intended for my own use. I have only 'tested' (used) it under SBCL.
Purpose
What do I mean by Template System? I wanted something similar to Edi Weitz' (of Hunchentoot fame) HTML-TEMPLATE, but more akin to ASP.NET or PHP than that Perl templating module which inspired HTML-TEMPLATE.
So the basic idea is to embed Lisp code in text files (think HTML pages) and process those files so that each Lisp expression be replaced with the result of evaluating it.
How it works
The most obvious use for roxbase is within a web context. The system integrates well with Hunchentoot, this website for one runs on Hunchentoot and roxbase. However, you can use within any other kind of context. roxbase only provides some basic string processing logic as described below.
Once you have set it up, you can feed roxbase templates such as this:
<%@ 'link-caption %>
Types of template expressions
There are two kinds of expressions that are being processed by roxbase:
Inline Expressions
— contains self-contained Lisp expressions:
Two plus two makes <%= (+ 2 2) %>
Mix-In Expressions
— mixes Lisp code and template contents arbitrarily:
Let's loop ten times: <? (dotimes (val 10) ?> times two : <?= (* val 2) ?> <? ) ?> That's it, thanks!
The process-template
function first processes all mix-in expressions it finds, then all inline expressions. You can have your mix-in or inline expressions generate new mix-in or inline expressions on the fly, making for neat and possibly hard-to-debug, macro-like, recursive templating: if as the result of processing, new mix-in or inline expressions are present in the intermediate output, it repeats this process until the template has been completely processed (tail-recursively, of course).
(Beware of creating endless loops when dynamically generating new mix-in or inline expressions!)
Inline Expressions (InLexes)
The process-template
function processes all inline expressions (InLexes) it finds in the template it is given. InLexes start with <%
(unless you change the value of +inlex-start-tag+
) and end with %>
(unless you change the value of +inlex-end-tag+
). In addition, <%
must be followed directly by a character that identifies the kind of InLex you're writing. The following InLexes are already built into roxbase. Needless to say, you can define your own. Only the first three InLex handlers really come with their own specific evaluation logic, the others translate into more complex InLex expressions of <%=
or <%%
.
Eval: <%=
— replaced with the result of evaluating the enclosed Lisp expression:
<%= (+ (random 20) (random 40)) %>
Def: <%%
— no output is generated. Can be used for defuns and other definitions. Your definitions are available to Eval InLexes. In contrast to Eval InLexes, the args hashtable is not available. Pass it as an argument to your functions if you need to access the template arguments.
<%% (defun rootpath (args) (gethash 'rox-rootpath args)) %> Root path of this web app: <%= (rootpath) %>
Comment: <%!
— no output is generated. Content is ignored and discarded.
<%! (needs-fix) %>
Load: <%~
— no output is generated. Load .lisp / .fasl code files. Unless an absolute path (starting with '/') is specified, the search path is considered relative to your root-path
.
<%~ "code/my-page-tools" "code/my-utils" %>
Query: <%?
— if you're using Hunchentoot, replaced with the value of the URL query string parameter with the specified name.
<%? "foo" %>
Form: <%&
— if you're using Hunchentoot, replaced with the value of the POSTed form parameter with the specified name.
<%& "foo" %>
Session: <%$
— if you're using Hunchentoot, replaced with the value of the session variable with the specified name.
<%$ "foo" %>
Header: <%|
— if you're using Hunchentoot, replaced with the value of the request header with the specified name.
<%| "Accept-Content-Type" %>
Argument: <%@
— replaced with the value associated with the specified key in the template's args
hash table.
<%@ "my-arg" %>
or<%@ 'my-other-arg %>
or whatever you chose to use for a hash key in theargs
table.
HTML Encoder: <%"
— replaced with the result of passing the enclosed Lisp expression to Hunchentoot's escape-for-html
function.
<pre><%" "Sample code: <hr />" %></pre>
JavaScript Encoder: <%'
— replaced with the result of passing the enclosed Lisp expression to the escape-for-js
function.
alert("<%' "'Bobo' said \"Hi\"." %>);
Safe Encoder: <%:
— replaced with the result of passing the enclosed Lisp expression to the escape-for-safe
function, which accepts multiple strings, concatenates them with *rox-escape-char*
(by default, the underscore) and replaces all non-alphanumeric characters in the resulting string with said escape character.
<a name="<%: "why not?" %>">Anchor</a>
Sub Template: <%#
— replaced with the result of processing the specified template with the specified arguments (those will be available to the template in its args
hash table).
<%# "foot.htm" ("msg" "©2008") ("color" "blue") %>
Getting started
Before you can use it, roxbase needs to be initialized. Call the following function:
server-startup
name
— This could be the name of your package or any other name. If you wantserver-startup
to start a Hunchentoot instance, this will be the name of your server.port
— If you do not wantserver-startup
to start a Hunchentoot instance, useNIL
; otherwise, specify the port of your Hunchentoot instance.pathname-root
— the absolute path of the directory serving as the base path for other relative path specifications used within templates or during a template processing request.script-path-handler
: designates a function that accepts three arguments and returns the path (absolute or relative topathname-root
) to the file that should be processed by roxbase.script-name
— the request path for the processing requestargs
— the Arguments hash table for the processing request (template processing arguments are described further below)subtemplate-p
—T
if this processing request was invoked recursively during a parent template processing.
&key foldername-static
— the path (relative topathname-root
) for files that should not be template-processed by roxbase.&key debug
— unlessport
isNIL
, sets the Hunchentoot variables*show-lisp-errors-p*
and*show-lisp-backtraces-p*
to the specified value (should be Boolean —NIL
orT
).&key scriptpackage
— unlessNIL
, all<%%
directives will be prepared with an(in-package)
call before processing, where the package is either the value ofscriptpackage
or ofname
, ifscriptpackage
's value isT
.
As described above, the port
argument implicitly controls whether server-startup
starts up and returns a new Hunchentoot server instance. If this is the case, roxbase sets up its own Hunchentoot request handlers that either simply return a file unprocessed if the request path fits the directory specification (relative to pathname-root
) in foldername-static
, otherwise, it invokes the specified script-path-handler
function and processes the file represented by the path (relative to pathname-root
) returned by the function call.
server-shutdown
After you're done with your template processing, you can call the server-shutdown
function to clean things up. If your previous server-startup
call initialized a new Hunchentoot server instance (that is still stored in the global variable rox-server), the service is stopped. Afterwards, the following global hash tables are cleared:
*rox-inlex-cache*
— contains cached compiled equivalents of successfully parsed inline expressions (InLexes)*rox-inlex-handlers*
— contains all known InLex handlers (InLexes and InLex handlers are described further below); pre-populated with the roxbase system InLex handlers byserver-startup
*rox-settings*
— pre-populated byserver-startup
with the following default values:'fn-html-condition-handler
(a function designator, by default#'default-html-condition-handler
, that produces output to indicate an error in place of a processed InLex and accepts three arguments:condtype
— either"EVAL"
,"COMPILE"
or"RUNTIME"
, depending on when the InLex error was encounteredcondsrc
— the complete InLex that caused the errorcondmsg
— the error message)
- and
'eval-compile-cache-handlers-p
(the string of InLex handler identifiers whose expressions should be compiled and cached in*rox-inlex-cache*
).
Invoking template processing requests
handle-request, create-args-table, process-template, load-template
If you're using roxbase with Hunchentoot, each non-static request is handled by handle-request
, which returns a string that is the result of processing the file indicated by the script-path-handler
you specified when you called server-startup
. You can call handle-request
yourself, it has the same signature as a script-path-handler
. In case you ever need to do just that, pass the following arguments:
script-name
— pass any value that yourscript-path-handler
will transform into the path (absolute or relative topathname-root
) of the file to be processedargs
— pass a hash table freshly created by thecreate-args-table
function:pathname-root
— just pass the same path name you passedserver-startup
script-path-handler
— just pass the same function you passedserver-startup
.parent-args
— just passNIL
root-args
— just passNIL
.
subtemplate-p
— just pass NIL
(this should only ever be T
if called automatically from a parent template processing request)If handle-request
finds the specified file, it loads all its raw, unprocessed string contents into memory and passes it to the process-template
and returns the result of that call; otherwise, it returns NIL
and, when using Hunchentoot, sends a 404 status code prior to returning.
Unless called manually, handle-request
is called either in response to a Hunchentoot web request or by the load-template
macro, which is a syntactically compact way to recursively load sub-templates during a parent template processing request.