Files
rose-ash/shared/sx/templates/tw-layout.sx
giles 33350ced6d Add comprehensive TW test suite (627 tests), fix 4 bugs, add 9 features
Bugs fixed:
- line-through: check full token not just head after hyphen split
- gap-x-N/gap-y-N: compound 2-part spacing prefix handler in tw-layout
- Negative values (-mt-4): replace ":" with ":-" instead of no-op
- Class name doubling: chain replace calls instead of concatenating

New features in tw-process-token:
- !important modifier (!p-4 → padding:1rem !important)
- dark: variant (class-based, .dark ancestor selector)
- group-hover:/group-focus:/group-active: (parent state)
- peer-focus:/peer-hover:/peer-checked:/peer-disabled: (sibling state)
- @container queries (@md:flex → @container(min-width:448px))
- Colour opacity modifier (bg-sky-500/50 → hsl with alpha)
- Ring colours (ring-sky-500 → --tw-ring-color)
- Arbitrary values (w-[300px], grid-cols-[1fr_2fr], bg-[#ff0000])
- colour-with-alpha helper for HSL+alpha generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:55:33 +00:00

418 lines
14 KiB
Plaintext

(define tw-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :gap-y "row-gap:{v}" :m "margin:{v}" :gap-x "column-gap:{v}" :my "margin-top:{v};margin-bottom:{v}" :px "padding-left:{v};padding-right:{v}" :pb "padding-bottom:{v}" :pr "padding-right:{v}" :p "padding:{v}" :gap "gap:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"})
(define tw-displays {:flex "flex" :table "table" :grid "grid" :inline-block "inline-block" :table-row "table-row" :inline "inline" :hidden "none" :block "block" :contents "contents" :inline-flex "inline-flex" :inline-grid "inline-grid" :table-cell "table-cell"})
(define tw-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :max "max-content" :5xl "64rem" :min "min-content" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw" :fit "fit-content"})
(define tw-min-widths {:full "100%" :0 "0px" :max "max-content" :min "min-content" :fit "fit-content"})
(define
tw-resolve-layout
(fn
(token)
(let
((parts (split token "-"))
(head (first parts))
(rest (slice parts 1)))
(cond
(and (= (len parts) 1) (not (nil? (get tw-displays head))))
(str "display:" (get tw-displays head))
(and (= (len parts) 2) (not (nil? (get tw-displays token))))
(str "display:" (get tw-displays token))
(and (get tw-spacing-props head) (= (len rest) 1))
(let
((tmpl (get tw-spacing-props head))
(v (tw-spacing-value (first rest))))
(if (nil? v) nil (tw-template tmpl v)))
(and
(= (len rest) 2)
(get tw-spacing-props (str head "-" (first rest))))
(let
((tmpl (get tw-spacing-props (str head "-" (first rest))))
(v (tw-spacing-value (last rest))))
(if (nil? v) nil (tw-template tmpl v)))
(and
(= head "space")
(= (len rest) 2)
(or (= (first rest) "x") (= (first rest) "y")))
(let
((v (tw-spacing-value (nth rest 1))) (dir (first rest)))
(if
(nil? v)
nil
(if (= dir "x") (str "column-gap:" v) (str "row-gap:" v))))
(and (= head "flex") (empty? rest))
"display:flex"
(and (= head "flex") (= (len rest) 1))
(case
(first rest)
"row"
"flex-direction:row"
"col"
"flex-direction:column"
"wrap"
"flex-wrap:wrap"
"nowrap"
"flex-wrap:nowrap"
"1"
"flex:1 1 0%"
"auto"
"flex:1 1 auto"
"initial"
"flex:0 1 auto"
"none"
"flex:none"
:else nil)
(and (= head "flex") (= (len rest) 2))
(case
(join "-" rest)
"row-reverse"
"flex-direction:row-reverse"
"col-reverse"
"flex-direction:column-reverse"
"wrap-reverse"
"flex-wrap:wrap-reverse"
:else nil)
(= head "grow")
(if
(empty? rest)
"flex-grow:1"
(if (= (first rest) "0") "flex-grow:0" nil))
(= head "shrink")
(if
(empty? rest)
"flex-shrink:1"
(if (= (first rest) "0") "flex-shrink:0" nil))
(and (= head "basis") (= (len rest) 1))
(let
((val (first rest)))
(cond
(= val "auto")
"flex-basis:auto"
(= val "full")
"flex-basis:100%"
(= val "0")
"flex-basis:0px"
(contains? val "/")
(let
((frac (split val "/")))
(if
(= (len frac) 2)
(let
((num (parse-int (first frac) nil))
(den (parse-int (nth frac 1) nil)))
(if
(or (nil? num) (nil? den))
nil
(str "flex-basis:" (* (/ num den) 100) "%")))
nil))
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "flex-basis:" (* n 0.25) "rem")))))
(and (= head "justify") (= (len rest) 1))
(case
(first rest)
"start"
"justify-content:flex-start"
"end"
"justify-content:flex-end"
"center"
"justify-content:center"
"between"
"justify-content:space-between"
"around"
"justify-content:space-around"
"evenly"
"justify-content:space-evenly"
"stretch"
"justify-content:stretch"
:else nil)
(and (= head "items") (= (len rest) 1))
(case
(first rest)
"start"
"align-items:flex-start"
"end"
"align-items:flex-end"
"center"
"align-items:center"
"baseline"
"align-items:baseline"
"stretch"
"align-items:stretch"
:else nil)
(and (= head "self") (= (len rest) 1))
(case
(first rest)
"auto"
"align-self:auto"
"start"
"align-self:flex-start"
"end"
"align-self:flex-end"
"center"
"align-self:center"
"stretch"
"align-self:stretch"
"baseline"
"align-self:baseline"
:else nil)
(and (= head "content") (= (len rest) 1))
(case
(first rest)
"start"
"align-content:flex-start"
"end"
"align-content:flex-end"
"center"
"align-content:center"
"between"
"align-content:space-between"
"around"
"align-content:space-around"
"evenly"
"align-content:space-evenly"
"stretch"
"align-content:stretch"
:else nil)
(and (= head "order") (= (len rest) 1))
(let
((val (first rest)))
(cond
(= val "first")
"order:-9999"
(= val "last")
"order:9999"
(= val "none")
"order:0"
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "order:" n)))))
(and (= head "grid") (empty? rest))
"display:grid"
(and (= head "grid") (>= (len rest) 2) (= (first rest) "cols"))
(let
((val (join "-" (slice rest 1))))
(cond
(= val "none")
"grid-template-columns:none"
(= val "subgrid")
"grid-template-columns:subgrid"
:else (let
((n (parse-int val nil)))
(if
(nil? n)
nil
(str "grid-template-columns:repeat(" n ",minmax(0,1fr))")))))
(and (= head "grid") (>= (len rest) 2) (= (first rest) "rows"))
(let
((val (join "-" (slice rest 1))))
(cond
(= val "none")
"grid-template-rows:none"
(= val "subgrid")
"grid-template-rows:subgrid"
:else (let
((n (parse-int val nil)))
(if
(nil? n)
nil
(str "grid-template-rows:repeat(" n ",minmax(0,1fr))")))))
(and (= head "grid") (>= (len rest) 2) (= (first rest) "flow"))
(case
(nth rest 1)
"row"
"grid-auto-flow:row"
"col"
"grid-auto-flow:column"
"dense"
"grid-auto-flow:dense"
:else nil)
(and (= head "col") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "span") (= val "full"))
"grid-column:1 / -1"
(= sub "span")
(let
((n (parse-int val nil)))
(if (nil? n) nil (str "grid-column:span " n " / span " n)))
(= sub "start")
(str "grid-column-start:" val)
(= sub "end")
(str "grid-column-end:" val)
:else nil))
(and (= head "row") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "span") (= val "full"))
"grid-row:1 / -1"
(= sub "span")
(let
((n (parse-int val nil)))
(if (nil? n) nil (str "grid-row:span " n " / span " n)))
(= sub "start")
(str "grid-row-start:" val)
(= sub "end")
(str "grid-row-end:" val)
:else nil))
(and (= head "auto") (>= (len rest) 2))
(let
((sub (first rest)) (val (join "-" (slice rest 1))))
(cond
(and (= sub "cols") (= val "auto"))
"grid-auto-columns:auto"
(and (= sub "cols") (= val "min"))
"grid-auto-columns:min-content"
(and (= sub "cols") (= val "max"))
"grid-auto-columns:max-content"
(and (= sub "cols") (= val "fr"))
"grid-auto-columns:minmax(0,1fr)"
(and (= sub "rows") (= val "auto"))
"grid-auto-rows:auto"
(and (= sub "rows") (= val "min"))
"grid-auto-rows:min-content"
(and (= sub "rows") (= val "max"))
"grid-auto-rows:max-content"
(and (= sub "rows") (= val "fr"))
"grid-auto-rows:minmax(0,1fr)"
:else nil))
(and
(= (len parts) 1)
(or
(= head "relative")
(= head "absolute")
(= head "fixed")
(= head "sticky")
(= head "static")))
(str "position:" head)
(and
(or
(= head "top")
(= head "right")
(= head "bottom")
(= head "left"))
(= (len rest) 1))
(let
((v (tw-spacing-value (first rest))))
(if (nil? v) nil (str head ":" v)))
(and (= head "inset") (= (len rest) 1))
(let
((v (tw-spacing-value (first rest))))
(if (nil? v) nil (str "inset:" v)))
(and (= head "inset") (= (len rest) 2))
(let
((dir (first rest)) (v (tw-spacing-value (nth rest 1))))
(if
(nil? v)
nil
(case
dir
"x"
(str "left:" v ";right:" v)
"y"
(str "top:" v ";bottom:" v)
:else nil)))
(and (= head "z") (= (len rest) 1))
(if
(= (first rest) "auto")
"z-index:auto"
(let
((n (parse-int (first rest) nil)))
(if (nil? n) nil (str "z-index:" n))))
(and (or (= head "w") (= head "h")) (= (len rest) 1))
(let
((prop (if (= head "w") "width" "height")) (val (first rest)))
(cond
(= val "full")
(str prop ":100%")
(= val "screen")
(str prop (if (= head "w") ":100vw" ":100vh"))
(= val "auto")
(str prop ":auto")
(= val "min")
(str prop ":min-content")
(= val "max")
(str prop ":max-content")
(= val "fit")
(str prop ":fit-content")
(contains? val "/")
(let
((frac (split val "/")))
(if
(= (len frac) 2)
(let
((num (parse-int (first frac) nil))
(den (parse-int (nth frac 1) nil)))
(if
(or (nil? num) (nil? den))
nil
(str prop ":" (* (/ num den) 100) "%")))
nil))
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str prop ":" (* n 0.25) "rem")))))
(and (= head "max") (>= (len rest) 2) (= (first rest) "w"))
(let
((val-name (join "-" (slice rest 1)))
(val (get tw-max-widths val-name)))
(if (nil? val) nil (str "max-width:" val)))
(and (= head "max") (>= (len rest) 2) (= (first rest) "h"))
(let
((val (first (slice rest 1))))
(cond
(= val "full")
"max-height:100%"
(= val "screen")
"max-height:100vh"
(= val "none")
"max-height:none"
:else (let
((n (parse-int val nil)))
(if (nil? n) nil (str "max-height:" (* n 0.25) "rem")))))
(and (= head "min") (>= (len rest) 2) (= (first rest) "w"))
(let
((val-name (join "-" (slice rest 1)))
(val (get tw-min-widths val-name)))
(if (nil? val) nil (str "min-width:" val)))
(and (= head "min") (>= (len rest) 2) (= (first rest) "h"))
(let
((val (first (slice rest 1))))
(cond
(= val "0")
"min-height:0px"
(= val "full")
"min-height:100%"
(= val "screen")
"min-height:100vh"
:else nil))
(and (= head "overflow") (= (len rest) 1))
(str "overflow:" (first rest))
(and (= head "overflow") (= (len rest) 2))
(str "overflow-" (first rest) ":" (nth rest 1))
(and (= head "aspect") (= (len rest) 1))
(case
(first rest)
"auto"
"aspect-ratio:auto"
"square"
"aspect-ratio:1 / 1"
"video"
"aspect-ratio:16 / 9"
:else nil)
(and (= head "object") (= (len rest) 1))
(str "object-fit:" (first rest))
(and (= (len parts) 1) (= head "visible"))
"visibility:visible"
(and (= (len parts) 1) (= head "invisible"))
"visibility:hidden"
(and (= (len parts) 1) (= head "collapse"))
"visibility:collapse"
(and (= (len parts) 1) (= head "container"))
"width:100%;max-width:100%"
(and (= (len parts) 1) (= head "isolate"))
"isolation:isolate"
:else nil))))