As you might know already Pico has a GUI system similar to for instance Haml. That’s all very well and fine, the problem is that as opposed to Haml a typical Pico GUI script contains a lot of logic as well, I shudder to think having designers poking around in there to output the HTML just like they want it. And we are not quite yet at the point where we are doing completely CSS controlled designs, it’s a goal though.
Another option would be to create some kind of Pico only solution that would separate the GUI code from the database logic a la Haml but I opted for creating a templating system that is slowly getting finished and I have to document it before I forget what the code is doing. Note that it can’t do caching yet, that’s on the todo list.
This is pretty advanced stuff, if you need an introduction see the beginner series.
I’ve used the following to test with so far:
(load "lib/tpl.l") (class +Person) (dm T (Age Name) (=: age Age) (=: name Name)) (class +Company) (dm T (Name Staff) (=: staff Staff) (=: name Name)) (setq Tpl (new '(+Tpl) '/opt/picolisp/projects/tpl-test/)) (setq Comps (list (new '(+Company) 'Scania (list (new '(+Person) 65 'john) (new '(+Person) 38 'fred) (new '(+Person) 21 'Scania))) (new '(+Company) 'Ericsson (list (new '(+Person) 41 'annie) (new '(+Person) 42 'sam))))) (put Tpl 'cssDir '/opt/picolisp/projects/tpl-test/css/) (assign> Tpl 'companies Comps) (assign> Tpl 'title 'Tpl-Test) (assign> Tpl 'main 'content) (assign> Tpl 'workwith 'person) (parse> Tpl 'index) (compRun> Tpl)
And the following templates (index.tpl and content.tpl):
<html> <head> <title></title> <link rel="stylesheet" href="<% var cssDir %>styles.css" type="text/css" > </head> <body> <% include get main %> </body> </html>
<table class="tbl"> <% loop companies company %> <tr> <td> <% get company name %>: <table cellpadding="3" cellSpacing="3"> <% loop company staff get workwith %> <tr> <td>Name: <% upc get person name %></td> <td>Age: <% get person age %></td> <td> <% if john eq get person name %> Hi John! <% elif sam eq get person name %> Hi Sam! <% elif get person name eq get company name %> Funny, you have the same name as the company. <% elif %> You're not Sam or John! You are <% get person name %> <% /if %> </td> </tr> <% elloop %> No staff <% /loop %> </table> </td> </tr> <% elloop %> No companies <% /loop %> </table>
Let’s walk through the code in order of execution:
(class +Tpl) (dm T (Basedir) (=: baseDir Basedir) (=: tplDir (pack Basedir 'templates/)) (=: cssDir (pack Basedir 'css/)) (=: tplVars (new)) (=: pExe '(include>)) (=: rExe '(gui>)) (=: html NIL) (=: alternatives '((elif> elif>) (loop> elloop>) (if> elif>))) (=: enders '((elif> /if) (elloop> /loop) (loop> /loop) (if> /if))) (=: conditionals '(loop> if>)))
The constructor. We have a variable keeping track of the base directory, the templates directory and the CSS directory. TplVars will contain all the data that we want to expose in the template. PExe contains all the methods we need to execute on the first pass, conversely rExe will contain all the methods we need to execute when we finally run the template. Html will contain the final result. Alternatives contains name pairs where the cdr is the alternative function to run if the primary (the car) for some reason should not be run. Enders contain the flags that signal an end to the control structures.
First we start with reading the template file into a simple list:
(dm parse> (Tpl) (let File (pack (: tplDir) "/" Tpl ".tpl") (use (@A @X @Z) (let L (in File (till)) (=: pTpl (make (while (match '(@A "<" "%" @X "%" ">" @Z) L) (link> This (pack (clip @A))) (pExe> This (list> This (str (pack @X)))) (setq L @Z)) (link> This (pack (clip @Z))))))))) (dm link> (El) (when El (link El)))
We begin by creating a list containing each character in its elements that we do the matching on, what’s left on each match will be put in @Z and subsequently parsed. The code to be executed is further handled by the list> and pExe> methods:
(dm list> (Lst) (if (ender?> This (car Lst)) (list (car Lst)) (any (glue " " (make (for El Lst (link (if (method> This El) (pack "(" El ">") El))) (link "]" )))))) (dm pExe> (Lst) (if (isIn> This Lst (: pExe)) (chain> This (eval> This Lst)) (link> This Lst)))
If we are not dealing with an ender we will simply create something that we can execute from the template code, get will become get> for instance, only if it is a method of the class. Another example: <% loop company staff get workwith %> will become (loop> company staff (get> workwith)).
In any case pExe> will get the result and execute the code if it is in the pExe list (include> is the only one for the time being):
(dm include> (Str) (parse> This Str))
We parse again, basically recursion at work. In our case ‘main contains ‘content which will resolve to content.tpl which we parse and the result is chained. Eval> is the main engine:
(dm eval> (Lst) (if (lst? Lst) (let (Func (pop 'Lst) El) (if (or (method Func This) (fun? Func)) (apply Func (make (link This) (loop (NIL Lst NIL) (setq El (pop 'Lst)) (T (and (lst? El) (method (car El) This)) (link (eval> This El))) (link El)))) Func)) Lst))
Do we have a list? If so get the first element, if not do nothing, simply return as is. If the first element is a method of +Tpl or a global function we proceed, otherwise we return the element as is. It’s time to apply the function with the help of a list that is generated in a recursive manner. That’s why (loop> company staff (get> workwith)) will first become (loop> company staff person) before it is used to apply with. The result of the application is returned.
As you can see above in the test code we use the compRun> method (this one might be chucked in the final version when caching is implemented, for now it’s just an alias for compile and run):
(dm compRun> () (compile> This) (run> This (: cTpl))) (dm compile> () (=: cTpl (buildTree> This (: pTpl))))
Since we parse before we compile the cTpl variable will contain the result of the parsing and we pass it along to buildTree>:
(dm buildTree> (Code) (=: c Code) (make (while (: c) (let Node (pop (:: c)) (if (cond?> This Node) (branch> This Node) (link> This Node)))))) (dm cond?> (Lst) (when (lst? Lst) (isIn> This Lst (: conditionals))))
We will store the code to build with, in a temporary variable called c. While c still has elements we pop/shift them off and check if they are in the conditionals list (if> loop>). A more proper name should maybe be fork or branch since they are responsible for doing just that with the code, however since the loop will only be run if it has something to work with it could also be viewed as a kind of conditional. If it is not a conditional we simply link> it, if it is though we call branch>:
(dm branch> (Node) (let Func (car Node) (link> This (make (link Node) (catch NIL (while (: c) (let C (pop (:: c)) (if (lst? C) (cond ((cond?> This C) (branch> This C)) ((inConf> This 'alternatives Func (car C)) (branch> This C) (throw)) ((inConf> This 'enders Func (car C)) (throw)) (T (link C))) (link> This C)))))))))
We basically build a list that we finally link>. Func will contain loop> or if>, Node needs to be linked first because it was not linked earlier in buildTree>. Next we start popping c (as C) again to build the executable list.
Is the current element a list? If not we simply link it (nothing to do, the Node and the element (C) straight after it in c are simply linked without further treatment).
If it is a list we check if it is a new branch and pass it recursively straight away. If it is the right alternative of elif> or elloop> we also branch but we also throw NIL to stop the loop and therefore work on the current branch. If it is an ender we simply stop and do nothing. Finally if no other earlier condition applied we simply link the element and continue the loop. This recursive procedure will build an execution list with the alternative code to execute - if the condition doesn’t apply - at the end of the list.
It is time to run the result we’ve got so far:
(dm run> (C) (when C (if (lst? C) (for N C (ifn (run?> This N) (html> This N) (if (fork?> This N) (apply> This N) (html> This (eval> This N))))) (html> This C)))) (dm html> (Str) (if (: html) (conc (: html) (list Str)) (=: html (list Str)))) (dm fork?> (El) (if (lst? El) (fork?> This (car El)) (find> This El (mapcar car (: enders))))) (dm apply> (N) (apply (caar N) (list This (getRun> This (cdr N)) (getAlt> This (cdr N)) (getArgs> This N))))
We no longer need to keep track of the code by popping off one element at a time, reason being that we are now executing. We will simply step through the code and end up somewhere in some branch depending on which conditionals evaluate to true or false, for will suffice for this. Do we have a list? If not we simply add it to the html variable.
We continue by checking if we can run each element (currently all methods except gui> are to be executed at runtime). If not we simply add it to html. Is the element a fork (one of either elif>, elloop>, loop> or if>)? If it is we simply apply> it by first getting the code to run in case it is to be executed, the code to run if not and the arguments to evaluate to check which code to run (the run code or the alternative code). If it is not a fork we perform the recursive apply we have in eval>.
(dm getRun> (C) (if (< 1 (length C)) (if (fork?> This (last C)) (head -1 C) C) (car C))) (dm getAlt> (C) (when (fork?> This (last C)) (last C))) (dm getArgs> (C) (when (and (lst? (car C)) (< 1 (length (car C)))) (cdar C)))
So the alternative code will reside in the last element, the run code in the car (or the whole list except the first element (head -1)) and the arguments to execute in the cdar.
Let’s check out loop>:
(dm loop> (RunC AltC . @) (let Args (evalList> This (rot (copy (car (rest))))) (let (As (pop 'Args) LoopMe (get> This (pop 'Args) Args)) (if (okLst?> This LoopMe) (for Item LoopMe (assign> This As Item) (run> This RunC)) (run> This AltC))))) (dm evalList> (Lst) (mapcar '((El)(eval> This (unest> This El))) Lst)) (dm unest> (Lst) (if (and (lst? Lst) (lst? (car Lst))) (unest> This (car Lst)) Lst))
As you can see by the @ symbol, any arguments except the run-code and alt-code are optional. The last element in the arguments list will end up as the first one with the call to rot. An example: (company staff (get> workwith)) will become ((get> workwith) company staff). This is passed to evalList> which will evaluate the list by first unest>ing them.
In this case arguments passed to eval> will be (get> workwith), company and staff. And the returned result will be (person company staff), person will end up in As and LoopMe will contain the list of employed persons (staff). In essence the above will mean “loop each member in the staff list in the template variable company and put each person in the variable person, then execute the run code with this information”. However if the list is not OK (empty for instance) we run the alternative code.
Only if> left now:
(dm if> (RunC AltC . @) (if (apply '= (evalSplit> This (unest> This (rest)) 'eq)) (run> This RunC) (when AltC (apply> This AltC)))) (dm evalSplit> (Lst . @) (evalList> This (split Lst (next))))
It’s similar to loop>. We begin with applying = to the result of evalSplit> which in turn gets the result of unest> and ‘eq. Let’s take (get> person name eq (get> company name)) as an example. In this case unest> is redundant since the car is not immediately nested. Since evalList> is calling evalList> it will pass (get> person name) and (get> company name) and return for instance (John Scania) which is the list that = is applied to. The result is of course NIL in this case and we end up running the alternative code.
Finally it’s time to output the result when the web server is started:
(dm out> () (httpHead NIL 0) (ht:Out *Chunked (for El (: html) (if (lst? El) (eval> This El) (prin El)))))
We need proper headers (with httpHead ) and a staggered output (ht:Out *Chunked). We will now loop through our final result that we have in html. If we encounter a list in an element it must mean it didn’t get executed in a run> (at the moment this only applies to gui>). It is therefore executed, otherwise we just print the element.
(dm gui> (F) (load (pack (: baseDir) F ".l")))
So the argument (filename) we pass to gui> is simply loaded and should output itself directly (call it a widget if you want). The HTML outputted in this way should be kept to a minimum to allow for the template engine to generate as much HTML as possible, otherwise the programmer might get designers breathing down his neck to change this or that in the output, and we wouldn’t want that now would we?