luv

Workshop wiki

css.lisp

wiki/css.lisp

system luv-wiki · 82 definitions · on GitHub

A small CSS compiler: the stylesheet as Lisp definitions.

The site's stylesheet is written as define-style forms and compiled to CSS text when the site is rendered, the way the shader language compiles to SPIR-V and MSL from forms. So the rules that draw a layout role can sit beside the layout that assigns it, be re-evaluated in a running image, and be computed by ordinary Lisp.

The compiler has three parts. A reader syntax (the luv.CSS:syntax readtable) reads CSS quantities and custom-property references as objects: 0.85rem and 100% are DIMENSIONs, 0.72rem/1 a slash pair, and --ink a variable-reference, so no number-and-unit symbols are interned. A syntax tree of CLOS instances -- style, rule, at-rule, declaration, selectors, and values -- built by the rule and define-style macros, whose bodies alternate :property keywords with values, where bare symbols are CSS words (grid, solid, inherit), strings are written verbatim, and lists are Lisp: (clamp 1rem 4vw 3rem) calls the CSS-function clamp, (gutter) calls a helper of your own, and a form may return values, declarations, or rules, which join the enclosing rule. Nested rules are ("selector" . body) with & standing for the parent, and (:media "query" . body) is an at-rule. Finally write-css is the text backend, a generic over the tree; another backend can walk the same objects.

in-package#:luv.css
eval-when
:compile-toplevel:load-toplevel:execute
defuncss-name
symbol

The CSS spelling of symbol: its name in lowercase, unless the name was escaped to keep lowercase letters, when it is kept as written.

let
name
symbol-namesymbol
if
some#'lower-case-pname
name
string-downcasename

Values

defclassdimension
number:initarg:number:readerdimension-number
unit:initarg:unit:readerdimension-unit
:documentation

A number with a unit: 0.85rem, 100%, 0.15s. Read from 0.85rem by the CSS syntax; plain numbers stay numbers.

defclassslash
left:initarg:left:readerslash-left
right:initarg:right:readerslash-right
:documentation

Two values joined by a slash, as in the font shorthand 0.72rem/1 or grid-area 1/3.

defclassvariable-reference
name:initarg:name:readervariable-reference-name
:documentation

A reference to the custom property --NAME, written var(--name). Read from --name by the CSS syntax, or made by var.

defclassfunction-call
name:initarg:name:readerfunction-call-name
arguments:initarg:arguments:readerfunction-call-arguments
:documentation

A CSS function call: NAME(ARGUMENT, ARGUMENT). An argument that is a list is a space-separated run.

defclasscomma-list
values:initarg:values:readercomma-list-values
:documentation

Comma-separated values, as in a font stack.

defclassquoted-string
text:initarg:text:readerquoted-string-text
:documentation

A CSS string literal, written in double quotes. A Lisp string is written verbatim, so this is how a font name or content is quoted.

defmethodprint-object
objectdimension
stream
print-unreadable-object
objectstream:typet
write-css-valueobjectstream
defmethodprint-object
stream
print-unreadable-object
objectstream:typet
write-css-valueobjectstream
defmethodprint-object
objectslash
stream
print-unreadable-object
objectstream:typet
write-css-valueobjectstream

The reader returns these as literal objects, so compiled files must dump them.

defmethodmake-load-form
objectdimension
&optionalenvironment
make-load-form-saving-slotsobject:environmentenvironment
defmethodmake-load-form
objectslash
&optionalenvironment
make-load-form-saving-slotsobject:environmentenvironment
defmethodmake-load-form
&optionalenvironment
make-load-form-saving-slotsobject:environmentenvironment
defundimension
numberunit
make-instance'dimension:numbernumber:unit
string-downcase
stringunit
defunvar
name

A reference to the custom property name, a string or symbol without the --.

make-instance'variable-reference:name
string-downcase
stringname
defunquoted
text

text as a CSS string literal.

make-instance'quoted-string:texttext
defuncomma-list
&restvalues
make-instance'comma-list:valuesvalues
defmacrodefine-css-function
name&optional
css-name
css-namename

Define name as a Lisp function building the CSS function call css-name(...).

