HTML generator

== Extending tcl to generate html ==

CMcC Following from jenglish's comment: why learn to use a monster HTML-template library when you can, with a few lines of code, extend the base language so you can write things like [<A> href "/home.html" { ! "Home" }] ... I wrote the following to do just that using the techniques in let unknown know.


 package provide know 1.0

 proc know {cond body} { 
    if {![info complete $body]} {error "incomplete command(s) $body"}
    proc unknown {args} [string map [list @c@ $cond @b@ $body] {
        if {![catch {expr {@c@}} res] && $res} {
            return [eval {@b@}]
        }
    }][info body unknown]
 } ;# RS

 package require know
 know {[string match <*> [lindex $args 0]]} {
    set tag [string trim [lindex $args 0] "<>"]
    ::proc ::<$tag> {args} [string map [list @T $tag] {
        set result {}
        foreach {n v} [lrange $args 0 end-1] {
            lappend result "$n='[armour $v]'"
        }
        return "<@T [join ${result}]>[lindex $args end]</@T>"
    }]
    return [eval $args]
 }

escargo 7 Jun 2007 - Is there really a "know" package available somewhere, or is this just presupposing that code from let unknown know has been sourced in somewhere? CMcC the latter. -- escargo - Perhaps you would be kind enough to paste in the version you require. I'd like to use wish-reaper to take all of the source from one place and then give it a bit of exercise.


This will generate a proc for any command like [<li>] which will do pretty much what you'd expect it to.

It could be extended to handle something like: [<tr><td> {tr-alist} {td-alist} args] which would use the join trickery below to generate a table row. [<ul><li>] [<ol><li>] would work similarly.

escargo 3 Jun 2007 - Pardon my density, but does this mean that [<li>] would put <li> in the output buffer? That's what I would expect it to do. What do you expect it to do? And I don't understand the { ! "Home" } notion at all. I presume ! must be shorthand for something, but It's not clear to me what. It can't be negation. Maybe some kind of data reference?

CMcC 'output buffer' is more than somewhat server dependent. What it does is return a properly formatted string suitable for delivery as part of an HTML page.

escargo How would this work for cases like (admittedly both limited and contrived.

 <table>
   <tr>
      <td NOWRAP align="center" class="first" id="monsterfromthe"><b>Forbidden Planet</b></td>
   </tr>
 <table>

CMcC something like this:

  [<table> [<tr> [<td> nowrap 1 align center class first id monster [<b> "Forbidden Planet"]]]]

would produce a rough equivalent of your example.

Of course, if you stored your table props in an array or dict it'd be even better:

  [<table> [<tr> [<td> {*}$attrs id monster [<b> "Forbidden Planet"]]]]

escargo - So one of the disadvantages is needing to quote strings ("Forbidden Planet") that would not need to be quoted in HTML?

CMcC in your example, the <table>...</table> string must have been implicitly quoted.


== Armouring HTML ==

Prevent output from containing html-significant characters

 interp alias {} armour {} string map {& &amp; < &lt; > &gt; \" &quot; ' &#39;}

== iteration ==

Often what you want isn't to iterate, but to convert a list into something else. One generic technique I've found helpful is the use of join, as follows:

  <ul><li>[[join $list </li></li>]</li></ul>

Which will convert a tcl list into an HTML unordered list. Similar techniques are possible with <table>s

The technique can be readily made generic.

== subst instead of eval ==

jcw's comment (on TemplaTcl: a Tcl template engine) that the problem with subst is that it requires awkward double-quoting lead to a realization that some of the common tcl commands which provide a body argument (if, while, foreach, for, switch) could be modified (in tcl) to use subst to evaluate the body using subst instead of eval

== Subst-if ==

Here's a variant of if which evaluates its conditional using subst and not eval

 proc If {args} {
    while {[llength $args] && ![uplevel expr [lindex $args 0]]} {
        set args [lrange $args 2 end]        ;# lose the cond and positive-cond

        if {[lindex $args 0] eq "else"} {
            break
        }

        set args [lrange $args 1 end] ;# assumed to be 'elseif'
    }
    return [uplevel subst [list [lindex $args 1]]] ;# return with neg-consequence
 }

 set a 1; set b 2
 puts [If {$a} {a is true} else {a is false}]
 puts [If {$a == $b} {a == b} elseif {$b == 1} {b is 1} else {b is not 1 it is $b}]

Note that this version does not support the then keyword in if, and requires the else keyword. This suits my style - it should be able to be modified if it doesn't suit yours.

== Subst-while ==

 proc While {cond body} {
    set result {}
    while {[uplevel $cond]} {
        lappend result [uplevel subst [list $body]]
    }
    return $result
 }

== Subst-foreach ==

 proc Foreach {args} {
    set body [lindex $args end]
    set vars [lrange $args 0 end-1]
    set script [string map [list %A $args %B $body %V $vars] {
        foreach %V {
            lappend {%A} [subst {%B}]
        }
        return [set {%A}]
    }]
    return [uplevel $script]
 }

 puts [Foreach i {a b c d} {<li>$i</li>}]

== Subst-switch ==

 proc Switch {args} {
    set switch {}
    foreach {key body} [lindex $args end] {
        if {$body eq "-"} {
            lappend switch $key -
        } else {
            lappend switch $key [list subst $body]
        }
    }
    return [uplevel [list switch {*}[lrange $args 0 end-1] $switch]]
 }

 foreach x {moop fnargle zip ni} {
    puts [Switch $x {
        moop {'$x' is moop}
        fnargle -
        zip {'$x' is one of fnargle or zip}
        default {who cares what '$x' is}
    }]
 }

Anything else needed? I guess someone could write For.


jcw - We'll have to agree to disagree here, Colin. You solve the problem by adding N alternative implementations, whereas I choose to solve it once with a pre-processing step.

CMcC I didn't mean to imply that you endorsed this direction of solution, merely that you had succinctly posed the problem. In considering the problem being solved by templating systems in light of some of the reasons given on that page, it occurred to me that the problem of adoption and usage by non-tcl people was not with tcl syntax per se (compared with the alternative offered,) but was as much with the superfluity of commands available, and with their lack of task-specificity. I therefore consider providing a bunch of functions which may apply to the specific domain to be an acceptable approach.


Using the above tools:

 [<ul> [Foreach item $cityList {
     <li>$item</li>
 }]

is equivalent to:

 [subst {
   <ul>[Foreach item $cityList {
     <li>$item</li>
   }]
   </ul>
 }]

and:

  [<ul> <li>[join $citiList </li><li>]</li>]

except the latter will always have at least one element.

The style you prefer is up to you.


Example - converting an array to a table:

  [<table> border 1 "[<tr> <th>Key</th><th>Value</th>]
    [Foreach {k v} [array get x] {
       [<tr> [<td> $k] [<td> $v]]
    }]"]

See Also: