newLISP Template System
The template module was inspired largely by the Django template system. I firmly believe that separating complex logic from the template itself leads to both cleaner code and, much more importantly, a simpler system for non-programmers. A content producer should not be required to know lisp to write a simple template.
From the :
If you have a background in programming, or if you’re used to languages like PHP which mix programming code directly into HTML, you’ll want to bear in mind that the Django template system is not simply Python embedded into HTML. This is by design: the template system is meant to express presentation, not program logic.
If you disagree, you are welcome to write your own template module ;)
Templates are rendered using data in a newLISP context. This is to preserve the encapsulation of the view data and to prevent tags from accidentally accessing external data. A custom tag or filter must explicitly access data from another context in order to display it in the rendered template.
The primary module function is Template:render
, which is called with a string (the template text) and a context (the hash containing the view data). This is tokenized and lexed by the module and rendered to a new string.
Tags may be defined as functions in the TAGS
context. A tag is a function which accepts first a view context, then a list of child nodes (parsed tokens from the template text), then a list of the arguments passed to the tag (in string format). The parsed tokens will each have a render
method, but more commonly Template:render
will be used on the rest of the list.
Complex tags, such as for
(below), iterate over a portion of the template (the child nodes passed to the function), modifying the context on each iteration and adding the rendered result to the string. Tags must be “ended”, so a for
tag must end in and endfor
tag.
(define (TAGS:for ctx rst) ;; (args) contains the words inside the tag tokens: ;; if the tag was called {% for i in x %}, then ;; (args) ; => '("i" "in" "x") (let ((elt (args 0 0)) (lst (args 0 2)) (buff "")) ;; get the data to iterate over (or use an empty list) (set 'lst (if (context? ctx lst) (context ctx lst) '())) (letex (e (sym elt)) (dolist (e lst) ;; update the context with the current list item (context ctx elt e) ;; add the current loop iteration (context ctx "counter" $idx) ;; render the child-nodes using the modified context (write-buffer buff (Template:render rst ctx))) ;; we are left with the fully rendered string buff)))
Simple tags will be passed an empty list in place of child nodes.
Currently, there is no checking to verify that a tag is used properly, nor is there any safety built in to prevent incorrectly used tags being rendered. At the moment, that is the responsibility of the developer.
Tags are used in the format:
{% some-tag args passed to tag %}
text to be parsed and passed to
the tag function as child nodes.
{% endsome-tag %}
Variables are tokens that appear between double curly braces, such as {{ some-var }}
. If they appear in the view context, they will be rendered as their string value. Otherwise, they will evaluate to an empty string (so that nil is a valid value for a variable).
Variables may be further modified in-place using filters. Filter functions are defined in the context FILTERS
and accept up to two arguments, the string value of the variable and a second string argument to the filter, in the form:
{{ variable:filter:"some argument" }}
Filters must evaluate to a string.
Comments in a template appear between curly braces and hash marks:
{# this is a comment.
it may be multi-line. #}
White space is preserved in the template, except for that which appears inside of comments. See tags.lsp, filters.lsp, and tokens.lsp for more detail on the implementation of each.
You can download the files here.
edit: posted a quick bug fix for situations where there are no tags, comments, or variables in a template.