## What is TL-WHO?
TL-WHO is a translation of the Common Lisp library
[CL-WHO](https://edicl.github.io/cl-who/) to TXR Lisp.
It is a derived work of CL-WHO, copyrighted by the original authors and
carrying their licensing notices.
TL-WHO provides a macro for generating HTML using Lisp syntax.
Unlike some HTML generating libraries, TL-WHO works by translating the
syntax into code that writes HTML to a stream, rather that translating
the syntax into code which constructs a nested data structure, which is then
traversed to produce HTML. TL-WHO merges strings as much as possible.
For instance, here is what happens when we use it on syntax which is
entirely static (contains no computations whose results are to be
inserted into the HTML):
::text
1> (tl-who:with-html-output (*stdout* nil :indent t)
(:table (:tr (:td (:span "foo")))))
foo
And here the code which produces the HTML:
::text
2> (expand '(tl-who:with-html-output (*stdout* nil :indent t)
(:table (:tr (:td (:span "foo"))))))
(let ((*stdout* (sys:dvbind *stdout*
*stdout*)))
(put-string "\n
\n
\n
\n foo\n \n
\n
\n
"
*stdout*))
What is noteworthy here is that a single string is produced out of the nested
Lisp syntax, and that string is therefore sent to the `*stdout*` stream
using a single call to the `put-string` function.
Another nice feature of TL-WHO is that it has a "quiet" syntax free
of extraneous punctuation and nesting. A HTML tag is
written as a list headed by a keyword symbol, for instance `(:a)`
encodes ``. If this keyword is followed by keyword-value
properties, those are the attributes:
::text
(:a :href "foo.html")
->
(:a :href "foo.html" :target "__blank")
->
Then any remaining material after the keyword-value pairs is
interpreted as the tag content.
::text
(:a :href "foo.html" :target "__blank" "foo")
-> foo
All material that is not a tag or attribute keyword is an evaluated Lisp
expression, so no special syntax is needed to indicate evaluation.
When the value of an attribute is given as a Lisp expression, that
value is interpolated automatically into the generated attribute
syntax.
In the interior of a tag, when a Lisp expression is evaluated, its
value is **not** turned into output. It is that expression's
responsibility to generate output, either directly to the HTML
stream object, or via the convenient local macros like `fmt`,
`esc` or `str`.
## Documentation
Users of TL-WHO may rely on the original CL-WHO API documentation.
Here are the differences to be aware of:
* The symbol package is `tl-who` not `cl-who`. It is case-sensitive;
the symbol `TL-WHO:WITH-HTML-OUTPUT` refers to a non-existent
package, and so cannot even be read.
* The `*downcase-tokens-p*` variable does not exist in TL-WHO. It is
replaced by `*upcase-tokens-p*`, whose default value is `nil`.
TXR Lisp is case sensitive and lower-case is preferred for symbols.
A symbol like `:table` in TXR Lisp has the name `"table"` rather
than `"TABLE"`. And HTML is usually written in lower case nowadays,
so no case conversion is required to turn `:table` into `
`.
Therefore, in TL-WHO it is better to have a case conversion
variable that is off by default.
* Because TXR Lisp is case-sensitive, a symbol like `:TABLE` will
produce a `
` tag in upper case, whereas `:table` will
produce `
`. They are different symbols. Thus it is possible to
control case on a case by case basis, no pun intended. If the
`*upcase-tokens-p*` variable is bound to `t`, then both will
produce `
`.
* All of the elaborate HTML escaping support featured in CL-WHO is
absent in TL-WHO. These symbols do not exist: `escape-string`,
`escape-char`, `*escape-char-p*`, `escape-string-minimal`,
`escape-string-minimal`, `escape-string-minimal-plus-quotes`,
`escape-string-iso-8859-1`, `escape-string-all`,
`escape-char-minimal`, `escape-char-minimal-plus-quotes`,
`escape-char-iso-8859-1` and `escape-char-all`.
TL-WHO uses the TXR Lisp standard functions `html-encode`,
and `html-encode*`, which have no options to control its behavior.
`html-encode` is used internally for escaping material
to be inserted into attributes; `html-encode*` is used by `esc`
and `fmt`.
* TL-WHO fixes the issue that CL-WHO doesn't HTML-escape the values of
of constant string material, of attributes, and of material added
by local macro `fmt`.
This is a potential security issue, because if an untrusted value
is interpolated, it can be a vector for an injection attack.
The special variable `*cl-who-compat*` can be set true to disable the
escaping, but is not recommended.
::text
[1]> (in-package :cl-who)
#
WHO[2]> (with-html-output-to-string (out)
(:a :href "https://example.com'>malicious heremalicious hereclick me"
Not escaping constant material is error-prone.
The CL-WHO user has to remember to write `(:div "black&white")`
whereas the TL-WHO user just writes `(:div "black&white");` the
`&` escape is produced by TL-WHO.
* TL-WHO provides a `noesc` syntax. When the value of an attribute is
expressed as `(noesc )`, escaping is disabled:
::text
(:a :href (noesc trusted-url) "click me")
A more limited form of `noesc` can be used in tag bodies,
to defeat the default escaping applied to constant material.
The following:
::text
(:div (noesc "&") " <")
produces `
& <
`.
* TL-WHO provides a `noesc-fmt` which doesn't HTML-escape.
* TL-WHO provides `escq` and `escj` local macros. `escq` is like `esc`
but also HTML-escapes the ASCII apostrophe and double quote.
`escj` escapes a string such that it can be safely interpolated into
a Javascript string literal, which is itself embedded in a HTML
`script` tag.
* TL-WHO's `esc` accepts arguments which are not strings. They
are turned to a string using `tostringp`, and HTML-escaped.
* TL-WHO allows `fmt` as well as `escj` to be used to calculate attributes.
These local macros have alternative definitions when used in attributes.
The remaining local macros don't make sense in attributes, and make
a mess of the output if used; unlike CL-WHO, TL-WHO warns when they
are used in attributes.
* TL-WHO provides a `deftag` macro for defining macro-expanding tags.
This is inspired by a `deftag` described in the
[Spinneret](https://github.com/ruricolist/spinneret) documentation.
Though ordinary macros can easily be used inside `with-html-output`,
`deftag` is an expansion mechanism which transforms the tag
markup, rather than embedded Lisp code. It is documented below.
* The CL-WHO `conc` function is missing. TXR Lisp has a function
like this, which is called `join`, and that is what is used in TL-WHO.
* There is no generic function `convert-tag-to-string-list`.
It is an ordinary function. There is no support for customizing
its behavior. TXR Lisp doesn't have CLOS, and thus no generic functions
or methods. A mechanism for customizing the conversion of tags to
string lists may be adopted by TL-WHO in the future.
* Numerous instances of internal unused code that in CL-WHO do
not appear at all in TL-WHO; they have been pruned. Code depending
on internal CL-WHO functions that are not present in TL-WHO
will not work and must be ported in an alternative way.
* TXR Lisp isn't Common Lisp and so CL-WHO examples which use ANSI CL
idioms like the `loop` macro will not work. All Lisp code
embedded in a CL-WHO expression has to be translated to TXR Lisp
in order for that expression to work as TL-WHO; the TL-WHO
library doesn't translate embedded Common Lisp to TXR Lisp;
it only translates the markup syntax to HTML in the same way as CL-WHO.
* The variable `*attribute-quote-char*` will not work if it is bound by
`let` around a `with-html-output-form`. The reason is that TL-WHO
interpolates this character at macro-expansion time, even for
HTML attributes whose values are calculated at run-time.
The binding construct `expander-let` must be used, or else the
variable's global binding must be assigned.
* CL-WHO has some bugs around attribute handling. When the value of an
attribute is a constant expression, only the specific values `T`
and `NIL` are treated properly, not constant expressions which evaluate
to `T` and `NIL`. Then we see the mistaken attribute values `'NIL` and `'T'`:
This works properly in TL-WHO:
::text
[1]> (in-package :cl-who)
#
WHO[2]> (with-html-output-to-string (str)
(:foo :bar t))
""
WHO[3]> (with-html-output-to-string (str)
(:foo :bar (quote t)))
""
WHO[4]> (with-html-output-to-string (str)
(:foo :bar nil))
""
WHO[5]> (with-html-output-to-string (str)
(:foo :bar 'nil))
""
Additionally, users (of both CL-WHO and TL-WHO alike) are advised to watch for the
following issue: the CL-WHO documentation is not accurately maintained and
makes some references to material that no longer exists in CL-WHO, such as the
macro `show-html-expansion`, which was removed from CL-WHO in 2009.
## The `deftag` macro
### Syntax:
::text
(deftag
...)
::= (* [ . ])
:= | ( [ []])
### Description:
A `deftag` macro rewrites HTML markup written in TL-WHO syntax
into other markup.
Simple example:
::text
(deftag :boldface (. attrs) body
^(:span :font "bold" ,*attrs ,*body))
(with-html-output-to-string (out)
(:boldface :id "hello-id" "Hello!"))
--> Hello!
Complex example, adapted from Spinneret documentation:
::text
(deftag :easy-input (label (name (gensym))
(id name) (type "text") . other-attrs) default
^(progn
(:label :for ,name ,label)
(:input :name ,name :id ,id :type ,type
,*other-attrs :value (progn ,*default))))
Note that `progn` here isn't the Lisp `progn` operator; it is recognized
by the `deftag` expansion mechanism as a way of producing multiple elements.
To invoke `:easy-input`, we might do this:
::text
5> (with-html-output (*stdout* nil :indent t)
(:div :class "cls"
(:easy-input :name "foo" :id "foo-23"
:style "style" :label "lab" "123")))
Note a small flexibility: the `body` argument `"123"` of `:easy-input` wasn't
inserted as an element in the middle of the tag as in the simple example, but
as an attribute value.
Note that the `other-attrs` rest parameter of `:easy-input` received
a list which only contained the `:style` attribute; all the others
were captured by their respective keyword parameters.
### Parameters:
The `attr-param-list` is an implicit keyword parameter list
which destructures attributes. It can specify default values for
attributes that are not passed. Its rest parameter is bound
to the remaining attributes that were not captured by the parameters.
Each `key-param` is an ordinary TXR Lisp key parameter, implemented
by the TXR Lisp `:key` parameter list macro. It may be just a
name, or a name with `default` expression giving a value for
the parameter if the corresponds keyword argument is missing,
possibly followed by another variable name which, if present,
will be a Boolean value indicating, if true, that the keyword
argument was present, or if false that it was missing (and
thus defaulted).
## Dependencies
TL-WHO has no external dependencies other than TXR itself.
It requires version 287 or newer.
## Installation/Use
To use TL-WHO, you just have to put the code somewhere and `load`
the `tl-who.tl` file (or its compiled version, if you compile the code).
::text
(load "path/to/tl-who")
TXR Lisp's `load` is intelligent; the `tl-who` loader module easily loads the
needed files, which it finds in the same directory as itself.
TL-WHO symbols are in a package called `tl-who`, which is case sensitive.
Its private symbols are in `tl-who-priv`.
Two TL-WHO symbols have the same names as standard TXR library symbols,
namely `fmt` and `str`. Code which references the `tl-who` package in
preference to the `usr` package (for instance by putting `tl-who` first
in the package `:fallback` list) must use `usr:fmt` and `usr:str` to refer to
the TXR Lisp`fmt` functions, because the unprefixed tokens will produce
`tl-who:fmt` and `tl-who:str`, which are different symbols.
## License
TL-WHO is distributed under the two-clause BSD license.
[`LICENSE`](../tree/LICENSE) file.