Table of Contents
In the crowded space of Common Lisp HTML generators, Spinneret
occupies the following coordinates:
-
Modern. Targets HTML5. Does not treat XML and HTML as the same
problem. Assumes you will be serving your documents as UTF-8. -
Composable. Makes it easy to refactor HTML generation into separate
functions and macros. -
Pretty. Treats HTML as a document format, not a serialization.
Output is idiomatic and readable, following the coding style of the
HTML5 specification. -
Aggressive. If something can be interpreted as HTML, then it will
be, meaning that some Lisp forms can’t be mixed with HTML syntax. In
the trade-off between 90% convenience and 10% correctness Spinneret
is on the side of convenience. -
Bilingual. Spinneret (after loading
spinneret/ps
) has the same semantics in Lisp and Parenscript.
HTML generation with Spinneret looks like this:
(in-package #:spinneret)
(defparameter *shopping-list*
'("Atmospheric ponds"
"Electric gumption socks"
"Mrs. Leland's embyronic television combustion"
"Savage gymnatic aggressors"
"Pharmaceutical pianos"
"Intravenous retribution champions"))
(defparameter *user-name* "John Q. Lisper")
(defparameter *last-login* "12th Never")
(defmacro with-page ((&key title) &body body)
`(with-html
(:doctype)
(:html
(:head
(:title ,title))
(:body ,@body))))
(defun shopping-list ()
(with-page (:title "Home page")
(:header
(:h1 "Home page"))
(:section
("~A, here is *your* shopping list: " *user-name*)
(:ol (dolist (item *shopping-list*)
(:li (1+ (random 10)) item))))
(:footer ("Last login: ~A" *last-login*))))
Which produces:
*print-pretty*
should you want to turn it off.)
Printing style
Spinneret tries hard to produce human-writable output – output that
looks like a human being wrote it. Sometimes, however, you may have
markup to render that there is no human-writable way to render,
because no human being would ever write it.
In these cases you can set or bind the *html-style*
variable to
control Spinneret’s print style. The default is :human
, which means
to attempt to produce human-writable output. It can also be set to
:tree
, which simply prints every element as if it were a block
element, and every run of text on a new line.
Text link text more text
”
(let ((*html-style* :tree))
(with-html-string
(:div
(:p “Text ” (:a “link text”) ” more text”))))
=>
Text
link text
more text
“>
(let ((*html-style* :human))
(with-html
(:div
(:p "Text " (:a "link text") " more text"))))
=>
Text link text more text
"
(let ((*html-style* :tree))
(with-html-string
(:div
(:p "Text " (:a "link text") " more text"))))
=>
Text
link text
more text
With *html-style*
bound to :tree
, and *print-pretty*
bound to
nil, output is verbose but predictable:
Text link text more text
“”>
(let ((*html-style* :tree)
(*print-pretty* nil))
(with-html-string
(:div
(:p "Text " (:a "link text") " more text"))))
=> "Text link text more text
"
Notice that binding *html-style*
to :tree
ensures that all tags are
closed.
Inserted spaces
By default, when objects are output to HTML, spaces are inserted betweeen them. This is nearly always the right thing to do, but in some special cases, the spaces may be a problem. They can be turned off by setting the flag *suppress-inserted-spaces*
to t
.
Line wrapping
When pretty-printing, Spinneret makes the best decisions about line
wrapping that it can, given the information it has about how to get
the print length of various types. But, in the case of user-defined
types, it has no way to tell in advance how long they will be when
printed. If you find Spinneret is making bad line-breaking decisions
with your types, you can help it out by specializing html-length
.
For example, if you use PURI, you could help Spinneret pretty-print
PURI URIs by teaching it how to get their length:
(defmethod html-length ((uri puri:uri))
;; Doesn't cons.
(length (puri:render-uri uri nil)))
Syntax
The rules for WITH-HTML are these:
-
All generated forms write to
*html*
. -
A keyword in function position is interpreted as a tag name. If the
name is not valid as a tag, it is ignored.Certain keywords are recognized as pseudo-tags and given special
treatment::RAW :DOCTYPE :!DOCTYPE :CDATA :!– :COMMENT :HTML :HEAD :H* :TAG
-
The pseudotag :RAW can be used to bypass Spinneret’s implicit
escaping for raw output. This allows inserting HTML literals, and
bypasses pretty printing.Note that you need :RAW for inline stylesheets and scripts,
otherwise angle brackets will be escaped as if they were HTML:p{color: white;}”))
=> “”
(with-html-string (:style (:raw “a > p{color: white;}”)))
=> “”
” dir=”auto”>(with-html-string (:style "a > p{color: white;}")) => "" (with-html-string (:style (:raw "a > p{color: white;}"))) => ""
-
The pseudotags :!– and :COMMENT insert comments into the output.
-
The pseudotag :H* renders as one of :H1 through :H6 depending on
how many :SECTION elements it is dynamically nested inside. At the
top level, :H* is equivalent to :H1. Inside the dynamic extent of
one :SECTION tag, it is equivalent to :H2; inside two section
tags, it is equivalent to :H3; and so forth up to :H6. -
The pseudotag :TAG allows dynamic selection of a tag.
The value of the LANG attribute of HTML is controlled by
*html-lang*
; the value of the meta charset attribute is controlled
by*html-charset*
. These are defaults; passing an explicit
attribute takes precedence.Constant classes and ids can be specified with a selector-like
syntax. E.g.:(:div#wrapper (:div.section ...)) ≡ (:div :id "wrapper" (:div :class "section" ...))
-
-
Keyword-value pairs following a tag are interpreted as attributes.
HTML syntax may not be used in attribute values. Attributes with nil
values are omitted from the output. Boolean attributes with non-nil
values are minimized.Duplicate attributes are handled like duplicate keyword arguments:
all values are evaluated, but only the leftmost value is used. The
exception is the handling of tokenized attributes, such as :CLASS or
:REL. The class of a tag is the union of all its :CLASS arguments.The argument :DATASET introduces a list of :DATA-FOO arguments:
(:p :dataset (:duck (dolomphious) :fish 'fizzgigious :spoon "runcible")) ≡ (:p :data-duck (dolomphious) :data-fish 'fizzgigious :data-spoon "runcible")
For flexibility, even at the cost of efficiency, the argument :ATTRS
introduces a form to evaluate at run time for a plist of extra
attributes and values. -
Forms after the attributes are treated as arguments. Each non-nil
(primary) value returned by an argument to a tag is written to the
stream by HTML, a generic function on which you can define your own
methods. By default only literal arguments are printed. Literal
arguments are strings, characters, numbers and symbols beside NIL.
WITH-HTML-STRING is like WITH-HTML, but intercepts the generated HTML
at run time and returns a string.
Dynamic output
For flexibility, even at the cost of efficiency, the pseudo-attribute
:ATTRS introduces a form to evaluate at run time for a plist of extra
attributes and values.
“>
(:p :attrs (list :id "dynamic!"))
=>
Similarly, the pseudo-tag :TAG allows you to select a tag at run time.
(:tag :name "div"
(:tag :name "p"
(:tag :name "span"
"Hello.")))
≡ (:div (:p (:span "Hello")))
Note that :TAG only allows you to select a tag, not create one.
The tag must still be one that is known to Spinneret to be valid. (That is, either defined as part of HTML or matching the requirements for a custom element.)
For maximum dynamicity, you can combine :TAG and :ATTRS:
“>
(:tag :name "div" :attrs (list :id "dynamic!"))
=>
Interpreting trees
For the ne plus ultra of flexibility, you can interpret trees at runtime using a subset of Spinneret syntax:
“>
(interpret-html-tree `(:div :id "dynamic!"))
=>
The interpreter is still under development; it supports most but not yet all Spinneret syntax.
Markdown
If the additional system spinneret/cl-markdown
is loaded, then a
string in function position is first compile
!DOCTYPE>