Leandro Facchinetti <me@leafac.com>
S-expression-based CSS.
Version
Documentation
Code of Conduct
Distribution
Source
Bug Reports
Contributions
S-expressions are a convenient way of representing hierarchical data. Racket’s syntax is based on S-expressions and it includes particular dialects to support embedded domain-specific languages. For example, one can use X-expressions to represent XML in general or HTML in specific. Embedding documents in Racket programs as X-expressions has several benefits: parts of the document can be generated programmatically, the text editor can syntax-highlight and indent the code without extra effort and transformations on the document are data structure manipulations which do not require custom tools.
CSS-expressions go great with Pollen and Pollen Component.
CSS is a domain-specific language for styling documents. Documents can be generated with X-expressions, so it is natural to want a S-expression-based representation for CSS as well. We introduce CSS-expressions, a domain-specific language embedded in Racket which is based on S-expressions and outputs CSS. In addition to the benefits of embedded domain-specific languages already mentioned, CSS-expressions also supports extended features generally found in CSS preprocessors including Sass, Less and Stylus. For example, nested rules and declarations. Finally, because CSS-expressions are embedded in Racket, many features from the CSS preprocessors are free: variables, mixins, operations, and so on.
;Styles from http://bettermotherfuckingwebsite.com/[body#:margin(40pxauto)#:max-width650px#:line-height1.6#:font-size18px#:color|#444|#:padding(010px)][h1h2h3#:line-height1.2]))069)"...")"body{margin:40px auto;max-width:650px;line-height:1.6;font-size:18px;..."
CSS-expressions are a Racket package. Install it in DrRacket or with the following command line:
$ raco pkg install css-expr
See CSS-expressions in action on my personal website (source). It also uses Pollen Component.
First, require the library. In Racket, add the following line:
Or, in Typed Racket, add the following line:
(require css-expr/typed)
Then, build CSS-expressions with css-expr . Finally, use css-expr->css to transform CSS-expressions into CSS.
syntax
( css-expr expr...)
'((body #:width 700px) (.menu #:width 700px))
procedure
( css-expr->css css-expr)→String
css-expr:Sexp
The following subsections describe CSS-expressions with English and examples. A formal grammar is also available in the next section.
A complete CSS-expression is, at the top level, a stylesheet, which is a list of rules.
"body{margin:40px auto;}h1,h2,h3{line-height:1.2;}"
In the example above, ([body... ][h1h2h3... ]) is a stylesheet and each of [body... ] and [h1h2h3... ] are rules.
There are two kinds of rules: qualified rules and at-rules. Qualified rules contain declarations for the styles in the document.
"body{margin:40px auto;font-family:\"Fira Sans\";}"
In the example above, [body... ] is a qualified rule and each of #:margin(40pxauto) and #:font-family"Fira Sans" are declarations.
At-rules contain elements that control the stylesheet itself.
Identifiers starting with @ can be troublesome to generate programmatically (for example, (string->symbol (~a "@"some-computation))). So [@name... ] is an at-rule equivalent to [@name... ].
"@import \"other-stylesheet.css\";"
"@font-face{font-family:\"Fira Sans\";src:\"...\";}"
"@media screen and (min-width:700px){body{font-size:20px;}}"
In the example above, [@import... ], [@font-face... ] and [@media... ] are at-rules. They differentiate from qualified rules by the @ prefix. The rule [@import... ] is requesting the browser to load another stylesheet; it shows how at-rules handle expressions (in the example, the expression is "other-stylesheet.css"). The rule [@font-face... ] is defining a new font and showing how at-rules can include declarations (in the example, #:font-family"Fira Sans" and #:src"..." are declarations). Finally, the rule [@media... ] is declaring qualified rules that only apply under certain conditions; showing that at-rules can include qualified rules (in the example, [body... ]) in addition to expressions (in the example, (and ... )).
CSS-expressions allow for arbitrary nesting of rules within one another, which is an extension to plain CSS generally found in preprocessors including Sass, Less and Stylus. The nested rules are unnested during the process of translating CSS-expressions into CSS.
".menu{width:700px;}.menu .item{text-decoration:none;}"
".menu{width:700px;}@media (min-width:500px){.menu{color:green;}}"
"@media screen and (min-width:700px){body{font-size:20px;}}"
In the first example, nested qualified rules illustrate how to compactly define components. In the second example, a media query is nested within a qualified rule, which is convenient for responsive design. The third example shows how nested at-rules compose.
When nesting qualified rules, the default combinator for selectors is the descendant combinator, which in CSS is pronounced with a space—see example above. To combine selectors differently, it is necessary to explicitly refer to the selector of the parent qualified rule in the selector of the nested qualified rule. Accomplish that using &.
".menu{width:700px;}.menu>.item{text-decoration:none;}"
In the example above, the declaration #:text-decorationnone is only effective on .items that are immediate children of .menus.
The following kinds of parent selectors allow for an added suffix with &-: identifiers, namespaced selectors, prefixed selectors or combinations in which the last selector is one of the previous kinds.
Under special conditions one can declare nested rules that add a suffix to the parent selector. Accomplish that using &-.
".menu{width:700px;}.menu-item{text-decoration:none;}"
"#main{color:blue;}#main-sidebar{color:pink;}"
At-rule expressions occur in at-rules, after the @name and before any declarations or inner rules. In its simplest form, an at-rule expression is a value.
"@import \"other-stylesheet.css\";"
In the example above, "other-stylesheet.css" is a value standing for an at-rule expression.
Multiple at-rule expressions in a list translate as a composite expression in CSS.
"@import \"other-stylesheet.css\" screen;"
In the example above, the list ("other-stylesheet.css"screen) translated to the composite expression "other-stylesheet.css"screen in CSS.
Multiple at-rule expressions in a row translate to alternative expressions in CSS.
"@media screen,print{body{line-height:1.2;}}"
In the example above, each of screen and print are at-rule expressions on their own. They translate to alternative expressions in CSS—separated by the comma.
Declarations can be at-rule expressions, and they have to be surrounded by parenthesis to be distinguished from declarations in the body of the at-rule.
"@media (min-width:700px){body{line-height:1.2;}}"
"@font-face{font-family:\"Fira Sans\";src:\"...\";}"
In the first example, (#:min-width700px) is a declaration working as an at-rule expression. Note the surrounding parenthesis; they are necessary to differentiate from declarations in the at-rule body, the case which the second example illustrates.
The last type of at-rule expressions is operations involving other at-rule expressions.
"@media screen and (min-width:700px){body{line-height:1.2;}}"
"@media screen or print{body{line-height:1.2;}}"
"@media not screen and (min-width:700px){body{line-height:1.2;}}"
"@media only screen{body{line-height:1.2;}}"
Declarations set CSS properties. In CSS-expressions, declarations are the property names as keywords followed by values and an optional !important flag.
"body{line-height:1.2;background-color:black;}"
"body{line-height:1.2 !important;}"
In the first example above, #:line-height1.2 and #:background-colorblack are declarations. The properties names are #:line-height and #:background-color, while 1.2 and black are values. The second example above illustrates the use of the !important flag.
CSS-expressions also support an extension generally found in CSS preprocessors including Sass, Less and Stylus: nested declarations.
"body{font-size:18px;font-family:Helvetica;}"
In the example above, the #:font common prefix has been factored out from the declarations of #:font-size and #:font-family.
Note that values can occur before the nested declarations.
"body{font:italic;font-size:18px;font-family:Helvetica;}"
In the example above, italic is a value before the nested declarations (#:size... ).
This simplest kinds of value are symbols, numbers and strings.
"body{font-style:italic;line-height:3.5;font-family:\"Fira Sans\";}"
In the example above, italic is a symbol value, 3.5 is a number value and "Fira Sans" is a string value. Note how strings are quoted in the CSS output while symbols are not.
Similar to what happens in at-rule expressions, composite values must be enclosed in parenthesis.
"body{font:italic 18px \"Fira Sans\";}"
In the example above, (italic18px"Fira Sans") is a composite value.
A list of values is just laid out after the property name, without enclosing parenthesis.
"body{font-family:\"Fira Sans\",sans-serif;}"
In the example above, "Fira Sans" and sans-serif compose a list of values.
Writing colors as "#ff00dd" does not work, because the CSS would include quotes around the value.
Hexadecimal colors in CSS-expressions are just symbols, but it is necessary to escape the hash because of Racket’s parsing rules.
"body{color:#ff00dd;}"
In the example above, one can write the color as either |#ff00dd| or \#ff00dd.
Measurement symbols can be troublesome to generate programmatically (for example, (string->symbol (~a some-computation"px"))). So (px12) is a value equivalent to 12px.
Measurements are also just symbols.
"body{margin-left:12px;}"
In the example above, 12px is a measurement. Valid measurement units are: %, em, ex, ch, rem, vw, vh, vmin, vmax, cm, mm, q, in, pt, pc, px, deg, grad, rad, turn, s, ms, hz, khz, dpi, dpcm and dppx.
Note that apply is a form in CSS-expressions. It results in CSS that looks like function application, which is not equivalent to unquoting from the CSS-expression and writing a Racket expression using Racket’s apply . For example, (apply calc2px) translates to calc(2px), while (px,(apply + '(11))) translates to 2px. Both are valid forms, useful in different contexts.
Use apply to write values that look like function application.
"body{color:rgb(20,30,40);}"
In the example above, (apply rgb203040) is a value that looks like a function application in CSS.
Note that operations on values are forms in CSS-expressions. They result in operations in CSS, which are not equivalent to unquoting from the CSS-expression and writing Racket expressions. For example, (+ 2px3px) translates to 2px+ 3px, while (px,(+ 23)) translates to 5px. Both are valid forms, useful in different contexts.
Operations on values are also valid values.
"body{width:calc(12px - 2px);}"
In the example above, (- 12px2px) is an operation. Valid operands are + , - , * and / .
The simplest kind of selector is an identifier.
"li{width:700px;}"
In the example above, li is a selector that matches all occurrences of the <li> HTML tag.
Multiple selectors in a row turn into a list.
"li,a{width:700px;}"
In the example above, the rule matches any <li> and any <a> HTML tag.
Enclose selectors in parenthesis to combine them with the descendant combinator—which in CSS is pronounced with a space.
"li a{width:700px;}"
In the example above, the rule matches any link (<a>) which occurs anywhere within a list item (<li>).
Namespaced symbols can be troublesome to generate programmatically (for example, (string->symbol (~a some-computation"|"other-computation))). So (\|nsa) is a selector equivalent to ns\|a. Again, note how it is necessary to escape the pipe (|).
Selectors can occur under a namespace.
"ns|a{width:700px;}"
In the example above, the rule only matches <a> tags under the ns namespace. Note that the pipe (|) needs escaping because of Racket’s parsing rules for identifiers.
Prefixed symbols can be troublesome to generate programmatically (for example, (string->symbol (~a some-computation"#"other-computation))). So (|#|main) is a selector equivalent to |#main|, and (|#|divmain) is a selector equivalent to div#main. Again, note how it is necessary to escape the hash (#).
This applies to all prefixed selectors. For example, (|.|amenu) is equivalent to a.menu. Note that the dot alone (.) has special meaning in Racket, so it also requires escaping.
Selectors may require prefixes.
"#menu{width:700px;}"
".menu-item{width:700px;}"
"a:hover{width:700px;}"
"a::before{width:700px;}"
In the first example above, note that the hash—the prefix for selecting by id—needs escaping because of Racket’s parsing rules for identifiers. The alternative spelling \#menu works the same.
Note that apply is a form in CSS-expressions. It results in CSS that looks like function application, which is not equivalent to unquoting from the CSS-expression and writing a Racket expression using Racket’s apply . For example, (apply nth-child2) translates to nth-child(2), while (|.|,(apply string-downcase '(ODD))) translates to .odd. Both are valid forms, useful in different contexts.
Pseudo-classes that look like function applications use the apply form.
"a:nth-child(2){width:700px;}"
In the example above, (apply nth-child2) translates to a pseudo-class that looks like function application: nth-child(2).
The arguments to those pseudo-classes that look like function applications can be selectors, other nested pseudo-classes that look like function application and An+B forms.
"a:not(.classy){width:700px;}"
"a:nth-child(odd){width:700px;}"
"a:nth-child(even){width:700px;}"
"a:nth-child(3){width:700px;}"
"a:nth-child(n){width:700px;}"
"a:nth-child(2n){width:700px;}"
"a:nth-child(2n+1){width:700px;}"
"a:nth-child(n+2){width:700px;}"
In the first example above, (apply not .classy) is a pseudo-class that looks like function application whose argument another selector (.classy). The rest of the examples illustrate the An+B form.
See Nesting for the specific cases in which it is valid to add a suffix to the parent selector with &-.
In nested qualified rules, the selector & stands for the parent selector. The selector &- is a reference to the parent selector that allows for suffixes in limited cases.
".menu{width:700px;}.menu>.item{text-decoration:none;}"
".menu{width:700px;}.menu-item{text-decoration:none;}"
"#main{color:blue;}#main-sidebar{color:pink;}"
Note that the || combinator requires escaping because of Racket’s parsing rules.
One can combine selectors in various ways.
".menu+.item{text-decoration:none;}"
".menu>.item{text-decoration:none;}"
".menu~.item{text-decoration:none;}"
".menu /for/ .item{text-decoration:none;}"
".menu||.item{text-decoration:none;}"
Attribute-based selectors use the attribute form.
".menu[selected]{text-decoration:none;}"
".menu[href=\"...\"]{text-decoration:none;}"
[(attribute.menu(~=href(case-insensitive"...")))#:text-decorationnone]))".menu[href~=\"...\" i]{text-decoration:none;}"
Valid operands for attributes are: = , ~=, ^=, $=, *= and \|=.
Thank you Matthew Butterick for Pollen—which motivated the creation of CSS-expressions—and for the feedback given in private email conversations. Thank you Greg Trzeciak for the early feedback. Thank you all Racket developers. Thank you all users of this library.
This section documents all notable changes to CSS-expressions. It follows recommendations from Keep a CHANGELOG and uses Semantic Versioning. Each released version is a Git tag.
Re-implemed CSS-expression from scratch using Nanopass. The user interface is the same.
Basic functionality.