`
defun,name
&restarguments
make-instance'function-call:name,css-name:argumentsarguments
defuncolor-mix
&restcolors

The CSS color-mix() of colors in the sRGB space, a percentage after a colour weighting it: (color-mix --paper 94% --accent).

let
groups'
dolist
itemcolors
if
and
typepitem'dimension
string=
dimension-unititem
"%"
groups
setf
firstgroups
append
firstgroups
listitem
push
listitem
groups
make-instance'function-call:name"color-mix":arguments
list*'
insrgb
nreversegroups
defunfont-stack
&restfamilies

A comma-separated font-family list; strings are quoted, symbols bare.

apply#'comma-list
mapcar
lambda
family
if
stringpfamily
quotedfamily
family
families

Reader syntax

defunread-token-text
stream

The characters of the token starting at stream, up to whitespace or a terminating macro character.

with-output-to-string
out
loopforchar=
peek-charnilstreamnilnil
while
andchar
not
memberchar'
#\Space#\Tab#\Newline#\Return#\Page#\Linefeed
not
findchar"\"'(),;`"
do
write-char
read-charstream
out
defunread-standard-object
text

text read with the standard syntax in the current package.

let
*readtable*
named-readtables:find-readtable:standard
values
read-from-stringtext
defunparse-number
text

text as a number, or NIL when it is not one.

and
plusp
lengthtext
every
lambda
char
or
digit-char-pchar
findchar"+-.eE/"
text
let
object
ignore-errors
and
numberpobject
object
defunparse-quantity
text

text as a number or dimension, or NIL.

or
let
unit-start
let
position
position-if-not
lambda
char
or
alpha-char-pchar
char=char#\%
text:from-endt
andunit-start
<unit-start
lengthtext
let
number
parse-number
subseqtext0unit-start
andnumber
dimensionnumber
subseqtextunit-start
defunparse-css-token
text

The object text denotes under the CSS syntax: a number, a dimension, a slash of two quantities, a variable-reference for --name, or else what the standard reader makes of it.

or
let
andslash
let
left
right
andleftright
make-instance'slash:leftleft:rightright
and
>
lengthtext
2
string="--"text:end22
var
subseqtext2
defunread-css-token
streamchar

Reader macro function for the digits and hyphen under the CSS syntax.

unread-charcharstream
named-readtables:defreadtablesyntax
:merge:standard
:macro-char#\0'read-css-tokent
:macro-char#\1'read-css-tokent
:macro-char#\2'read-css-tokent
:macro-char#\3'read-css-tokent
:macro-char#\4'read-css-tokent
:macro-char#\5'read-css-tokent
:macro-char#\6'read-css-tokent
:macro-char#\7'read-css-tokent
:macro-char#\8'read-css-tokent
:macro-char#\9'read-css-tokent
:macro-char#\-'read-css-tokent

Selectors

defclassselector
defclasscomplex-selector
text:initarg:text:readerselector-text
:documentation

One selector as CSS text,

.door>.title

; & in it stands for the parent selector when nested.

defclassselector-list
selectors:initarg:selectors:readerselector-list-selectors
:documentation

A comma-separated group of selectors, matching any of them.

defgenericparse-selector
designator
:documentation

The selector designator names: a string is one selector, a list of strings a group.

:method
designatorselector
designator
:method
designatorstring
make-instance'complex-selector:textdesignator
:method
designatorlist
make-instance'selector-list:selectors
mapcar#'parse-selectordesignator
defgenericselector-alternatives
:documentation

The complex selectors selector is a group of.

:method
mapcan#'selector-alternatives
selector-list-selectorsselector
defgenericcombine-selectors
parentchild
:documentation

child nested under parent: each alternative of child against each of parent, & standing for the parent, otherwise a descendant.

:method
parentnull
child
child
:method
let
parent-text
child-text
make-instance'complex-selector:text
if
search"&"child-text
replace-allchild-text"&"parent-text
concatenate'stringparent-text" "child-text
:method
parentselector
make-instance'selector-list:selectors
loopforpinappend
defunreplace-all
stringpartreplacement
with-output-to-string
out
loopwithstart=0forposition=
searchpartstring:start2start
do
write-stringstringout:startstart:end
whilepositiondo
write-stringreplacementout
setfstart

The tree

