A small alternative to the Garden CSS compiler that maps to modern CSS:
- It compiles to native nested rules.
[:a :b :c {…}]compiles toa b c {…}nota, b, c {…}.- Its grammar is flexible enough to allow arbitrary
@-rules.
Sections:
(->css
[":root" {:color-scheme "dark"
:background-color "light-dark(#fff, #000)"
:color "light-dark(#000, #fff)"}]
"/* NOTE: no commas are inserted between `article` and `p` like Garden does */"
[:article :p
[:strong {:border-bottom "2px solid #000"
:padding-bottom "4px"}]
["&::first-line" {:font-size "1.2rem"
:font-weight :bold
:text-decoration "underline"}]]
["blockquote::before" {:content (pr-str "“")
:font-size "2rem"
:color "#aaa"}]
["h1, h2, h3" ;; <-- you can do `:h1 "," :h2 "," :h3` if you really want to
{:font-weight :bold
:letter-spacing "0.02em"}]
[:h2 '+ :p {:margin-top "0.5rem"
:font-size "1.1em"}]
[:nav :> :ul {:list-style :none
:padding 0}]
[:button {:background :navy
:color :white}
["&:hover" {:background "#003399"}]
["&:disabled" {:opacity "0.5"
:cursor :not-allowed}]]
[:.alert '+ :.alert {:margin-top "8px"}]
["a[href^=https]" {:color "#0a0"}]
["input[type=text]" {:border "1px solid #ccc"
:border-radius "4px"}]
["[aria-hidden=true]" {:display :none}]
"@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700');"
"@layer reset, base, theme, utilities;"
["@media (max-width: 768px)"
[:body {:margin "0 8px"}]]
[:.foo {:display "grid"}
["@media (orientation: landscape)" {:grid-auto-flow :column}
["@media (width > 1024px)" {:max-inline-size "1024px"}]]]
["@layer utilities"
[:.padding-sm {:padding "0.5rem"}]
[:.margin-sm {:margin "0.5rem"}]]
["@page" {} ;; <-- empty map required for comment below to stay inside the block
"/* margin box at top right showing page number */"
["@top-right" {:content "\"Page \" counter(pageNumber)"}]]
"/* Targets all even-numbered pages */"
["@page :left" {:margin-top "4in"}]):root {
color-scheme: dark;
background-color: light-dark(#fff, #000);
color: light-dark(#000, #fff);
}
/* NOTE: no commas are inserted between `article` and `p` like Garden does */
article p {
strong {
border-bottom: 2px solid #000;
padding-bottom: 4px;
}
&::first-line {
font-size: 1.2rem;
font-weight: bold;
text-decoration: underline;
}
}
blockquote::before {
content: "“";
font-size: 2rem;
color: #aaa;
}
h1, h2, h3 {
font-weight: bold;
letter-spacing: 0.02em;
}
h2 + p {
margin-top: 0.5rem;
font-size: 1.1em;
}
nav > ul {
list-style: none;
padding: 0;
}
button {
background: navy;
color: white;
&:hover {
background: #003399;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.alert + .alert {
margin-top: 8px;
}
a[href^=https] {
color: #0a0;
}
input[type=text] {
border: 1px solid #ccc;
border-radius: 4px;
}
[aria-hidden=true] {
display: none;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700');
@layer reset, base, theme, utilities;
@media (max-width: 768px) {
body {
margin: 0 8px;
}
}
.foo {
display: grid;
@media (orientation: landscape) {
grid-auto-flow: column;
@media (width > 1024px) {
max-inline-size: 1024px;
}
}
}
@layer utilities {
.padding-sm {
padding: 0.5rem;
}
.margin-sm {
margin: 0.5rem;
}
}
@page {
/* margin box at top right showing page number */
@top-right {
content: "Page " counter(pageNumber);
}
}
/* Targets all even-numbered pages */
@page :left {
margin-top: 4in;
}We start by defining the CSS forms we care about:
css = item*
item = comment | statement | rule
comment = "/*" any* "*/"
statement = any* ";"
rule = prelude+ "{" attr* item* "}"
attr = key ":" value ";"
prelude = selector | at-ruleThen, we apply the following overrides to get our Clojure representation:
/* Hiccup-like representation */
rule = "[" prelude+ map? item* "]"
/* These are just passed through */
comment = string
statement = string
/* Allows any selector, combinator, at-rule, etc */
prelude = string | keyword | symbolNote
For templating:
- nil values in a rule are removed, allowing
(when ...) - non-vector sequences in a rule are unrolled, allowing
(for ...)
The code is short enough to understand and paste into your codebase. License is Zero-Clause BSD.
(defn unroll [xs]
(mapcat #(cond
(nil? %) nil ;; removed
(seq? %) (unroll %)
:else [%]) xs))
(defn ->css* [item]
(cond
;; for comments or at-rule statements
(string? item) item
;; for rule blocks with curly braces
(vector? item)
(let [rule item
prelude? (some-fn keyword? string? symbol?)
[preludes body] (->> rule
unroll
(split-with prelude?))]
(when-not (seq preludes)
(throw (ex-info "no prelude found" {:rule rule})))
(format "%s {%s}"
(str/join " " (map name preludes))
(if-not (seq body)
"\n"
(let [[m & nested-rules] (if (map? (first body))
body
(cons nil body))]
(as-> (concat (for [[k v] m
:when (and (some? k)
(some? v))]
(format "%s: %s;"
(name k)
(if (keyword? v)
(name v)
(str v))))
(map ->css* nested-rules)) _
(str/join "\n" _)
(str/replace _ #"(?m)^" " ") ;; indent each line
(str "\n" _ "\n"))))))
:else (throw (ex-info (format "->css* expects a rule vector or string, got %s." (pr-str (type item)))
{:item item}))))
(defn ->css [& items]
(->> items
unroll
(map ->css*)
(str/join "\n")))Feedback appreciated below.