https://github.com/ruricolist/spinneret
Skip to content Toggle navigation
Sign up
* Product
+
Actions
Automate any workflow
+
Packages
Host and manage packages
+
Security
Find and fix vulnerabilities
+
Codespaces
Instant dev environments
+
Copilot
Write better code with AI
+
Code review
Manage code changes
+
Issues
Plan and track work
+
Discussions
Collaborate outside of code
Explore
+ All features
+ Documentation
+ GitHub Skills
+ Blog
* Solutions
For
+ Enterprise
+ Teams
+ Startups
+ Education
By Solution
+ CI/CD & Automation
+ DevOps
+ DevSecOps
Resources
+ Learning Pathways
+ White papers, Ebooks, Webinars
+ Customer Stories
+ Partners
* Open Source
+
GitHub Sponsors
Fund open source developers
+
The ReadME Project
GitHub community articles
Repositories
+ Topics
+ Trending
+ Collections
* Pricing
Search or jump to...
Search code, repositories, users, issues, pull requests...
Search
[ ]
Clear
Search syntax tips
Provide feedback
We read every piece of feedback, and take your input very seriously.
[ ] [ ] Include my email address so I can be
contacted
Cancel Submit feedback
Saved searches
Use saved searches to filter your results more quickly
Name [ ]
Query [ ]
To see all available qualifiers, see our documentation.
Cancel Create saved search
Sign in
Sign up
You signed in with another tab or window. Reload to refresh your
session. You signed out in another tab or window. Reload to refresh
your session. You switched accounts on another tab or window. Reload
to refresh your session.
Dismiss alert
{{ message }}
ruricolist / spinneret Public
* Notifications
* Fork 20
* Star 299
Common Lisp HTML5 generator
License
MIT license
299 stars 20 forks Activity
Star
Notifications
* Code
* Issues 4
* Pull requests 0
* Actions
* Projects 0
* Wiki
* Security
* Insights
More
* Code
* Issues
* Pull requests
* Actions
* Projects
* Wiki
* Security
* Insights
ruricolist/spinneret
This commit does not belong to any branch on this repository, and may
belong to a fork outside of the repository.
master
Switch branches/tags
[ ]
Branches Tags
Could not load branches
Nothing to show
{{ refName }} default View all branches
Could not load tags
Nothing to show
{{ refName }} default
View all tags
Name already in use
A tag already exists with the provided branch name. Many Git commands
accept both tag and branch names, so creating this branch may cause
unexpected behavior. Are you sure you want to create this branch?
Cancel Create
11 branches 0 tags
Code
* Local
* Codespaces
*
Clone
HTTPS GitHub CLI
[https://github.com/r]
Use Git or checkout with SVN using the web URL.
[gh repo clone rurico]
Work fast with our official CLI. Learn more about the CLI.
* Open with GitHub Desktop
* Download ZIP
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
@ruricolist
ruricolist Allow keyword deftags to use selector syntax
...
d82aea8 Sep 16, 2023
Allow keyword deftags to use selector syntax
Closes #84.
d82aea8
Git stats
* 324 commits
Files
Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
.github/workflows
Pin CCL version for CI
April 29, 2023 13:15
.gitattributes
Add .gitattributes to protect tilde-newline directives.
February 6, 2019 23:29
LICENSE.txt
Initial import.
April 3, 2012 14:32
README.md
Allow using :raw on attribute values
April 29, 2023 13:15
cl-markdown.lisp
Fixed spinneret/cl-markdown loading from ASDF package inferred
systems.
December 19, 2018 21:50
compile.lisp
Allow keyword deftags to use selector syntax
September 16, 2023 12:16
deftag.lisp
Don't walk inner forms of a deftag in with-html
July 28, 2023 07:29
dynamic.lisp
Handle metatags when interpreting
September 4, 2022 10:40
functions.lisp
Use hash tables for looking up tags and properties
November 26, 2022 17:17
interpret.lisp
Fix test for ECL
December 3, 2022 18:07
package.lisp
Export html-stream class.
June 1, 2023 12:17
ps.lisp
Style: :import-from qualified external symbols
September 18, 2022 21:19
run.lisp
fix: don't insert leading newline when pretty printing pre tags
June 4, 2023 11:14
special.lisp
Added option to always quote attributes.
April 23, 2023 13:20
spinneret.asd
Allow overriding lang default with attribute
October 17, 2022 18:20
spinneret.lisp
Add a no-such-tag condition.
February 9, 2019 15:12
stream.lisp
Style: :import-from qualified external symbols
September 18, 2022 21:19
syntax.lisp
Added option to always quote attributes.
April 23, 2023 13:20
tags.lisp
Use load-time-value when generating hash tables
August 20, 2023 09:31
tests.lisp
Allow keyword deftags to use selector syntax
September 16, 2023 12:16
View code
[ ]
Spinneret Printing style Inserted spaces Line wrapping Syntax Dynamic
output Interpreting trees Markdown get-html-path *html-path* deftag
Spinneret in Parenscript Parenscript in Spinneret Validation
README.md
Table of Contents
* Spinneret
+ Printing style
+ Inserted spaces
+ Line wrapping
+ Syntax
o Dynamic output
o Interpreting trees
o Markdown
+ get-html-path
o *html-path*
+ deftag
+ Parenscript
+ Validation
Spinneret
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:
Home page
Home page
John Q. Lisper, here is your shopping list:
10 Atmospheric ponds
6 Electric gumption socks
4 Mrs. Leland's embyronic television combustion
9 Savage gymnatic aggressors
6 Pharmaceutical pianos
9 Intravenous retribution champions
(Pretty-printing is pretty fast, but Spinneret obeys *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.
(let ((*html-style* :human))
(with-html
(:div
(:p "Text " (:a "link text") " more text"))))
=>
With *html-style* bound to :tree, and *print-pretty* bound to nil,
output is verbose but predictable:
(let ((*html-style* :tree)
(*print-pretty* nil))
(with-html-string
(:div
(:p "Text " (:a "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:
(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 compiled as Markdown (using
CL-MARKDOWN), then passed to format as a control string and applied
to its arguments.
This is useful for inline formatting, like links, where sexps would
be clumsy:
(with-html
("Here is some copy, with [a link](~a)" link))
(with-html
(:span "Here is some copy, with "
(:a :href link "a link.")))
get-html-path
Sometimes it is useful for a piece of HTML-generating code to know
where in the document it appears. You might, for example, want to
define a tabulate function that prints list-of-lists as rows of
cells, but only prints the surrounding
if it is not
already within a table. The function get-html-path returns a list of
open tags, from latest to earliest. Usually it will look something
like
(get-html-path) ;-> '(:table :section :body :html)
Thus `tabulate' could be written
(defun tabulate (&rest rows)
(with-html
(flet ((tabulate ()
(loop for row in rows do
(:tr (loop for cell in row do
(:td cell))))))
(if (find :table (get-html-path))
(tabulate)
(:table (:tbody (tabulate)))))))
Note that get-html-path returns a freshly-consed list each time it is
called.
*html-path*
The variable underneath get-html-path is *html-path*, and it can be
let-bound to manipulate the nested tags (like :h* and tabulate from
the example above).
WARNING: Spinneret binds *html-path* with dynamic extent. If you need
to inspect the binding, use get-html-path instead to get a value you
can safely store.
*html-path* is most useful if the document generated by Spinneret is
split into several functions. Binding *html-path* allows to preserve
the structure of the document there.
Example:
(defun inner-section ()
"Binds *HTML-PATH* to replicate the depth the output is used in."
(with-html-string
(let ((*html-path* (append *html-path* '(:section :section))))
(:h* "Heading three levels deep"))))
(defun outer-section (html)
"Uses HTML from elsewhere and embed it into a section"
(with-html-string
(:section
(:h* "Heading two levels deep")
(:section
(:raw html)))))
(outer-section (inner-section))
;;
;;
Heading two levels deep
;;
Heading three levels deep
;;
;;
deftag
The stumbling block for all sexp-based HTML generators is order of
evaluation. It's tempting to write something like this:
;; Doesn't work
(defun field (control)
(with-html (:p control)))
(defun input (default &key name label (type "text"))
(with-html
(:label :for name label)
(:input :name name :id name :type type :value default)))
But it won't work: in (field (input "Default" :name "why" :label
"Reason")), (input) gets evaluated before (field), and the HTML is
printed inside-out.
Macros do work:
(defmacro field (control)
`(with-html (:p ,control)))
(defmacro input (name label &key (type "text"))
`(with-html
(:label :for ,name ,label)
(:input :name ,name :id ,name :type ,type)))
But we can do better than this. Spinneret provides a macro-writing
macro, deftag, which lets you refactor HTML without hiding it.
(deftag field (control attrs)
`(:p ,@attrs ,@control))
(deftag input (default attrs &key name label (type "text"))
(once-only (name)
`(progn
(:label :for ,name ,label)
(:input :name ,name :id ,name :type ,type
,@attrs
:value (progn ,@default)))))
A macro defined using deftag takes its arguments just like an HTML
element. Instead of
(input "Default" :name "why" :label "Reason") ; defmacro
You write
(input :name "why" :label "Reason" "Default") ; deftag
The macro re-arranges the arguments so they can be bound to an
ordinary lambda list, like the one above: the body of the tag is
bound to the first argument, and matching attributes are bound to
keywords. Multiple :class arguments, :dataset, and other shorthands
are handled exactly as in the usual HTML syntax.
But the great advantage of deftag is how it handles attributes which
are not bound to keywords. In the definition of input using deftag,
you see that the attrs catch-all argument is spliced into the call to
:input. This means that any unhandled attributes pass through to the
actual input element.
(input :name "why" :label "Reason" :required t :class "special" "Default")
=>
In effect, input extends the :input tag, almost like a subclass. This
is a very idiomatic and expressive way of building abstractions over
HTML.
(Spinneret used to provide a more elaborate way of building HTML
abstractions, deftemplate, but deftag is simpler and more useful.)
Spinneret in Parenscript
To use Spinneret with Parenscript, load the system spinneret/ps.
The semantics of Spinneret in Parenscript are almost the same. There
is no with-html-string, and with-html returns a DocumentFragment.
If Markdown support is enabled, strings in function position are
still parsed as Markdown, but supplying arguments triggers an error
(since Parenscript does not have format).
get-html-path is not implemented for Parenscript.
Neither :ATTRS nor :TAG is available in Parenscript.
Parenscript in Spinneret
To use Parenscript in Spinneret, remember to wrap the ps macro with
:raw, otherwise the generated JavaScript will be escaped.
(with-html-string
(:script
(:raw (ps
(defun greeting ()
(alert "Hello"))))))
=>
""
(with-html-string
(:div :onclick (:raw (ps (alert "Hello")))))
""
Validation
Spinneret does not do document validation, but it does warn, at
compile time, about invalid tags and attributes.
Although HTML5 does include a mechanism for application-specific
attributes (the data- prefix), some client-side frameworks choose to
employ their own prefixes instead. You can disable validation for a
given prefix by adding it to *unvalidated-attribute-prefixes*.
(pushnew "ng-" *unvalidated-attribute-prefixes* :test #'equal)
You can disable attribute validation altogether by adding the empty
string to the list:
;; Disable attribute validation.
(setf *unvalidated-attribute-prefixes* '(""))
Tags are considered valid if they are defined as part of the HTML
standard, or if they match the rules for the name of a custom element
- basically, start with an ASCII alphabetic character and include a
hyphen. For custom elements, attributes are not validated.
About
Common Lisp HTML5 generator
Resources
Readme
License
MIT license
Activity
Stars
299 stars
Watchers
10 watching
Forks
20 forks
Report repository
Releases
No releases published
Packages 0
No packages published
Contributors 17
* @ruricolist
* @aartaka
* @kilianmh
* @PuercoPop
* @svetlyak40wt
* @aadcg
* @russell
* @iamFIREcracker
* @wlangstroth
* @diasbruno
* @splittist
+ 6 contributors
Languages
* Common Lisp 100.0%
Footer
(c) 2023 GitHub, Inc.
Footer navigation
* Terms
* Privacy
* Security
* Status
* Docs
* Contact GitHub
* Pricing
* API
* Training
* Blog
* About
You can't perform that action at this time.