defclassnode
:documentation

Anything in a stylesheet: a declaration, rule, at-rule, or style group.

defclassdeclaration
property:initarg:property:readerdeclaration-property
values:initarg:values:initform'
:accessordeclaration-values
:documentation

PROPERTY: VALUES; -- the property a keyword, the values a list written space-separated.

defclasscontainer
children:initform'
:accessorcontainer-children
:documentation

A node holding declarations, rules, and at-rules in order.

defclassrule
selector:initarg:selector:readerrule-selector
:documentation

selector { declarations }, and nested rules and at-rules whose selectors combine with this one.

defclassat-rule
name:initarg:name:readerat-rule-name
prelude:initarg:prelude:readerat-rule-prelude
:documentation

@NAME PRELUDE { children }. Inside a rule, its declarations apply to that rule's selector under the query.

defclassstyle
name:initarg:name:readerstyle-name
documentation:initarg:documentation:initformnil:accessorstyle-documentation
:documentation

A named group of rules, compiled in definition order under its documentation as a comment.

defgenericadd-child
:documentation

Append node to container's children.

:method
pushnode
container-childrencontainer
:method
error"A declaration ~S outside any rule in style ~A."node
style-namecontainer
defgenericadd-item
:documentation

Add item, one evaluated form of a rule body, to container. declaration is the open declaration values append to; the value returned is the declaration open after item.

:method
cond
keywordpitem
let
new
make-instance'declaration:propertyitem
new
t
call-next-method
:method
unlessdeclaration
error"Value ~S in ~S has no property."itemcontainer
setf
declaration-valuesdeclaration
append
declaration-valuesdeclaration
listitem
declaration
defunadd-body

Add items to container in order, then fix the order of its children.

setf
container-childrencontainer
nreverse
container-childrencontainer
container
defunmake-rule
add-body
make-instance'rule:selector
items
defunmake-at-rule
namepreludeitems
add-body
make-instance'at-rule:namename:preludeprelude
items
defvar*styles*'

style objects in the order they were first defined; redefining a name replaces its rules in place, so a re-evaluated group keeps its position.

defunensure-style
namedocumentationitems

Define or redefine the style name; the first definition fixes its order.

let
style
or
findname*styles*:key#'style-name
let
new
make-instance'style:namename
setf*styles*
append*styles*
listnew
new
setf
style-documentationstyle
documentation
container-childrenstyle
'
defunfind-style
name
findname*styles*:key#'style-name

The macros

eval-when
:compile-toplevel:load-toplevel:execute
defunselector-designator-p
object
or
stringpobject
and
conspobject
every#'stringpobject
defunbody-item-form
item

The form for item of a rule body: keywords are properties, other symbols CSS words, ("selector" . body) a nested rule, (:name prelude . body) an at-rule, and any other list a Lisp form.

typecaseitem
keyworditem
symbol`',item
cons
let
head
firstitem
cond
selector-designator-phead
`
rule,head,@
restitem
keywordphead
`
at-rule,head,
seconditem
,@
cddritem
titem
titem
defmacrorule
selector&bodybody

A rule for selector, a string, a list of strings, or a form; body as define-style describes. Bare symbols in body are CSS words, not variables: a body computed from Lisp values is a list for make-rule.

`
make-rule,
if
selector-designator-pselector
`',selectorselector
list,@
mapcar#'body-item-formbody
defmacroat-rule
nameprelude&bodybody

The at-rule @name prelude with body as define-style describes.

`
make-at-rule,name,prelude
list,@
mapcar#'body-item-formbody
defmacrodeclarations
&bodybody

The declarations body makes, read as a rule body, for a rule to include: the way to write a mixin.

`
container-children
add-body
make-instance'container
list,@
mapcar#'body-item-formbody
defuncustom-property
name&restvalues

The declaration of the custom property --NAME with values.

make-instance'declaration:property
intern
formatnil"--~:@(~A~)"name
:keyword
:valuesvalues
defmacrodefine-style
name&bodybody

Define the style group name: an optional docstring, then rules.

A rule is (selector . body), selector a string or a list of strings. body alternates :property keywords with values -- bare symbols are CSS words, strings are written verbatim, numbers and 0.85rem quantities and --name references are themselves -- and lists are Lisp forms: a CSS function such as (clamp 1rem 4vw 3rem), or any function returning values, declarations, or rules to include. Nested rules ("selector" . body) combine with the parent, & standing for it; (:media "query" . body) is an at-rule.

let
documentation
and
stringp
firstbody
popbody
`
ensure-style',name,documentation
list,@
mapcar#'body-item-formbody

Text backend

defvar*blank-line-pending*nil"Whether the next block written should be preceded by a blank line."
defunwrite-indent
indentstream
looprepeat
*2indent
do
write-char#\Spacestream
defunbegin-block
stream
when*blank-line-pending*
terpristream
defgenericwrite-css-value
valuestream
:documentation

Write value as CSS text.

:method
valuesymbol
stream
write-string
css-namevalue
stream
:method
valuestring
stream
write-stringvaluestream
:method
valueinteger
stream
princvaluestream
:method
valuefloat
stream
let
*read-default-float-format*
type-ofvalue
princvaluestream
:method
valueratio
stream
write-css-value
floatvalue1.0
stream
:method
valuelist
stream
write-css-valuesvaluestream
:method
stream
write-css-value
dimension-numbervalue
stream
write-string
dimension-unitvalue
stream
:method
valueslash
stream
write-css-value
slash-leftvalue
stream
write-char#\/stream
write-css-value
slash-rightvalue
stream
:method
formatstream"var(--~A)"
variable-reference-namevalue
:method
stream
write-string
function-call-namevalue
stream
write-char#\(stream
loopfor
argument.more
on
function-call-argumentsvalue
do
write-css-valueargumentstream
whenmore
write-string", "stream
write-char#\)stream
:method
stream
loopfor
item.more
on
comma-list-valuesvalue
do
write-css-valueitemstream
whenmore
write-string", "stream
:method
stream
write-char#\"stream
loopforcharacross
quoted-string-textvalue
do
when
findchar"\"\\"
write-char#\\stream
write-charcharstream
write-char#\"stream
defunwrite-css-values
valuesstream

Write values space-separated.

loopfor
value.more
onvaluesdo
write-css-valuevaluestream
whenmore
write-char#\Spacestream
defuncss-value-text
value
with-output-to-string
out
defgenericwrite-css
nodestream&keyparentindent
:documentation

Write node as CSS text to stream. parent is the selector node is nested under, indent the block depth.

defmethodwrite-css
stream&keyparent
indent0
declare
ignoreparent
when
null
declaration-valuesdeclaration
error"Declaration ~A has no value."
declaration-propertydeclaration
write-indentindentstream
formatstream"~A: "
css-name
declaration-propertydeclaration
write-css-values
declaration-valuesdeclaration
stream
formatstream";~%"
defunwrite-declaration-block
write-indentindentstream
formatstream"~A {~%"
write-indentindentstream
formatstream"}~%"
defunpartition-children

container's declarations, and its other children, in order.

let
children
container-childrencontainer
values
remove-if-not
lambda
child
typepchild'declaration
children
remove-if
lambda
child
typepchild'declaration
children
defmethodwrite-css
stream&keyparent
indent0
let
multiple-value-bind
dolist
nodenested
write-cssnodestream:parentselector:indentindent
defmethodwrite-css
stream&keyparent
indent0
write-indentindentstream
formatstream"@~A ~A {~%"
css-name
at-rule-nameat-rule
at-rule-preludeat-rule
multiple-value-bind
whendeclarations
unlessparent
error"Declarations directly inside @~A ~A at top level."
css-name
at-rule-nameat-rule
at-rule-preludeat-rule
dolist
nodenested
write-cssnodestream:parentparent:indent
1+indent
write-indentindentstream
formatstream"}~%"
defmethodwrite-css
stream&keyparent
indent0
when
style-documentationstyle
write-indentindentstream
formatstream"/* ~A */~%"
style-documentationstyle
dolist
node
container-childrenstyle
write-cssnodestream:parentparent:indentindent
defuncss-text
&restnodes

The CSS text of nodes.

let
with-output-to-string
out
dolist
nodenodes
defunstylesheet-text
&optional
styles*styles*

The whole stylesheet: every style group in order.

apply#'css-textstyles